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) {