feature/conn25: add IPv6 support

Make the DNS handling portions of conn25 work with IPv6 addresses.

Fixes tailscale/corp#37850

Signed-off-by: Fran Bull <fran@tailscale.com>
main
Fran Bull 2 weeks ago committed by franbull
parent 1f84729908
commit 96c3ad582b
  1. 133
      feature/conn25/conn25.go
  2. 353
      feature/conn25/conn25_test.go
  3. 8
      types/appctype/appconnector.go

@ -229,7 +229,7 @@ func (c *Conn25) ConnectorRealIPForTransitIPConnection(src, transit netip.Addr)
func (e *extension) getMagicRange() views.Slice[netip.Prefix] { func (e *extension) getMagicRange() views.Slice[netip.Prefix] {
cfg := e.conn25.client.getConfig() cfg := e.conn25.client.getConfig()
return views.SliceOf(cfg.magicIPSet.Prefixes()) return views.SliceOf(slices.Concat(cfg.v4MagicIPSet.Prefixes(), cfg.v6MagicIPSet.Prefixes()))
} }
// Shutdown implements [ipnlocal.Extension]. // Shutdown implements [ipnlocal.Extension].
@ -493,8 +493,10 @@ type config struct {
appsByName map[string]appctype.Conn25Attr appsByName map[string]appctype.Conn25Attr
appNamesByDomain map[dnsname.FQDN][]string appNamesByDomain map[dnsname.FQDN][]string
selfRoutedDomains set.Set[dnsname.FQDN] selfRoutedDomains set.Set[dnsname.FQDN]
transitIPSet netipx.IPSet v4TransitIPSet netipx.IPSet
magicIPSet netipx.IPSet v4MagicIPSet netipx.IPSet
v6TransitIPSet netipx.IPSet
v6MagicIPSet netipx.IPSet
} }
func configFromNodeView(n tailcfg.NodeView) (config, error) { func configFromNodeView(n tailcfg.NodeView) (config, error) {
@ -531,16 +533,26 @@ func configFromNodeView(n tailcfg.NodeView) (config, error) {
// global IP pool config. For now just take it from the first app. // global IP pool config. For now just take it from the first app.
if len(apps) != 0 { if len(apps) != 0 {
app := apps[0] app := apps[0]
mipp, err := ipSetFromIPRanges(app.MagicIPPool) v4Mipp, err := ipSetFromIPRanges(app.V4MagicIPPool)
if err != nil { if err != nil {
return config{}, err return config{}, err
} }
tipp, err := ipSetFromIPRanges(app.TransitIPPool) v4Tipp, err := ipSetFromIPRanges(app.V4TransitIPPool)
if err != nil { if err != nil {
return config{}, err return config{}, err
} }
cfg.magicIPSet = *mipp v6Mipp, err := ipSetFromIPRanges(app.V6MagicIPPool)
cfg.transitIPSet = *tipp if err != nil {
return config{}, err
}
v6Tipp, err := ipSetFromIPRanges(app.V6TransitIPPool)
if err != nil {
return config{}, err
}
cfg.v4MagicIPSet = *v4Mipp
cfg.v4TransitIPSet = *v4Tipp
cfg.v6MagicIPSet = *v6Mipp
cfg.v6TransitIPSet = *v6Tipp
} }
return cfg, nil return cfg, nil
} }
@ -553,11 +565,13 @@ type client struct {
logf logger.Logf logf logger.Logf
addrsCh chan addrs addrsCh chan addrs
mu sync.Mutex // protects the fields below mu sync.Mutex // protects the fields below
magicIPPool *ippool v4MagicIPPool *ippool
transitIPPool *ippool v4TransitIPPool *ippool
assignments addrAssignments v6MagicIPPool *ippool
config config v6TransitIPPool *ippool
assignments addrAssignments
config config
} }
func (c *client) getConfig() config { func (c *client) getConfig() config {
@ -575,7 +589,7 @@ func (c *client) transitIPForMagicIP(magicIP netip.Addr) (netip.Addr, error) {
if ok { if ok {
return v.transit, nil return v.transit, nil
} }
if !c.config.magicIPSet.Contains(magicIP) { if !c.config.v4MagicIPSet.Contains(magicIP) && !c.config.v6MagicIPSet.Contains(magicIP) {
return netip.Addr{}, nil return netip.Addr{}, nil
} }
return netip.Addr{}, ErrUnmappedMagicIP return netip.Addr{}, ErrUnmappedMagicIP
@ -613,8 +627,10 @@ func (c *client) reconfig(newCfg config) error {
c.config = newCfg c.config = newCfg
c.magicIPPool = newIPPool(&(newCfg.magicIPSet)) c.v4MagicIPPool = newIPPool(&(newCfg.v4MagicIPSet))
c.transitIPPool = newIPPool(&(newCfg.transitIPSet)) c.v4TransitIPPool = newIPPool(&(newCfg.v4TransitIPSet))
c.v6MagicIPPool = newIPPool(&(newCfg.v6MagicIPSet))
c.v6TransitIPPool = newIPPool(&(newCfg.v6TransitIPSet))
return nil return nil
} }
@ -630,6 +646,9 @@ func (c *client) isConnectorDomain(domain dnsname.FQDN) bool {
// It checks that this domain should be routed and that this client is not itself a connector for the domain // It checks that this domain should be routed and that this client is not itself a connector for the domain
// and generally if it is valid to make the assignment. // and generally if it is valid to make the assignment.
func (c *client) reserveAddresses(domain dnsname.FQDN, dst netip.Addr) (addrs, error) { func (c *client) reserveAddresses(domain dnsname.FQDN, dst netip.Addr) (addrs, error) {
if !dst.IsValid() {
return addrs{}, errors.New("dst is not valid")
}
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
if existing, ok := c.assignments.lookupByDomainDst(domain, dst); ok { if existing, ok := c.assignments.lookupByDomainDst(domain, dst); ok {
@ -641,13 +660,29 @@ func (c *client) reserveAddresses(domain dnsname.FQDN, dst netip.Addr) (addrs, e
} }
// only reserve for first app // only reserve for first app
app := appNames[0] app := appNames[0]
mip, err := c.magicIPPool.next()
if err != nil { var mip, tip netip.Addr
return addrs{}, err var err error
} if dst.Is4() {
tip, err := c.transitIPPool.next() mip, err = c.v4MagicIPPool.next()
if err != nil { if err != nil {
return addrs{}, err return addrs{}, err
}
tip, err = c.v4TransitIPPool.next()
if err != nil {
return addrs{}, err
}
} else if dst.Is6() {
mip, err = c.v6MagicIPPool.next()
if err != nil {
return addrs{}, err
}
tip, err = c.v6TransitIPPool.next()
if err != nil {
return addrs{}, err
}
} else {
return addrs{}, errors.New("unexpected neither 4 nor 6")
} }
as := addrs{ as := addrs{
dst: dst, dst: dst,
@ -856,8 +891,7 @@ func (c *client) mapDNSResponse(buf []byte) []byte {
// * not send through the additional section // * not send through the additional section
// * provide our answers, or no answers if we don't handle those answers (possibly in the future we should write through answers for eg TypeTXT) // * provide our answers, or no answers if we don't handle those answers (possibly in the future we should write through answers for eg TypeTXT)
var answers []dnsResponseRewrite var answers []dnsResponseRewrite
if question.Type != dnsmessage.TypeA { if question.Type != dnsmessage.TypeA && question.Type != dnsmessage.TypeAAAA {
// we plan to support TypeAAAA soon (2026-03-11)
c.logf("mapping dns response for connector domain, unsupported type: %v", question.Type) c.logf("mapping dns response for connector domain, unsupported type: %v", question.Type)
newBuf, err := c.rewriteDNSResponse(hdr, questions, answers) newBuf, err := c.rewriteDNSResponse(hdr, questions, answers)
if err != nil { if err != nil {
@ -895,7 +929,15 @@ func (c *client) mapDNSResponse(buf []byte) []byte {
c.logf("error parsing dns response: %v", err) c.logf("error parsing dns response: %v", err)
return makeServFail(c.logf, hdr, question) return makeServFail(c.logf, hdr, question)
} }
case dnsmessage.TypeA: case dnsmessage.TypeA, dnsmessage.TypeAAAA:
if h.Type != question.Type {
// would not expect a v4 response to a v6 question or vice versa, don't add a rewrite for this.
if err := p.SkipAnswer(); err != nil {
c.logf("error parsing dns response: %v", err)
return makeServFail(c.logf, hdr, question)
}
continue
}
domain, err := normalizeDNSName(h.Name.String()) domain, err := normalizeDNSName(h.Name.String())
if err != nil { if err != nil {
c.logf("bad dnsname: %v", err) c.logf("bad dnsname: %v", err)
@ -910,12 +952,23 @@ func (c *client) mapDNSResponse(buf []byte) []byte {
} }
continue continue
} }
r, err := p.AResource() var dstAddr netip.Addr
if err != nil { if h.Type == dnsmessage.TypeA {
c.logf("error parsing dns response: %v", err) r, err := p.AResource()
return makeServFail(c.logf, hdr, question) if err != nil {
c.logf("error parsing dns response: %v", err)
return makeServFail(c.logf, hdr, question)
}
dstAddr = netip.AddrFrom4(r.A)
} else {
r, err := p.AAAAResource()
if err != nil {
c.logf("error parsing dns response: %v", err)
return makeServFail(c.logf, hdr, question)
}
dstAddr = netip.AddrFrom16(r.AAAA)
} }
answers = append(answers, dnsResponseRewrite{domain: domain, dst: netip.AddrFrom4(r.A)}) answers = append(answers, dnsResponseRewrite{domain: domain, dst: dstAddr})
default: default:
// we already checked the question was for a supported type, this answer is unexpected // we already checked the question was for a supported type, this answer is unexpected
c.logf("unexpected type for connector domain dns response: %v %v", queriedDomain, h.Type) c.logf("unexpected type for connector domain dns response: %v %v", queriedDomain, h.Type)
@ -934,8 +987,6 @@ func (c *client) mapDNSResponse(buf []byte) []byte {
} }
func (c *client) rewriteDNSResponse(hdr dnsmessage.Header, questions []dnsmessage.Question, answers []dnsResponseRewrite) ([]byte, error) { func (c *client) rewriteDNSResponse(hdr dnsmessage.Header, questions []dnsmessage.Question, answers []dnsResponseRewrite) ([]byte, error) {
// We are currently (2026-03-10) only doing this for AResource records, we know that if we are here
// with non-empty answers, the type was AResource.
b := dnsmessage.NewBuilder(nil, hdr) b := dnsmessage.NewBuilder(nil, hdr)
b.EnableCompression() b.EnableCompression()
if err := b.StartQuestions(); err != nil { if err := b.StartQuestions(); err != nil {
@ -963,10 +1014,18 @@ func (c *client) rewriteDNSResponse(hdr dnsmessage.Header, questions []dnsmessag
if err != nil { if err != nil {
return nil, err return nil, err
} }
// only handling TypeA right now if rw.dst.Is4() {
rhdr := dnsmessage.ResourceHeader{Name: name, Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET, TTL: 0} rhdr := dnsmessage.ResourceHeader{Name: name, Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET, TTL: 0}
if err := b.AResource(rhdr, dnsmessage.AResource{A: as.magic.As4()}); err != nil { if err := b.AResource(rhdr, dnsmessage.AResource{A: as.magic.As4()}); err != nil {
return nil, err return nil, err
}
} else if rw.dst.Is6() {
rhdr := dnsmessage.ResourceHeader{Name: name, Type: dnsmessage.TypeAAAA, Class: dnsmessage.ClassINET, TTL: 0}
if err := b.AAAAResource(rhdr, dnsmessage.AAAAResource{AAAA: as.magic.As16()}); err != nil {
return nil, err
}
} else {
return nil, errors.New("unexpected neither 4 nor 6")
} }
} }
// We do _not_ include the additional section in our rewrite. (We don't want to include // We do _not_ include the additional section in our rewrite. (We don't want to include
@ -997,7 +1056,7 @@ func (c *connector) realIPForTransitIPConnection(srcIP netip.Addr, transitIP net
if ok { if ok {
return v.addr, nil return v.addr, nil
} }
if !c.config.transitIPSet.Contains(transitIP) { if !c.config.v4TransitIPSet.Contains(transitIP) && !c.config.v6TransitIPSet.Contains(transitIP) {
return netip.Addr{}, nil return netip.Addr{}, nil
} }
return netip.Addr{}, ErrUnmappedSrcAndTransitIP return netip.Addr{}, ErrUnmappedSrcAndTransitIP

@ -390,38 +390,57 @@ func TestHandleConnectorTransitIPRequest(t *testing.T) {
func TestReserveIPs(t *testing.T) { func TestReserveIPs(t *testing.T) {
c := newConn25(logger.Discard) c := newConn25(logger.Discard)
c.client.magicIPPool = newIPPool(mustIPSetFromPrefix("100.64.0.0/24")) c.client.v4MagicIPPool = newIPPool(mustIPSetFromPrefix("100.64.0.0/24"))
c.client.transitIPPool = newIPPool(mustIPSetFromPrefix("169.254.0.0/24")) c.client.v6MagicIPPool = newIPPool(mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0100::/80"))
c.client.v4TransitIPPool = newIPPool(mustIPSetFromPrefix("169.254.0.0/24"))
c.client.v6TransitIPPool = newIPPool(mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0200::/80"))
app := "a"
domainStr := "example.com."
mbd := map[dnsname.FQDN][]string{} mbd := map[dnsname.FQDN][]string{}
mbd["example.com."] = []string{"a"} mbd["example.com."] = []string{app}
c.client.config.appNamesByDomain = mbd c.client.config.appNamesByDomain = mbd
domain := must.Get(dnsname.ToFQDN(domainStr))
dst := netip.MustParseAddr("0.0.0.1") for _, tt := range []struct {
addrs, err := c.client.reserveAddresses("example.com.", dst) name string
if err != nil { dst netip.Addr
t.Fatal(err) wantMagic netip.Addr
} wantTransit netip.Addr
}{
wantDst := netip.MustParseAddr("0.0.0.1") // same as dst we pass in {
wantMagic := netip.MustParseAddr("100.64.0.0") // first from magic pool name: "v4",
wantTransit := netip.MustParseAddr("169.254.0.0") // first from transit pool dst: netip.MustParseAddr("0.0.0.1"),
wantApp := "a" // the app name related to example.com. wantMagic: netip.MustParseAddr("100.64.0.0"), // first from magic pool
wantDomain := must.Get(dnsname.ToFQDN("example.com.")) wantTransit: netip.MustParseAddr("169.254.0.0"), // first from transit pool
},
if wantDst != addrs.dst { {
t.Errorf("want %v, got %v", wantDst, addrs.dst) name: "v6",
} dst: netip.MustParseAddr("::1"),
if wantMagic != addrs.magic { wantMagic: netip.MustParseAddr("fd7a:115c:a1e0:a99c:100::"), // first from magic pool
t.Errorf("want %v, got %v", wantMagic, addrs.magic) wantTransit: netip.MustParseAddr("fd7a:115c:a1e0:a99c:200::"), // first from transit pool
} },
if wantTransit != addrs.transit { } {
t.Errorf("want %v, got %v", wantTransit, addrs.transit) t.Run(tt.name, func(t *testing.T) {
} addrs, err := c.client.reserveAddresses(domain, tt.dst)
if wantApp != addrs.app { if err != nil {
t.Errorf("want %s, got %s", wantApp, addrs.app) t.Fatal(err)
} }
if wantDomain != addrs.domain { if tt.dst != addrs.dst {
t.Errorf("want %s, got %s", wantDomain, addrs.domain) t.Errorf("want %v, got %v", tt.dst, addrs.dst)
}
if tt.wantMagic != addrs.magic {
t.Errorf("want %v, got %v", tt.wantMagic, addrs.magic)
}
if tt.wantTransit != addrs.transit {
t.Errorf("want %v, got %v", tt.wantTransit, addrs.transit)
}
if app != addrs.app {
t.Errorf("want %s, got %s", app, addrs.app)
}
if domain != addrs.domain {
t.Errorf("want %s, got %s", domain, addrs.domain)
}
})
} }
} }
@ -549,13 +568,20 @@ func makeSelfNode(t *testing.T, attrs []appctype.Conn25Attr, tags []string) tail
}).View() }).View()
} }
func rangeFrom(from, to string) netipx.IPRange { func v4RangeFrom(from, to string) netipx.IPRange {
return netipx.IPRangeFrom( return netipx.IPRangeFrom(
netip.MustParseAddr("100.64.0."+from), netip.MustParseAddr("100.64.0."+from),
netip.MustParseAddr("100.64.0."+to), netip.MustParseAddr("100.64.0."+to),
) )
} }
func v6RangeFrom(from, to string) netipx.IPRange {
return netipx.IPRangeFrom(
netip.MustParseAddr("fd7a:115c:a1e0:a99c:"+from+"::"),
netip.MustParseAddr("fd7a:115c:a1e0:a99c:"+to+"::"),
)
}
func makeDNSResponse(t *testing.T, domain string, addrs []*dnsmessage.AResource) []byte { func makeDNSResponse(t *testing.T, domain string, addrs []*dnsmessage.AResource) []byte {
t.Helper() t.Helper()
name := dnsmessage.MustNewName(domain) name := dnsmessage.MustNewName(domain)
@ -682,13 +708,14 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
for _, tt := range []struct { for _, tt := range []struct {
name string name string
domain string domain string
addrs []*dnsmessage.AResource v4Addrs []*dnsmessage.AResource
v6Addrs []*dnsmessage.AAAAResource
wantByMagicIP map[netip.Addr]addrs wantByMagicIP map[netip.Addr]addrs
}{ }{
{ {
name: "one-ip-matches", name: "one-ip-matches",
domain: "example.com.", domain: "example.com.",
addrs: []*dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}}, v4Addrs: []*dnsmessage.AResource{{A: [4]byte{1, 0, 0, 0}}},
// these are 'expected' because they are the beginning of the provided pools // these are 'expected' because they are the beginning of the provided pools
wantByMagicIP: map[netip.Addr]addrs{ wantByMagicIP: map[netip.Addr]addrs{
netip.MustParseAddr("100.64.0.0"): { netip.MustParseAddr("100.64.0.0"): {
@ -700,10 +727,34 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
}, },
}, },
}, },
{
name: "v6-ip-matches",
domain: "example.com.",
v6Addrs: []*dnsmessage.AAAAResource{
{AAAA: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}},
{AAAA: [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2}},
},
wantByMagicIP: map[netip.Addr]addrs{
netip.MustParseAddr("fd7a:115c:a1e0:a99c::"): {
domain: "example.com.",
dst: netip.MustParseAddr("::1"),
magic: netip.MustParseAddr("fd7a:115c:a1e0:a99c:0::"),
transit: netip.MustParseAddr("fd7a:115c:a1e0:a99c:40::"),
app: "app1",
},
netip.MustParseAddr("fd7a:115c:a1e0:a99c::1"): {
domain: "example.com.",
dst: netip.MustParseAddr("::2"),
magic: netip.MustParseAddr("fd7a:115c:a1e0:a99c:0::1"),
transit: netip.MustParseAddr("fd7a:115c:a1e0:a99c:40::1"),
app: "app1",
},
},
},
{ {
name: "multiple-ip-matches", name: "multiple-ip-matches",
domain: "example.com.", domain: "example.com.",
addrs: []*dnsmessage.AResource{ v4Addrs: []*dnsmessage.AResource{
{A: [4]byte{1, 0, 0, 0}}, {A: [4]byte{1, 0, 0, 0}},
{A: [4]byte{2, 0, 0, 0}}, {A: [4]byte{2, 0, 0, 0}},
}, },
@ -727,20 +778,27 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
{ {
name: "no-domain-match", name: "no-domain-match",
domain: "x.example.com.", domain: "x.example.com.",
addrs: []*dnsmessage.AResource{ v4Addrs: []*dnsmessage.AResource{
{A: [4]byte{1, 0, 0, 0}}, {A: [4]byte{1, 0, 0, 0}},
{A: [4]byte{2, 0, 0, 0}}, {A: [4]byte{2, 0, 0, 0}},
}, },
}, },
} { } {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
dnsResp := makeDNSResponse(t, tt.domain, tt.addrs) var dnsResp []byte
if len(tt.v4Addrs) > 0 {
dnsResp = makeDNSResponse(t, tt.domain, tt.v4Addrs)
} else {
dnsResp = makeV6DNSResponse(t, tt.domain, tt.v6Addrs)
}
sn := makeSelfNode(t, []appctype.Conn25Attr{{ sn := makeSelfNode(t, []appctype.Conn25Attr{{
Name: "app1", Name: "app1",
Connectors: []string{"tag:woo"}, Connectors: []string{"tag:woo"},
Domains: []string{"example.com"}, Domains: []string{"example.com"},
MagicIPPool: []netipx.IPRange{rangeFrom("0", "10"), rangeFrom("20", "30")}, V4MagicIPPool: []netipx.IPRange{v4RangeFrom("0", "10"), v4RangeFrom("20", "30")},
TransitIPPool: []netipx.IPRange{rangeFrom("40", "50")}, V6MagicIPPool: []netipx.IPRange{v6RangeFrom("0", "10"), v6RangeFrom("20", "30")},
V4TransitIPPool: []netipx.IPRange{v4RangeFrom("40", "50")},
V6TransitIPPool: []netipx.IPRange{v6RangeFrom("40", "50")},
}}, []string{}) }}, []string{})
c := newConn25(logger.Discard) c := newConn25(logger.Discard)
c.reconfig(sn) c.reconfig(sn)
@ -754,30 +812,48 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
} }
func TestReserveAddressesDeduplicated(t *testing.T) { func TestReserveAddressesDeduplicated(t *testing.T) {
c := newConn25(logger.Discard) for _, tt := range []struct {
c.client.magicIPPool = newIPPool(mustIPSetFromPrefix("100.64.0.0/24")) name string
c.client.transitIPPool = newIPPool(mustIPSetFromPrefix("169.254.0.0/24")) dst netip.Addr
c.client.config.appNamesByDomain = map[dnsname.FQDN][]string{"example.com.": {"a"}} }{
{
name: "v4",
dst: netip.MustParseAddr("0.0.0.1"),
},
{
name: "v6",
dst: netip.MustParseAddr("::1"),
},
} {
t.Run(tt.name, func(t *testing.T) {
c := newConn25(logger.Discard)
c.client.v4MagicIPPool = newIPPool(mustIPSetFromPrefix("100.64.0.0/24"))
c.client.v6MagicIPPool = newIPPool(mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0100::/80"))
c.client.v4TransitIPPool = newIPPool(mustIPSetFromPrefix("169.254.0.0/24"))
c.client.v6TransitIPPool = newIPPool(mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0200::/80"))
c.client.config.appNamesByDomain = map[dnsname.FQDN][]string{"example.com.": {"a"}}
first, err := c.client.reserveAddresses("example.com.", tt.dst)
if err != nil {
t.Fatal(err)
}
dst := netip.MustParseAddr("0.0.0.1") second, err := c.client.reserveAddresses("example.com.", tt.dst)
first, err := c.client.reserveAddresses("example.com.", dst) if err != nil {
if err != nil { t.Fatal(err)
t.Fatal(err) }
}
second, err := c.client.reserveAddresses("example.com.", dst) if first != second {
if err != nil { t.Errorf("expected same addrs on repeated call, got first=%v second=%v", first, second)
t.Fatal(err) }
} if got := len(c.client.assignments.byMagicIP); got != 1 {
t.Errorf("want 1 entry in byMagicIP, got %d", got)
}
if got := len(c.client.assignments.byDomainDst); got != 1 {
t.Errorf("want 1 entry in byDomainDst, got %d", got)
}
if first != second { })
t.Errorf("expected same addrs on repeated call, got first=%v second=%v", first, second)
}
if got := len(c.client.assignments.byMagicIP); got != 1 {
t.Errorf("want 1 entry in byMagicIP, got %d", got)
}
if got := len(c.client.assignments.byDomainDst); got != 1 {
t.Errorf("want 1 entry in byDomainDst, got %d", got)
} }
} }
@ -961,11 +1037,13 @@ func TestMapDNSResponseRewritesResponses(t *testing.T) {
domainName := configuredDomain + "." domainName := configuredDomain + "."
dnsMessageName := dnsmessage.MustNewName(domainName) dnsMessageName := dnsmessage.MustNewName(domainName)
sn := makeSelfNode(t, []appctype.Conn25Attr{{ sn := makeSelfNode(t, []appctype.Conn25Attr{{
Name: "app1", Name: "app1",
Connectors: []string{"tag:connector"}, Connectors: []string{"tag:connector"},
Domains: []string{configuredDomain}, Domains: []string{configuredDomain},
MagicIPPool: []netipx.IPRange{rangeFrom("0", "10")}, V4MagicIPPool: []netipx.IPRange{v4RangeFrom("0", "10")},
TransitIPPool: []netipx.IPRange{rangeFrom("40", "50")}, V4TransitIPPool: []netipx.IPRange{v4RangeFrom("40", "50")},
V6MagicIPPool: []netipx.IPRange{netipx.IPRangeFrom(netip.MustParseAddr("2606:4700::6812:100"), netip.MustParseAddr("2606:4700::6812:1ff"))},
V6TransitIPPool: []netipx.IPRange{netipx.IPRangeFrom(netip.MustParseAddr("2606:4700::6813:100"), netip.MustParseAddr("2606:4700::6813:1ff"))},
}}, []string{}) }}, []string{})
compareToRecords := func(t *testing.T, resources []dnsmessage.Resource, want []netip.Addr) { compareToRecords := func(t *testing.T, resources []dnsmessage.Resource, want []netip.Addr) {
@ -1060,12 +1138,17 @@ func TestMapDNSResponseRewritesResponses(t *testing.T) {
), ),
}, },
{ {
name: "ipv6-no-answers", name: "ipv6-multiple",
toMap: makeV6DNSResponse(t, domainName, []*dnsmessage.AAAAResource{ toMap: makeV6DNSResponse(t, domainName, []*dnsmessage.AAAAResource{
{AAAA: netip.MustParseAddr("2606:4700::6812:1a78").As16()}, {AAAA: netip.MustParseAddr("2606:4700::6812:1a78").As16()},
{AAAA: netip.MustParseAddr("2606:4700::6812:1b78").As16()}, {AAAA: netip.MustParseAddr("2606:4700::6812:1b78").As16()},
}), }),
assertFx: assertParsesToAnswers(nil), assertFx: assertParsesToAnswers(
[]netip.Addr{
netip.MustParseAddr("2606:4700::6812:100"),
netip.MustParseAddr("2606:4700::6812:101"),
},
),
}, },
{ {
name: "not-our-domain", name: "not-our-domain",
@ -1174,7 +1257,7 @@ func TestMapDNSResponseRewritesResponses(t *testing.T) {
assertFx: assertParsesToAnswers(nil), assertFx: assertParsesToAnswers(nil),
}, },
{ {
name: "answer-type-mismatch", name: "answer-type-mismatch-want-v4",
toMap: makeDNSResponseForSections(t, toMap: makeDNSResponseForSections(t,
[]dnsmessage.Question{ []dnsmessage.Question{
{ {
@ -1205,6 +1288,38 @@ func TestMapDNSResponseRewritesResponses(t *testing.T) {
), ),
assertFx: assertParsesToAnswers([]netip.Addr{netip.MustParseAddr("100.64.0.0")}), assertFx: assertParsesToAnswers([]netip.Addr{netip.MustParseAddr("100.64.0.0")}),
}, },
{
name: "answer-type-mismatch-want-v6",
toMap: makeDNSResponseForSections(t,
[]dnsmessage.Question{
{
Name: dnsMessageName,
Type: dnsmessage.TypeAAAA,
Class: dnsmessage.ClassINET,
},
},
[]dnsmessage.Resource{
{
Header: dnsmessage.ResourceHeader{
Name: dnsMessageName,
Type: dnsmessage.TypeAAAA,
Class: dnsmessage.ClassINET,
},
Body: &dnsmessage.AAAAResource{AAAA: netip.MustParseAddr("1.2.3.4").As16()},
},
{
Header: dnsmessage.ResourceHeader{
Name: dnsMessageName,
Type: dnsmessage.TypeA,
Class: dnsmessage.ClassINET,
},
Body: &dnsmessage.AResource{A: netip.MustParseAddr("5.6.7.8").As4()},
},
},
nil,
),
assertFx: assertParsesToAnswers([]netip.Addr{netip.MustParseAddr("2606:4700::6812:100")}),
},
} { } {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
c := newConn25(logger.Discard) c := newConn25(logger.Discard)
@ -1458,12 +1573,22 @@ func TestTransitIPConnMapping(t *testing.T) {
func TestClientTransitIPForMagicIP(t *testing.T) { func TestClientTransitIPForMagicIP(t *testing.T) {
sn := makeSelfNode(t, []appctype.Conn25Attr{{ sn := makeSelfNode(t, []appctype.Conn25Attr{{
MagicIPPool: []netipx.IPRange{rangeFrom("0", "10")}, // 100.64.0.0 - 100.64.0.10 V4MagicIPPool: []netipx.IPRange{v4RangeFrom("0", "10")}, // 100.64.0.0 - 100.64.0.10
V6MagicIPPool: []netipx.IPRange{v6RangeFrom("0", "10")},
}}, []string{}) }}, []string{})
mappedMip := netip.MustParseAddr("100.64.0.0") mappedMip := netip.MustParseAddr("100.64.0.0")
mappedTip := netip.MustParseAddr("169.0.0.0") mappedTip := netip.MustParseAddr("169.0.0.0")
unmappedMip := netip.MustParseAddr("100.64.0.1") unmappedMip := netip.MustParseAddr("100.64.0.1")
nonMip := netip.MustParseAddr("100.64.0.11") nonMip := netip.MustParseAddr("100.64.0.11")
dst := netip.MustParseAddr("0.0.0.1")
v6MappedMip := netip.MustParseAddr("fd7a:115c:a1e0:a99c:0::")
v6MappedTip := netip.MustParseAddr("fd7a:115c:a1e0:a99c:100::")
v6UnmappedMip := netip.MustParseAddr("fd7a:115c:a1e0:a99c:1::")
v6NonMip := netip.MustParseAddr("fd7a:115c:a1e0:a99c:11::")
v6Dst := netip.MustParseAddr("::1")
for _, tt := range []struct { for _, tt := range []struct {
name string name string
mip netip.Addr mip netip.Addr
@ -1488,16 +1613,44 @@ func TestClientTransitIPForMagicIP(t *testing.T) {
wantTip: mappedTip, wantTip: mappedTip,
wantErr: nil, wantErr: nil,
}, },
{
name: "v6-not-magic",
mip: v6NonMip,
wantTip: netip.Addr{},
wantErr: nil,
},
{
name: "v6-unmapped-magic-ip",
mip: v6UnmappedMip,
wantTip: netip.Addr{},
wantErr: ErrUnmappedMagicIP,
},
{
name: "v6-mapped-magic-ip",
mip: v6MappedMip,
wantTip: v6MappedTip,
wantErr: nil,
},
} { } {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
c := newConn25(t.Logf) c := newConn25(t.Logf)
if err := c.reconfig(sn); err != nil { if err := c.reconfig(sn); err != nil {
t.Fatal(err) t.Fatal(err)
} }
c.client.assignments.insert(addrs{ if err := c.client.assignments.insert(addrs{
magic: mappedMip, magic: mappedMip,
transit: mappedTip, transit: mappedTip,
}) dst: dst,
}); err != nil {
t.Fatal(err)
}
if err := c.client.assignments.insert(addrs{
magic: v6MappedMip,
transit: v6MappedTip,
dst: v6Dst,
}); err != nil {
t.Fatal(err)
}
tip, err := c.client.transitIPForMagicIP(tt.mip) tip, err := c.client.transitIPForMagicIP(tt.mip)
if tip != tt.wantTip { if tip != tt.wantTip {
t.Fatalf("checking transit ip: want %v, got %v", tt.wantTip, tip) t.Fatalf("checking transit ip: want %v, got %v", tt.wantTip, tip)
@ -1511,7 +1664,7 @@ func TestClientTransitIPForMagicIP(t *testing.T) {
func TestConnectorRealIPForTransitIPConnection(t *testing.T) { func TestConnectorRealIPForTransitIPConnection(t *testing.T) {
sn := makeSelfNode(t, []appctype.Conn25Attr{{ sn := makeSelfNode(t, []appctype.Conn25Attr{{
TransitIPPool: []netipx.IPRange{rangeFrom("40", "50")}, // 100.64.0.40 - 100.64.0.50 V4TransitIPPool: []netipx.IPRange{v4RangeFrom("40", "50")}, // 100.64.0.40 - 100.64.0.50
}}, []string{}) }}, []string{})
mappedSrc := netip.MustParseAddr("100.0.0.1") mappedSrc := netip.MustParseAddr("100.0.0.1")
unmappedSrc := netip.MustParseAddr("100.0.0.2") unmappedSrc := netip.MustParseAddr("100.0.0.2")
@ -1650,3 +1803,53 @@ func TestConnectorPacketFilterAllow(t *testing.T) {
t.Fatal("unknownTip: should not have been allowed") t.Fatal("unknownTip: should not have been allowed")
} }
} }
func TestGetMagicRange(t *testing.T) {
sn := makeSelfNode(t, []appctype.Conn25Attr{{
Name: "app1",
Connectors: []string{"tag:woo"},
Domains: []string{"example.com"},
V4MagicIPPool: []netipx.IPRange{netipx.IPRangeFrom(netip.MustParseAddr("0.0.0.1"), netip.MustParseAddr("0.0.0.3"))},
V6MagicIPPool: []netipx.IPRange{netipx.IPRangeFrom(netip.MustParseAddr("::1"), netip.MustParseAddr("::3"))},
}}, []string{})
c := newConn25(t.Logf)
if err := c.reconfig(sn); err != nil {
t.Fatal(err)
}
ext := &extension{
conn25: c,
}
mRange := ext.getMagicRange()
somePrefixCovers := func(a netip.Addr) bool {
for _, r := range mRange.All() {
if r.Contains(a) {
return true
}
}
return false
}
ins := []string{
"0.0.0.1",
"0.0.0.2",
"0.0.0.3",
"::1",
"::2",
"::3",
}
outs := []string{
"0.0.0.0",
"0.0.0.4",
"::",
"::4",
}
for _, s := range ins {
if !somePrefixCovers(netip.MustParseAddr(s)) {
t.Fatalf("expected addr to be covered but was not: %s", s)
}
}
for _, s := range outs {
if somePrefixCovers(netip.MustParseAddr(s)) {
t.Fatalf("expected addr to NOT be covered but WAS: %s", s)
}
}
}

@ -104,7 +104,9 @@ type Conn25Attr struct {
// Connectors enumerates the app connectors which service these domains. // Connectors enumerates the app connectors which service these domains.
// These can either be "*" to match any advertising connector, or a // These can either be "*" to match any advertising connector, or a
// tag of the form tag:<tag-name>. // tag of the form tag:<tag-name>.
Connectors []string `json:"connectors,omitempty"` Connectors []string `json:"connectors,omitempty"`
MagicIPPool []netipx.IPRange `json:"magicIPPool,omitempty"` V4MagicIPPool []netipx.IPRange `json:"v4MagicIPPool,omitempty"`
TransitIPPool []netipx.IPRange `json:"transitIPPool,omitempty"` V4TransitIPPool []netipx.IPRange `json:"v4TransitIPPool,omitempty"`
V6MagicIPPool []netipx.IPRange `json:"v6MagicIPPool,omitempty"`
V6TransitIPPool []netipx.IPRange `json:"v6TransitIPPool,omitempty"`
} }

Loading…
Cancel
Save