diff --git a/client/web/web.go b/client/web/web.go index c76cd4955..e42a61c58 100644 --- a/client/web/web.go +++ b/client/web/web.go @@ -474,7 +474,7 @@ func (s *Server) servePostNodeUpdate(w http.ResponseWriter, r *http.Request) { return } - routes, err := netutil.CalcAdvertiseRoutes(postData.AdvertiseRoutes, postData.AdvertiseExitNode) + routes, err := netutil.CalcAdvertiseRoutes(st.TailscaleIPs[0], postData.AdvertiseRoutes, postData.AdvertiseExitNode) if err != nil { w.WriteHeader(http.StatusInternalServerError) json.NewEncoder(w).Encode(mi{"error": err.Error()}) diff --git a/cmd/tailscale/cli/set.go b/cmd/tailscale/cli/set.go index d589da3ae..1c8fc34a8 100644 --- a/cmd/tailscale/cli/set.go +++ b/cmd/tailscale/cli/set.go @@ -140,7 +140,7 @@ func runSet(ctx context.Context, args []string) (retErr error) { return err } if maskedPrefs.AdvertiseRoutesSet { - maskedPrefs.AdvertiseRoutes, err = calcAdvertiseRoutesForSet(advertiseExitNodeSet, advertiseRoutesSet, curPrefs, setArgs) + maskedPrefs.AdvertiseRoutes, err = calcAdvertiseRoutesForSet(st.TailscaleIPs[0], advertiseExitNodeSet, advertiseRoutesSet, curPrefs, setArgs) if err != nil { return err } @@ -174,13 +174,13 @@ func runSet(ctx context.Context, args []string) (retErr error) { // advertiseRoutesSet is whether the --advertise-routes flag was set. // curPrefs is the current Prefs. // setArgs is the parsed command-line arguments. -func calcAdvertiseRoutesForSet(advertiseExitNodeSet, advertiseRoutesSet bool, curPrefs *ipn.Prefs, setArgs setArgsT) (routes []netip.Prefix, err error) { +func calcAdvertiseRoutesForSet(addrV4 netip.Addr, advertiseExitNodeSet, advertiseRoutesSet bool, curPrefs *ipn.Prefs, setArgs setArgsT) (routes []netip.Prefix, err error) { if advertiseExitNodeSet && advertiseRoutesSet { - return netutil.CalcAdvertiseRoutes(setArgs.advertiseRoutes, setArgs.advertiseDefaultRoute) + return netutil.CalcAdvertiseRoutes(addrV4, setArgs.advertiseRoutes, setArgs.advertiseDefaultRoute) } if advertiseRoutesSet { - return netutil.CalcAdvertiseRoutes(setArgs.advertiseRoutes, curPrefs.AdvertisesExitNode()) + return netutil.CalcAdvertiseRoutes(addrV4, setArgs.advertiseRoutes, curPrefs.AdvertisesExitNode()) } if advertiseExitNodeSet { alreadyAdvertisesExitNode := curPrefs.AdvertisesExitNode() diff --git a/cmd/tailscale/cli/up.go b/cmd/tailscale/cli/up.go index 850a56cf2..b55ae0cf1 100644 --- a/cmd/tailscale/cli/up.go +++ b/cmd/tailscale/cli/up.go @@ -228,7 +228,7 @@ func warnf(format string, args ...any) { // 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 := netutil.CalcAdvertiseRoutes(upArgs.advertiseRoutes, upArgs.advertiseDefaultRoute) + routes, err := netutil.CalcAdvertiseRoutes(netip.Addr{}, upArgs.advertiseRoutes, upArgs.advertiseDefaultRoute) if err != nil { return nil, err } diff --git a/ipn/ipnlocal/c2n.go b/ipn/ipnlocal/c2n.go index afe9f56ee..0a4da6113 100644 --- a/ipn/ipnlocal/c2n.go +++ b/ipn/ipnlocal/c2n.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "io" + "log" "net" "net/http" "os" @@ -283,7 +284,9 @@ func (b *LocalBackend) handleC2NWoL(w http.ResponseWriter, r *http.Request) { http.Error(w, "bad method", http.StatusMethodNotAllowed) return } - r.ParseForm() + err := r.ParseForm() + log.Printf("ParseForm=%v: Form=%q, PostForm=%q", err, r.Form, r.PostForm) + var macs []net.HardwareAddr for _, macStr := range r.Form["mac"] { mac, err := net.ParseMAC(macStr) diff --git a/net/dns/resolver/tsdns.go b/net/dns/resolver/tsdns.go index ddb7b6cdb..9503dbe8f 100644 --- a/net/dns/resolver/tsdns.go +++ b/net/dns/resolver/tsdns.go @@ -8,6 +8,7 @@ package resolver import ( "bufio" "context" + "encoding/binary" "encoding/hex" "errors" "fmt" @@ -21,6 +22,7 @@ import ( "strings" "sync" "time" + "unicode" dns "golang.org/x/net/dns/dnsmessage" "tailscale.com/control/controlknobs" @@ -680,17 +682,21 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netip.Addr, // (2022-06-02) to work around an issue in Chrome where it would treat // "http://via-1.1.2.3.4" as a search string instead of a URL. We should rip out // the old format in early 2023. -func (r *Resolver) parseViaDomain(domain dnsname.FQDN, typ dns.Type) (netip.Addr, bool) { - fqdn := string(domain.WithoutTrailingDot()) +func (r *Resolver) parseViaDomain(domainFQDN dnsname.FQDN, typ dns.Type) (netip.Addr, bool) { + fqdn := string(domainFQDN.WithoutTrailingDot()) if typ != dns.TypeAAAA { return netip.Addr{}, false } if len(fqdn) < len("via-X.0.0.0.0") { return netip.Addr{}, false // too short to be valid } + r.mu.Lock() + hosts := r.hostToIP + r.mu.Unlock() var siteID string var ip4Str string + var prefix uint32 switch { case strings.Contains(fqdn, "-via-"): // Format number 3: "192-168-1-2-via-7" or "192-168-1-2-via-7.foo.ts.net." @@ -703,8 +709,24 @@ func (r *Resolver) parseViaDomain(domain dnsname.FQDN, typ dns.Type) (netip.Addr if !ok { return netip.Addr{}, false } - siteID = suffix ip4Str = strings.ReplaceAll(v4hyphens, "-", ".") + if strings.ContainsFunc(suffix, unicode.IsLetter) { + // Advertising a whole LAN case. IPv4 address via a specific named node + // ("10-0-0-1-via-appletv.foo.ts.net.") where suffix here is + // "appletv" and not a numeric site ID. + _, node, _ := strings.Cut(domainFQDN.WithTrailingDot(), "-via-") + for _, addr := range hosts[dnsname.FQDN(node)] { + if addr.Is4() { + a4 := addr.As4() + prefix = binary.BigEndian.Uint32(a4[:]) + } + } + if prefix == 0 { + return netip.Addr{}, false + } + } else { + siteID = suffix + } case strings.HasPrefix(fqdn, "via-"): firstDot := strings.Index(fqdn, ".") if firstDot < 0 { @@ -730,13 +752,16 @@ func (r *Resolver) parseViaDomain(domain dnsname.FQDN, typ dns.Type) (netip.Addr return netip.Addr{}, false // badly formed, don't respond } - prefix, err := strconv.ParseUint(siteID, 0, 32) - if err != nil { - return netip.Addr{}, false // badly formed, don't respond + if prefix == 0 { + prefix64, err := strconv.ParseUint(siteID, 0, 32) + if err != nil { + return netip.Addr{}, false // badly formed, don't respond + } + prefix = uint32(prefix64) } // MapVia will never error when given an IPv4 netip.Prefix. - out, _ := tsaddr.MapVia(uint32(prefix), netip.PrefixFrom(ip4, ip4.BitLen())) + out, _ := tsaddr.MapVia(prefix, netip.PrefixFrom(ip4, ip4.BitLen())) return out.Addr(), true } diff --git a/net/dns/resolver/tsdns_test.go b/net/dns/resolver/tsdns_test.go index 882462012..b96c36ecb 100644 --- a/net/dns/resolver/tsdns_test.go +++ b/net/dns/resolver/tsdns_test.go @@ -32,8 +32,9 @@ import ( ) var ( - testipv4 = netip.MustParseAddr("1.2.3.4") - testipv6 = netip.MustParseAddr("0001:0203:0405:0607:0809:0a0b:0c0d:0e0f") + testipv4 = netip.MustParseAddr("1.2.3.4") + test3ipv4 = netip.MustParseAddr("5.6.7.8") + testipv6 = netip.MustParseAddr("0001:0203:0405:0607:0809:0a0b:0c0d:0e0f") testipv4Arpa = dnsname.FQDN("4.3.2.1.in-addr.arpa.") testipv6Arpa = dnsname.FQDN("f.0.e.0.d.0.c.0.b.0.a.0.9.0.8.0.7.0.6.0.5.0.4.0.3.0.2.0.1.0.0.0.ip6.arpa.") @@ -43,8 +44,9 @@ var ( var dnsCfg = Config{ Hosts: map[dnsname.FQDN][]netip.Addr{ - "test1.ipn.dev.": {testipv4}, - "test2.ipn.dev.": {testipv6}, + "test1.ipn.dev.": {testipv4}, + "test2.ipn.dev.": {testipv6}, + "test3.foo.ts.net.": {test3ipv4}, }, LocalDomains: []dnsname.FQDN{"ipn.dev.", "3.2.1.in-addr.arpa.", "1.0.0.0.ip6.arpa."}, } @@ -352,7 +354,6 @@ func TestResolveLocal(t *testing.T) { // Hyphenated 4via6 format. // Without any suffix domain: - {"via_form3_hex_bare", dnsname.FQDN("1-2-3-4-via-0xff."), dns.TypeAAAA, netip.MustParseAddr("fd7a:115c:a1e0:b1a:0:ff:1.2.3.4"), dns.RCodeSuccess}, {"via_form3_dec_bare", dnsname.FQDN("1-2-3-4-via-1."), dns.TypeAAAA, netip.MustParseAddr("fd7a:115c:a1e0:b1a:0:1:1.2.3.4"), dns.RCodeSuccess}, // With a Tailscale domain: {"via_form3_dec_ts.net", dnsname.FQDN("1-2-3-4-via-1.foo.ts.net."), dns.TypeAAAA, netip.MustParseAddr("fd7a:115c:a1e0:b1a:0:1:1.2.3.4"), dns.RCodeSuccess}, @@ -361,6 +362,8 @@ func TestResolveLocal(t *testing.T) { // suffixes are currently hard-coded and not plumbed via the netmap) {"via_form3_dec_example.com", dnsname.FQDN("1-2-3-4-via-1.example.com."), dns.TypeAAAA, netip.Addr{}, dns.RCodeRefused}, {"via_form3_dec_examplets.net", dnsname.FQDN("1-2-3-4-via-1.examplets.net."), dns.TypeAAAA, netip.Addr{}, dns.RCodeRefused}, + + {"via_lan", dnsname.FQDN("10-20-30-40-via-test3.foo.ts.net."), dns.TypeAAAA, netip.MustParseAddr("fd7a:115c:a1e0:b1a:0506:0708:10.20.30.40"), dns.RCodeSuccess}, } for _, tt := range tests { diff --git a/net/netutil/routes.go b/net/netutil/routes.go index 83f29bf3a..e9a8c019c 100644 --- a/net/netutil/routes.go +++ b/net/netutil/routes.go @@ -41,12 +41,25 @@ func validateViaPrefix(ipp netip.Prefix) error { // 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) { +func CalcAdvertiseRoutes(selfIPv4 netip.Addr, 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 { + if s == "lan-via6" { + if !selfIPv4.IsValid() { + return nil, fmt.Errorf("cannot advertise lan-via6 route until you're connected to Tailscale") + } + a4 := selfIPv4.As4() + selfSiteID := binary.BigEndian.Uint32(a4[:]) + for _, pfxStr := range []string{"10.0.0.0/8", "192.168.0.0/16", "172.16.0.0/12"} { + pfx := netip.MustParsePrefix(pfxStr) + pfx6, _ := tsaddr.MapVia(selfSiteID, pfx) + routeMap[pfx6] = true + } + continue + } ipp, err := netip.ParsePrefix(s) if err != nil { return nil, fmt.Errorf("%q is not a valid IP address or CIDR prefix", s)