mirror of
https://github.com/tailscale/tailscale.git
synced 2025-05-05 07:01:01 +00:00
tailcfg: add DNS address list for IsWireGuardOnly nodes
Tailscale exit nodes provide DNS service over the peer API, however IsWireGuardOnly nodes do not have a peer API, and instead need client DNS parameters passed in their node description. For Mullvad nodes this will contain the in network 10.64.0.1 address. Updates #9377 Signed-off-by: James Tucker <james@tailscale.com>
This commit is contained in:
parent
335a5aaf9a
commit
e7727db553
@ -693,6 +693,19 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
|
|||||||
if va == nil || vb == nil || *va != *vb {
|
if va == nil || vb == nil || *va != *vb {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
case "ExitNodeDNSResolvers":
|
||||||
|
va, vb := was.ExitNodeDNSResolvers(), views.SliceOfViews(n.ExitNodeDNSResolvers)
|
||||||
|
|
||||||
|
if va.Len() != vb.Len() {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range va.LenIter() {
|
||||||
|
if !va.At(i).Equal(vb.At(i)) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ret != nil {
|
if ret != nil {
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/tstest"
|
"tailscale.com/tstest"
|
||||||
"tailscale.com/tstime"
|
"tailscale.com/tstime"
|
||||||
|
"tailscale.com/types/dnstype"
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/types/netmap"
|
"tailscale.com/types/netmap"
|
||||||
@ -835,6 +836,40 @@ func TestPatchifyPeersChanged(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "change_exitnodednsresolvers",
|
||||||
|
mr0: &tailcfg.MapResponse{
|
||||||
|
Node: &tailcfg.Node{Name: "foo.bar.ts.net."},
|
||||||
|
Peers: []*tailcfg.Node{
|
||||||
|
{ID: 1, ExitNodeDNSResolvers: []*dnstype.Resolver{{Addr: "dns.exmaple.com"}}, Hostinfo: hi},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mr1: &tailcfg.MapResponse{
|
||||||
|
PeersChanged: []*tailcfg.Node{
|
||||||
|
{ID: 1, ExitNodeDNSResolvers: []*dnstype.Resolver{{Addr: "dns2.exmaple.com"}}, Hostinfo: hi},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &tailcfg.MapResponse{
|
||||||
|
PeersChanged: []*tailcfg.Node{
|
||||||
|
{ID: 1, ExitNodeDNSResolvers: []*dnstype.Resolver{{Addr: "dns2.exmaple.com"}}, Hostinfo: hi},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "same_exitnoderesolvers",
|
||||||
|
mr0: &tailcfg.MapResponse{
|
||||||
|
Node: &tailcfg.Node{Name: "foo.bar.ts.net."},
|
||||||
|
Peers: []*tailcfg.Node{
|
||||||
|
{ID: 1, ExitNodeDNSResolvers: []*dnstype.Resolver{{Addr: "dns.exmaple.com"}}, Hostinfo: hi},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mr1: &tailcfg.MapResponse{
|
||||||
|
PeersChanged: []*tailcfg.Node{
|
||||||
|
{ID: 1, ExitNodeDNSResolvers: []*dnstype.Resolver{{Addr: "dns.exmaple.com"}}, Hostinfo: hi},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &tailcfg.MapResponse{},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
@ -336,6 +336,10 @@ type Node struct {
|
|||||||
// is not expected to speak Disco or DERP, and it must have Endpoints in
|
// is not expected to speak Disco or DERP, and it must have Endpoints in
|
||||||
// order to be reachable.
|
// order to be reachable.
|
||||||
IsWireGuardOnly bool `json:",omitempty"`
|
IsWireGuardOnly bool `json:",omitempty"`
|
||||||
|
|
||||||
|
// ExitNodeDNSResolvers is the list of DNS servers that should be used when this
|
||||||
|
// node is marked IsWireGuardOnly and being used as an exit node.
|
||||||
|
ExitNodeDNSResolvers []*dnstype.Resolver `json:",omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisplayName returns the user-facing name for a node which should
|
// DisplayName returns the user-facing name for a node which should
|
||||||
|
@ -65,6 +65,12 @@ func (src *Node) Clone() *Node {
|
|||||||
if dst.SelfNodeV4MasqAddrForThisPeer != nil {
|
if dst.SelfNodeV4MasqAddrForThisPeer != nil {
|
||||||
dst.SelfNodeV4MasqAddrForThisPeer = ptr.To(*src.SelfNodeV4MasqAddrForThisPeer)
|
dst.SelfNodeV4MasqAddrForThisPeer = ptr.To(*src.SelfNodeV4MasqAddrForThisPeer)
|
||||||
}
|
}
|
||||||
|
if src.ExitNodeDNSResolvers != nil {
|
||||||
|
dst.ExitNodeDNSResolvers = make([]*dnstype.Resolver, len(src.ExitNodeDNSResolvers))
|
||||||
|
for i := range dst.ExitNodeDNSResolvers {
|
||||||
|
dst.ExitNodeDNSResolvers[i] = src.ExitNodeDNSResolvers[i].Clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
return dst
|
return dst
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,6 +107,7 @@ var _NodeCloneNeedsRegeneration = Node(struct {
|
|||||||
Expired bool
|
Expired bool
|
||||||
SelfNodeV4MasqAddrForThisPeer *netip.Addr
|
SelfNodeV4MasqAddrForThisPeer *netip.Addr
|
||||||
IsWireGuardOnly bool
|
IsWireGuardOnly bool
|
||||||
|
ExitNodeDNSResolvers []*dnstype.Resolver
|
||||||
}{})
|
}{})
|
||||||
|
|
||||||
// Clone makes a deep copy of Hostinfo.
|
// Clone makes a deep copy of Hostinfo.
|
||||||
|
@ -350,7 +350,7 @@ func TestNodeEqual(t *testing.T) {
|
|||||||
"UnsignedPeerAPIOnly",
|
"UnsignedPeerAPIOnly",
|
||||||
"ComputedName", "computedHostIfDifferent", "ComputedNameWithHost",
|
"ComputedName", "computedHostIfDifferent", "ComputedNameWithHost",
|
||||||
"DataPlaneAuditLogID", "Expired", "SelfNodeV4MasqAddrForThisPeer",
|
"DataPlaneAuditLogID", "Expired", "SelfNodeV4MasqAddrForThisPeer",
|
||||||
"IsWireGuardOnly",
|
"IsWireGuardOnly", "ExitNodeDNSResolvers",
|
||||||
}
|
}
|
||||||
if have := fieldsOf(reflect.TypeOf(Node{})); !reflect.DeepEqual(have, nodeHandles) {
|
if have := fieldsOf(reflect.TypeOf(Node{})); !reflect.DeepEqual(have, nodeHandles) {
|
||||||
t.Errorf("Node.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
t.Errorf("Node.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
||||||
|
@ -181,6 +181,9 @@ func (v NodeView) SelfNodeV4MasqAddrForThisPeer() *netip.Addr {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (v NodeView) IsWireGuardOnly() bool { return v.ж.IsWireGuardOnly }
|
func (v NodeView) IsWireGuardOnly() bool { return v.ж.IsWireGuardOnly }
|
||||||
|
func (v NodeView) ExitNodeDNSResolvers() views.SliceView[*dnstype.Resolver, dnstype.ResolverView] {
|
||||||
|
return views.SliceOfViews[*dnstype.Resolver, dnstype.ResolverView](v.ж.ExitNodeDNSResolvers)
|
||||||
|
}
|
||||||
func (v NodeView) Equal(v2 NodeView) bool { return v.ж.Equal(v2.ж) }
|
func (v NodeView) Equal(v2 NodeView) bool { return v.ж.Equal(v2.ж) }
|
||||||
|
|
||||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||||
@ -216,6 +219,7 @@ var _NodeViewNeedsRegeneration = Node(struct {
|
|||||||
Expired bool
|
Expired bool
|
||||||
SelfNodeV4MasqAddrForThisPeer *netip.Addr
|
SelfNodeV4MasqAddrForThisPeer *netip.Addr
|
||||||
IsWireGuardOnly bool
|
IsWireGuardOnly bool
|
||||||
|
ExitNodeDNSResolvers []*dnstype.Resolver
|
||||||
}{})
|
}{})
|
||||||
|
|
||||||
// View returns a readonly view of Hostinfo.
|
// View returns a readonly view of Hostinfo.
|
||||||
|
@ -8,6 +8,7 @@ package dnstype
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Resolver is the configuration for one DNS resolver.
|
// Resolver is the configuration for one DNS resolver.
|
||||||
@ -51,3 +52,15 @@ func (r *Resolver) IPPort() (ipp netip.AddrPort, ok bool) {
|
|||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Equal reports whether r and other are equal.
|
||||||
|
func (r *Resolver) Equal(other *Resolver) bool {
|
||||||
|
if r == nil || other == nil {
|
||||||
|
return r == other
|
||||||
|
}
|
||||||
|
if r == other {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.Addr == other.Addr && slices.Equal(r.BootstrapResolution, other.BootstrapResolution)
|
||||||
|
}
|
||||||
|
81
types/dnstype/dnstype_test.go
Normal file
81
types/dnstype/dnstype_test.go
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
// Copyright (c) Tailscale Inc & AUTHORS
|
||||||
|
// SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
package dnstype
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/netip"
|
||||||
|
"reflect"
|
||||||
|
"slices"
|
||||||
|
"sort"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResolverEqual(t *testing.T) {
|
||||||
|
var fieldNames []string
|
||||||
|
for _, field := range reflect.VisibleFields(reflect.TypeOf(Resolver{})) {
|
||||||
|
fieldNames = append(fieldNames, field.Name)
|
||||||
|
}
|
||||||
|
sort.Strings(fieldNames)
|
||||||
|
if !slices.Equal(fieldNames, []string{"Addr", "BootstrapResolution"}) {
|
||||||
|
t.Errorf("Resolver fields changed; update test")
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
a, b *Resolver
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil",
|
||||||
|
a: nil,
|
||||||
|
b: nil,
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "nil vs non-nil",
|
||||||
|
a: nil,
|
||||||
|
b: &Resolver{},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-nil vs nil",
|
||||||
|
a: &Resolver{},
|
||||||
|
b: nil,
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "equal",
|
||||||
|
a: &Resolver{Addr: "dns.example.com"},
|
||||||
|
b: &Resolver{Addr: "dns.example.com"},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not equal addrs",
|
||||||
|
a: &Resolver{Addr: "dns.example.com"},
|
||||||
|
b: &Resolver{Addr: "dns2.example.com"},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not equal bootstrap",
|
||||||
|
a: &Resolver{
|
||||||
|
Addr: "dns.example.com",
|
||||||
|
BootstrapResolution: []netip.Addr{netip.MustParseAddr("8.8.8.8")},
|
||||||
|
},
|
||||||
|
b: &Resolver{
|
||||||
|
Addr: "dns.example.com",
|
||||||
|
BootstrapResolution: []netip.Addr{netip.MustParseAddr("8.8.4.4")},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := tt.a.Equal(tt.b)
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("got %v; want %v", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -64,6 +64,7 @@ func (v ResolverView) Addr() string { return v.ж.Addr }
|
|||||||
func (v ResolverView) BootstrapResolution() views.Slice[netip.Addr] {
|
func (v ResolverView) BootstrapResolution() views.Slice[netip.Addr] {
|
||||||
return views.SliceOf(v.ж.BootstrapResolution)
|
return views.SliceOf(v.ж.BootstrapResolution)
|
||||||
}
|
}
|
||||||
|
func (v ResolverView) Equal(v2 ResolverView) bool { return v.ж.Equal(v2.ж) }
|
||||||
|
|
||||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||||
var _ResolverViewNeedsRegeneration = Resolver(struct {
|
var _ResolverViewNeedsRegeneration = Resolver(struct {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user