From 69f1324c9e3b169bb8e71cf4584a7c276545e95d Mon Sep 17 00:00:00 2001 From: Will Norris Date: Tue, 8 Aug 2023 16:21:17 -0700 Subject: [PATCH] cmd/tailscale: refactor shared utility methods Refactor two shared functions used by the tailscale cli, calcAdvertiseRoutes and licensesURL. These are used by the web client as well as other tailscale subcommands. The web client is being moved out of the cli package, so move these two functions to new locations. Updates tailscale/corp#13775 Signed-off-by: Will Norris --- cmd/tailscale/cli/licenses.go | 20 ++------ cmd/tailscale/cli/set.go | 5 +- cmd/tailscale/cli/up.go | 81 +----------------------------- cmd/tailscale/cli/web.go | 6 ++- cmd/tailscale/depaware.txt | 1 + licenses/licenses.go | 21 ++++++++ net/netutil/routes.go | 93 +++++++++++++++++++++++++++++++++++ 7 files changed, 127 insertions(+), 100 deletions(-) create mode 100644 licenses/licenses.go create mode 100644 net/netutil/routes.go diff --git a/cmd/tailscale/cli/licenses.go b/cmd/tailscale/cli/licenses.go index 3183b5809..72c0b80fd 100644 --- a/cmd/tailscale/cli/licenses.go +++ b/cmd/tailscale/cli/licenses.go @@ -5,9 +5,9 @@ import ( "context" - "runtime" "github.com/peterbourgon/ff/v3/ffcli" + "tailscale.com/licenses" ) var licensesCmd = &ffcli.Command{ @@ -18,27 +18,13 @@ Exec: runLicenses, } -// licensesURL returns the absolute URL containing open source license information for the current platform. -func licensesURL() string { - switch runtime.GOOS { - case "android": - return "https://tailscale.com/licenses/android" - case "darwin", "ios": - return "https://tailscale.com/licenses/apple" - case "windows": - return "https://tailscale.com/licenses/windows" - default: - return "https://tailscale.com/licenses/tailscale" - } -} - func runLicenses(ctx context.Context, args []string) error { - licenses := licensesURL() + url := licenses.LicensesURL() outln(` Tailscale wouldn't be possible without the contributions of thousands of open source developers. To see the open source packages included in Tailscale and their respective license information, visit: - ` + licenses) + ` + url) return nil } diff --git a/cmd/tailscale/cli/set.go b/cmd/tailscale/cli/set.go index 5ed2fe284..fdd332060 100644 --- a/cmd/tailscale/cli/set.go +++ b/cmd/tailscale/cli/set.go @@ -12,6 +12,7 @@ "github.com/peterbourgon/ff/v3/ffcli" "tailscale.com/ipn" + "tailscale.com/net/netutil" "tailscale.com/net/tsaddr" "tailscale.com/safesocket" ) @@ -159,11 +160,11 @@ func runSet(ctx context.Context, args []string) (retErr error) { // setArgs is the parsed command-line arguments. func calcAdvertiseRoutesForSet(advertiseExitNodeSet, advertiseRoutesSet bool, curPrefs *ipn.Prefs, setArgs setArgsT) (routes []netip.Prefix, err error) { if advertiseExitNodeSet && advertiseRoutesSet { - return calcAdvertiseRoutes(setArgs.advertiseRoutes, setArgs.advertiseDefaultRoute) + return netutil.CalcAdvertiseRoutes(setArgs.advertiseRoutes, setArgs.advertiseDefaultRoute) } if advertiseRoutesSet { - return calcAdvertiseRoutes(setArgs.advertiseRoutes, curPrefs.AdvertisesExitNode()) + return netutil.CalcAdvertiseRoutes(setArgs.advertiseRoutes, curPrefs.AdvertisesExitNode()) } if advertiseExitNodeSet { alreadyAdvertisesExitNode := curPrefs.AdvertisesExitNode() diff --git a/cmd/tailscale/cli/up.go b/cmd/tailscale/cli/up.go index aa23a8cdb..d0cded47e 100644 --- a/cmd/tailscale/cli/up.go +++ b/cmd/tailscale/cli/up.go @@ -6,7 +6,6 @@ import ( "context" "encoding/base64" - "encoding/binary" "encoding/json" "errors" "flag" @@ -33,7 +32,7 @@ "tailscale.com/health/healthmsg" "tailscale.com/ipn" "tailscale.com/ipn/ipnstate" - "tailscale.com/net/tsaddr" + "tailscale.com/net/netutil" "tailscale.com/safesocket" "tailscale.com/tailcfg" "tailscale.com/types/logger" @@ -220,82 +219,6 @@ func warnf(format string, args ...any) { printf("Warning: "+format+"\n", args...) } -var ( - ipv4default = netip.MustParsePrefix("0.0.0.0/0") - ipv6default = netip.MustParsePrefix("::/0") -) - -func validateViaPrefix(ipp netip.Prefix) error { - if !tsaddr.IsViaPrefix(ipp) { - return fmt.Errorf("%v is not a 4-in-6 prefix", ipp) - } - if ipp.Bits() < (128 - 32) { - return fmt.Errorf("%v 4-in-6 prefix must be at least a /%v", ipp, 128-32) - } - a := ipp.Addr().As16() - // The first 64 bits of a are the via prefix. - // The next 32 bits are the "site ID". - // The last 32 bits are the IPv4. - // For now, we reserve the top 3 bytes of the site ID, - // and only allow users to use site IDs 0-255. - siteID := binary.BigEndian.Uint32(a[8:12]) - if siteID > 0xFF { - return fmt.Errorf("route %v contains invalid site ID %08x; must be 0xff or less", ipp, siteID) - } - return nil -} - -func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([]netip.Prefix, error) { - routeMap := map[netip.Prefix]bool{} - if advertiseRoutes != "" { - var default4, default6 bool - advroutes := strings.Split(advertiseRoutes, ",") - for _, s := range advroutes { - ipp, err := netip.ParsePrefix(s) - if err != nil { - return nil, fmt.Errorf("%q is not a valid IP address or CIDR prefix", s) - } - if ipp != ipp.Masked() { - return nil, fmt.Errorf("%s has non-address bits set; expected %s", ipp, ipp.Masked()) - } - if tsaddr.IsViaPrefix(ipp) { - if err := validateViaPrefix(ipp); err != nil { - return nil, err - } - } - if ipp == ipv4default { - default4 = true - } else if ipp == ipv6default { - default6 = true - } - routeMap[ipp] = true - } - if default4 && !default6 { - return nil, fmt.Errorf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv4default, ipv6default) - } else if default6 && !default4 { - return nil, fmt.Errorf("%s advertised without its IPv4 counterpart, please also advertise %s", ipv6default, ipv4default) - } - } - if advertiseDefaultRoute { - routeMap[netip.MustParsePrefix("0.0.0.0/0")] = true - routeMap[netip.MustParsePrefix("::/0")] = true - } - if len(routeMap) == 0 { - return nil, nil - } - routes := make([]netip.Prefix, 0, len(routeMap)) - for r := range routeMap { - routes = append(routes, r) - } - sort.Slice(routes, func(i, j int) bool { - if routes[i].Bits() != routes[j].Bits() { - return routes[i].Bits() < routes[j].Bits() - } - return routes[i].Addr().Less(routes[j].Addr()) - }) - return routes, nil -} - // prefsFromUpArgs returns the ipn.Prefs for the provided args. // // Note that the parameters upArgs and warnf are named intentionally @@ -303,7 +226,7 @@ func calcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([] // function exists for testing and should have no side effects or // outside interactions (e.g. no making Tailscale LocalAPI calls). func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goos string) (*ipn.Prefs, error) { - routes, err := calcAdvertiseRoutes(upArgs.advertiseRoutes, upArgs.advertiseDefaultRoute) + routes, err := netutil.CalcAdvertiseRoutes(upArgs.advertiseRoutes, upArgs.advertiseDefaultRoute) if err != nil { return nil, err } diff --git a/cmd/tailscale/cli/web.go b/cmd/tailscale/cli/web.go index f29eac26a..04ae3aa92 100644 --- a/cmd/tailscale/cli/web.go +++ b/cmd/tailscale/cli/web.go @@ -28,6 +28,8 @@ "tailscale.com/envknob" "tailscale.com/ipn" "tailscale.com/ipn/ipnstate" + "tailscale.com/licenses" + "tailscale.com/net/netutil" "tailscale.com/tailcfg" "tailscale.com/util/cmpx" "tailscale.com/util/groupmember" @@ -385,7 +387,7 @@ func webHandler(w http.ResponseWriter, r *http.Request) { return } - routes, err := calcAdvertiseRoutes(postData.AdvertiseRoutes, postData.AdvertiseExitNode) + routes, err := netutil.CalcAdvertiseRoutes(postData.AdvertiseRoutes, postData.AdvertiseExitNode) if err != nil { w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(mi{"error": err.Error()}) @@ -437,7 +439,7 @@ func webHandler(w http.ResponseWriter, r *http.Request) { Profile: profile, Status: st.BackendState, DeviceName: deviceName, - LicensesURL: licensesURL(), + LicensesURL: licenses.LicensesURL(), TUNMode: st.TUN, IsSynology: distro.Get() == distro.Synology || envknob.Bool("TS_FAKE_SYNOLOGY"), DSMVersion: distro.DSMVersion(), diff --git a/cmd/tailscale/depaware.txt b/cmd/tailscale/depaware.txt index 0d0152a9f..d8af6cb42 100644 --- a/cmd/tailscale/depaware.txt +++ b/cmd/tailscale/depaware.txt @@ -81,6 +81,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep tailscale.com/hostinfo from tailscale.com/net/interfaces+ tailscale.com/ipn from tailscale.com/cmd/tailscale/cli+ tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+ + tailscale.com/licenses from tailscale.com/cmd/tailscale/cli tailscale.com/metrics from tailscale.com/derp tailscale.com/net/dns/recursive from tailscale.com/net/dnsfallback tailscale.com/net/dnscache from tailscale.com/derp/derphttp+ diff --git a/licenses/licenses.go b/licenses/licenses.go new file mode 100644 index 000000000..5e59edb9f --- /dev/null +++ b/licenses/licenses.go @@ -0,0 +1,21 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +// Package licenses provides utilities for working with open source licenses. +package licenses + +import "runtime" + +// LicensesURL returns the absolute URL containing open source license information for the current platform. +func LicensesURL() string { + switch runtime.GOOS { + case "android": + return "https://tailscale.com/licenses/android" + case "darwin", "ios": + return "https://tailscale.com/licenses/apple" + case "windows": + return "https://tailscale.com/licenses/windows" + default: + return "https://tailscale.com/licenses/tailscale" + } +} diff --git a/net/netutil/routes.go b/net/netutil/routes.go new file mode 100644 index 000000000..83f29bf3a --- /dev/null +++ b/net/netutil/routes.go @@ -0,0 +1,93 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package netutil + +import ( + "encoding/binary" + "fmt" + "net/netip" + "sort" + "strings" + + "tailscale.com/net/tsaddr" +) + +var ( + ipv4default = netip.MustParsePrefix("0.0.0.0/0") + ipv6default = netip.MustParsePrefix("::/0") +) + +func validateViaPrefix(ipp netip.Prefix) error { + if !tsaddr.IsViaPrefix(ipp) { + return fmt.Errorf("%v is not a 4-in-6 prefix", ipp) + } + if ipp.Bits() < (128 - 32) { + return fmt.Errorf("%v 4-in-6 prefix must be at least a /%v", ipp, 128-32) + } + a := ipp.Addr().As16() + // The first 64 bits of a are the via prefix. + // The next 32 bits are the "site ID". + // The last 32 bits are the IPv4. + // For now, we reserve the top 3 bytes of the site ID, + // and only allow users to use site IDs 0-255. + siteID := binary.BigEndian.Uint32(a[8:12]) + if siteID > 0xFF { + return fmt.Errorf("route %v contains invalid site ID %08x; must be 0xff or less", ipp, siteID) + } + return nil +} + +// CalcAdvertiseRoutes calculates the requested routes to be advertised by a node. +// advertiseRoutes is the user-provided, comma-separated list of routes (IP addresses or CIDR prefixes) to advertise. +// advertiseDefaultRoute indicates whether the node should act as an exit node and advertise default routes. +func CalcAdvertiseRoutes(advertiseRoutes string, advertiseDefaultRoute bool) ([]netip.Prefix, error) { + routeMap := map[netip.Prefix]bool{} + if advertiseRoutes != "" { + var default4, default6 bool + advroutes := strings.Split(advertiseRoutes, ",") + for _, s := range advroutes { + ipp, err := netip.ParsePrefix(s) + if err != nil { + return nil, fmt.Errorf("%q is not a valid IP address or CIDR prefix", s) + } + if ipp != ipp.Masked() { + return nil, fmt.Errorf("%s has non-address bits set; expected %s", ipp, ipp.Masked()) + } + if tsaddr.IsViaPrefix(ipp) { + if err := validateViaPrefix(ipp); err != nil { + return nil, err + } + } + if ipp == ipv4default { + default4 = true + } else if ipp == ipv6default { + default6 = true + } + routeMap[ipp] = true + } + if default4 && !default6 { + return nil, fmt.Errorf("%s advertised without its IPv6 counterpart, please also advertise %s", ipv4default, ipv6default) + } else if default6 && !default4 { + return nil, fmt.Errorf("%s advertised without its IPv4 counterpart, please also advertise %s", ipv6default, ipv4default) + } + } + if advertiseDefaultRoute { + routeMap[netip.MustParsePrefix("0.0.0.0/0")] = true + routeMap[netip.MustParsePrefix("::/0")] = true + } + if len(routeMap) == 0 { + return nil, nil + } + routes := make([]netip.Prefix, 0, len(routeMap)) + for r := range routeMap { + routes = append(routes, r) + } + sort.Slice(routes, func(i, j int) bool { + if routes[i].Bits() != routes[j].Bits() { + return routes[i].Bits() < routes[j].Bits() + } + return routes[i].Addr().Less(routes[j].Addr()) + }) + return routes, nil +}