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.<tailnet>" 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 <raj@tailscale.com>
This commit is contained in:
Raj Singh
2026-03-24 01:58:49 -07:00
parent 44ec71cf94
commit db6e22b0e0
2 changed files with 157 additions and 0 deletions

View File

@@ -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.<tailnet>" 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
}

View File

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