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>
This commit is contained in:
+81
-22
@@ -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
|
||||||
}
|
}
|
||||||
@@ -554,8 +566,10 @@ type client struct {
|
|||||||
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
|
||||||
|
v6MagicIPPool *ippool
|
||||||
|
v6TransitIPPool *ippool
|
||||||
assignments addrAssignments
|
assignments addrAssignments
|
||||||
config config
|
config 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,14 +660,30 @@ 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()
|
|
||||||
|
var mip, tip netip.Addr
|
||||||
|
var err error
|
||||||
|
if dst.Is4() {
|
||||||
|
mip, err = c.v4MagicIPPool.next()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return addrs{}, err
|
return addrs{}, err
|
||||||
}
|
}
|
||||||
tip, err := c.transitIPPool.next()
|
tip, err = c.v4TransitIPPool.next()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return addrs{}, err
|
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,
|
||||||
magic: mip,
|
magic: mip,
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
var dstAddr netip.Addr
|
||||||
|
if h.Type == dnsmessage.TypeA {
|
||||||
r, err := p.AResource()
|
r, err := p.AResource()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
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)
|
||||||
}
|
}
|
||||||
answers = append(answers, dnsResponseRewrite{domain: domain, dst: netip.AddrFrom4(r.A)})
|
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: 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,11 +1014,19 @@ 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
|
||||||
// eg DNSSEC info, or other extra info like related records).
|
// eg DNSSEC info, or other extra info like related records).
|
||||||
@@ -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
|
||||||
|
|||||||
+247
-44
@@ -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
|
||||||
|
dst netip.Addr
|
||||||
|
wantMagic netip.Addr
|
||||||
|
wantTransit netip.Addr
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "v4",
|
||||||
|
dst: netip.MustParseAddr("0.0.0.1"),
|
||||||
|
wantMagic: netip.MustParseAddr("100.64.0.0"), // first from magic pool
|
||||||
|
wantTransit: netip.MustParseAddr("169.254.0.0"), // first from transit pool
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "v6",
|
||||||
|
dst: netip.MustParseAddr("::1"),
|
||||||
|
wantMagic: netip.MustParseAddr("fd7a:115c:a1e0:a99c:100::"), // first from magic pool
|
||||||
|
wantTransit: netip.MustParseAddr("fd7a:115c:a1e0:a99c:200::"), // first from transit pool
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
addrs, err := c.client.reserveAddresses(domain, tt.dst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
if tt.dst != addrs.dst {
|
||||||
wantDst := netip.MustParseAddr("0.0.0.1") // same as dst we pass in
|
t.Errorf("want %v, got %v", tt.dst, addrs.dst)
|
||||||
wantMagic := netip.MustParseAddr("100.64.0.0") // first from magic pool
|
|
||||||
wantTransit := netip.MustParseAddr("169.254.0.0") // first from transit pool
|
|
||||||
wantApp := "a" // the app name related to example.com.
|
|
||||||
wantDomain := must.Get(dnsname.ToFQDN("example.com."))
|
|
||||||
|
|
||||||
if wantDst != addrs.dst {
|
|
||||||
t.Errorf("want %v, got %v", wantDst, addrs.dst)
|
|
||||||
}
|
}
|
||||||
if wantMagic != addrs.magic {
|
if tt.wantMagic != addrs.magic {
|
||||||
t.Errorf("want %v, got %v", wantMagic, addrs.magic)
|
t.Errorf("want %v, got %v", tt.wantMagic, addrs.magic)
|
||||||
}
|
}
|
||||||
if wantTransit != addrs.transit {
|
if tt.wantTransit != addrs.transit {
|
||||||
t.Errorf("want %v, got %v", wantTransit, addrs.transit)
|
t.Errorf("want %v, got %v", tt.wantTransit, addrs.transit)
|
||||||
}
|
}
|
||||||
if wantApp != addrs.app {
|
if app != addrs.app {
|
||||||
t.Errorf("want %s, got %s", wantApp, addrs.app)
|
t.Errorf("want %s, got %s", app, addrs.app)
|
||||||
}
|
}
|
||||||
if wantDomain != addrs.domain {
|
if domain != addrs.domain {
|
||||||
t.Errorf("want %s, got %s", wantDomain, 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,18 +812,33 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestReserveAddressesDeduplicated(t *testing.T) {
|
func TestReserveAddressesDeduplicated(t *testing.T) {
|
||||||
|
for _, tt := range []struct {
|
||||||
|
name string
|
||||||
|
dst netip.Addr
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
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 := 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"))
|
||||||
c.client.config.appNamesByDomain = map[dnsname.FQDN][]string{"example.com.": {"a"}}
|
c.client.config.appNamesByDomain = map[dnsname.FQDN][]string{"example.com.": {"a"}}
|
||||||
|
|
||||||
dst := netip.MustParseAddr("0.0.0.1")
|
first, 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)
|
second, err := c.client.reserveAddresses("example.com.", tt.dst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -779,6 +852,9 @@ func TestReserveAddressesDeduplicated(t *testing.T) {
|
|||||||
if got := len(c.client.assignments.byDomainDst); got != 1 {
|
if got := len(c.client.assignments.byDomainDst); got != 1 {
|
||||||
t.Errorf("want 1 entry in byDomainDst, got %d", got)
|
t.Errorf("want 1 entry in byDomainDst, got %d", got)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type testNodeBackend struct {
|
type testNodeBackend struct {
|
||||||
@@ -964,8 +1040,10 @@ func TestMapDNSResponseRewritesResponses(t *testing.T) {
|
|||||||
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -105,6 +105,8 @@ type Conn25Attr struct {
|
|||||||
// 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"`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user