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 +}