From db6e22b0e018bf45db2da56992f419d5b76d3a52 Mon Sep 17 00:00:00 2001 From: Raj Singh Date: Tue, 24 Mar 2026 01:58:49 -0700 Subject: [PATCH] net/tsdial: add VIP service names to dnsmap with correct address family selection Adds VIP service name resolution to the MagicDNS map so that service names like "mydb" and "mydb." resolve to the service VIP address. Uses the same address family iteration as the peer loop to avoid inserting unreachable IPv4 addresses on IPv6-only nodes. Fixes #19097 Signed-off-by: Raj Singh --- net/tsdial/dnsmap.go | 26 ++++++++ net/tsdial/dnsmap_test.go | 131 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) diff --git a/net/tsdial/dnsmap.go b/net/tsdial/dnsmap.go index d7204463f..3c83aca18 100644 --- a/net/tsdial/dnsmap.go +++ b/net/tsdial/dnsmap.go @@ -74,6 +74,32 @@ func dnsMapFromNetworkMap(nm *netmap.NetworkMap) dnsMap { } ret[canonMapKey(rec.Name)] = ip } + // Map VIP service names into the DNS map so that MagicDNS can + // resolve e.g. "mydb" or "mydb." to the service VIP. + // suffix is derived from the self node name; it's only empty when + // the self node is invalid, in which case there's nothing to do. + if suffix != "" { + for svcName, svcAddrs := range nm.GetVIPServiceIPMap() { + bare := svcName.WithoutPrefix() + if bare == "" { + continue + } + var ip netip.Addr + for _, a := range svcAddrs { + if a.Is4() && !have4 { + continue + } + ip = a + break + } + if !ip.IsValid() { + continue + } + fqdn := bare + "." + suffix + ret[canonMapKey(fqdn)] = ip + ret[canonMapKey(bare)] = ip + } + } return ret } diff --git a/net/tsdial/dnsmap_test.go b/net/tsdial/dnsmap_test.go index b2a50fa0c..7250a6cea 100644 --- a/net/tsdial/dnsmap_test.go +++ b/net/tsdial/dnsmap_test.go @@ -4,6 +4,7 @@ package tsdial import ( + "encoding/json" "net/netip" "reflect" "testing" @@ -12,6 +13,15 @@ "tailscale.com/types/netmap" ) +func mustMarshal(t *testing.T, v any) []byte { + t.Helper() + b, err := json.Marshal(v) + if err != nil { + t.Fatal(err) + } + return b +} + func nodeViews(v []*tailcfg.Node) []tailcfg.NodeView { nv := make([]tailcfg.NodeView, len(v)) for i, n := range v { @@ -113,6 +123,127 @@ func TestDNSMapFromNetworkMap(t *testing.T) { "b.tailnet": ip("100::202"), }, }, + { + name: "vip_services", + nm: &netmap.NetworkMap{ + SelfNode: (&tailcfg.Node{ + Name: "foo.tailnet.", + Addresses: []netip.Prefix{ + pfx("100.102.103.104/32"), + pfx("100::123/128"), + }, + CapMap: tailcfg.NodeCapMap{ + tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{ + tailcfg.RawMessage(mustMarshal(t, tailcfg.ServiceIPMappings{ + "svc:mydb": { + netip.MustParseAddr("100.65.32.1"), + netip.MustParseAddr("fd7a:115c:a1e0::1234"), + }, + })), + }, + }, + }).View(), + }, + want: dnsMap{ + "foo": ip("100.102.103.104"), + "foo.tailnet": ip("100.102.103.104"), + "mydb.tailnet": ip("100.65.32.1"), + "mydb": ip("100.65.32.1"), + }, + }, + { + name: "vip_services_v6_only_self", + nm: &netmap.NetworkMap{ + SelfNode: (&tailcfg.Node{ + Name: "foo.tailnet.", + Addresses: []netip.Prefix{ + pfx("100::123/128"), + }, + CapMap: tailcfg.NodeCapMap{ + tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{ + tailcfg.RawMessage(mustMarshal(t, tailcfg.ServiceIPMappings{ + "svc:mydb": { + netip.MustParseAddr("100.65.32.1"), + netip.MustParseAddr("fd7a:115c:a1e0::1234"), + }, + })), + }, + }, + }).View(), + }, + want: dnsMap{ + "foo": ip("100::123"), + "foo.tailnet": ip("100::123"), + "mydb.tailnet": ip("fd7a:115c:a1e0::1234"), + "mydb": ip("fd7a:115c:a1e0::1234"), + }, + }, + { + // VIP service has only IPv4 addrs but self is IPv6-only. + // Should be excluded entirely since no reachable address exists. + name: "vip_services_v4_only_addrs_v6_only_self", + nm: &netmap.NetworkMap{ + SelfNode: (&tailcfg.Node{ + Name: "foo.tailnet.", + Addresses: []netip.Prefix{ + pfx("100::123/128"), + }, + CapMap: tailcfg.NodeCapMap{ + tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{ + tailcfg.RawMessage(mustMarshal(t, tailcfg.ServiceIPMappings{ + "svc:mydb": { + netip.MustParseAddr("100.65.32.1"), + netip.MustParseAddr("100.65.32.2"), + }, + })), + }, + }, + }).View(), + }, + want: dnsMap{ + "foo": ip("100::123"), + "foo.tailnet": ip("100::123"), + // mydb should NOT appear — both addrs are IPv4 and self is v6-only + }, + }, + { + // VIP service name collides with a peer name. + // VIP runs after peers so it overwrites. + name: "vip_service_overwrites_peer", + nm: &netmap.NetworkMap{ + SelfNode: (&tailcfg.Node{ + Name: "foo.tailnet.", + Addresses: []netip.Prefix{ + pfx("100.102.103.104/32"), + pfx("100::123/128"), + }, + CapMap: tailcfg.NodeCapMap{ + tailcfg.NodeAttrServiceHost: []tailcfg.RawMessage{ + tailcfg.RawMessage(mustMarshal(t, tailcfg.ServiceIPMappings{ + "svc:a": { + netip.MustParseAddr("100.65.32.1"), + }, + })), + }, + }, + }).View(), + Peers: nodeViews([]*tailcfg.Node{ + { + Name: "a.tailnet", + Addresses: []netip.Prefix{ + pfx("100.0.0.201/32"), + pfx("100::201/128"), + }, + }, + }), + }, + want: dnsMap{ + "foo": ip("100.102.103.104"), + "foo.tailnet": ip("100.102.103.104"), + "a": ip("100.65.32.1"), // VIP overwrites peer + "a.tailnet": ip("100.65.32.1"), // VIP overwrites peer + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) {