feature/conn25: set assignment expiry based on dns response TTL
Updates tailscale/corp#39975 Signed-off-by: Fran Bull <fran@tailscale.com>
This commit is contained in:
@@ -38,6 +38,14 @@ func (a *addrAssignments) insert(as *addrs) error {
|
|||||||
return a.insertWithExpiry(as, defaultExpiry)
|
return a.insertWithExpiry(as, defaultExpiry)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *addrAssignments) insertFromTTL(as *addrs, ttl time.Duration) error {
|
||||||
|
const minTTL = time.Minute * 1
|
||||||
|
const maxTTL = time.Hour * 72
|
||||||
|
expiry := max(minTTL, ttl)
|
||||||
|
expiry = min(maxTTL, expiry)
|
||||||
|
return a.insertWithExpiry(as, expiry)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *addrAssignments) insertWithExpiry(as *addrs, d time.Duration) error {
|
func (a *addrAssignments) insertWithExpiry(as *addrs, d time.Duration) error {
|
||||||
now := a.clock.Now()
|
now := a.clock.Now()
|
||||||
if !as.expiresAt.IsZero() && !as.expiresAt.Before(now) {
|
if !as.expiresAt.IsZero() && !as.expiresAt.Before(now) {
|
||||||
|
|||||||
@@ -748,7 +748,7 @@ func (cfg *config) getAppsForConnectorDomain(domain dnsname.FQDN, prefsAdvertise
|
|||||||
// the app name refers to a configured app.
|
// the app name refers to a configured app.
|
||||||
// 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(appName string, domain dnsname.FQDN, dst netip.Addr) (*addrs, error) {
|
func (c *client) reserveAddresses(appName string, domain dnsname.FQDN, dst netip.Addr, ttl time.Duration) (*addrs, error) {
|
||||||
if !dst.IsValid() {
|
if !dst.IsValid() {
|
||||||
return nil, errors.New("dst is not valid")
|
return nil, errors.New("dst is not valid")
|
||||||
}
|
}
|
||||||
@@ -810,7 +810,7 @@ func (c *client) reserveAddresses(appName string, domain dnsname.FQDN, dst netip
|
|||||||
app: appName,
|
app: appName,
|
||||||
domain: domain,
|
domain: domain,
|
||||||
}
|
}
|
||||||
if err := c.assignments.insert(as); err != nil {
|
if err := c.assignments.insertFromTTL(as, ttl); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
err = c.enqueueAddressAssignment(as)
|
err = c.enqueueAddressAssignment(as)
|
||||||
@@ -952,6 +952,7 @@ func (e *extension) sendAddressAssignment(ctx context.Context, as addrs) (tailcf
|
|||||||
type dnsResponseRewrite struct {
|
type dnsResponseRewrite struct {
|
||||||
domain dnsname.FQDN
|
domain dnsname.FQDN
|
||||||
dst netip.Addr
|
dst netip.Addr
|
||||||
|
ttl time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeServFail(logf logger.Logf, h dnsmessage.Header, q dnsmessage.Question) []byte {
|
func makeServFail(logf logger.Logf, h dnsmessage.Header, q dnsmessage.Question) []byte {
|
||||||
@@ -1143,7 +1144,7 @@ func (c *Conn25) mapDNSResponse(buf []byte) []byte {
|
|||||||
}
|
}
|
||||||
dstAddr = netip.AddrFrom16(r.AAAA)
|
dstAddr = netip.AddrFrom16(r.AAAA)
|
||||||
}
|
}
|
||||||
answers = append(answers, dnsResponseRewrite{domain: queriedDomain, dst: dstAddr})
|
answers = append(answers, dnsResponseRewrite{domain: queriedDomain, dst: dstAddr, ttl: time.Second * time.Duration(h.TTL)})
|
||||||
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)
|
||||||
@@ -1178,7 +1179,7 @@ func (c *client) rewriteDNSResponse(appName string, hdr dnsmessage.Header, quest
|
|||||||
|
|
||||||
// make an answer for each rewrite
|
// make an answer for each rewrite
|
||||||
for _, rw := range answers {
|
for _, rw := range answers {
|
||||||
as, err := c.reserveAddresses(appName, rw.domain, rw.dst)
|
as, err := c.reserveAddresses(appName, rw.domain, rw.dst, rw.ttl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import (
|
|||||||
"tailscale.com/net/tstun"
|
"tailscale.com/net/tstun"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/tsd"
|
"tailscale.com/tsd"
|
||||||
|
"tailscale.com/tstest"
|
||||||
"tailscale.com/types/appctype"
|
"tailscale.com/types/appctype"
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
@@ -428,7 +429,7 @@ func TestReserveIPs(t *testing.T) {
|
|||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
addrs, err := c.client.reserveAddresses(appName, domain, tt.dst)
|
addrs, err := c.client.reserveAddresses(appName, domain, tt.dst, 10)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -1122,6 +1123,93 @@ func TestMapDNSResponseAssignsAddrs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMapDNSResponseSetsExpiryBasedOnTTL(t *testing.T) {
|
||||||
|
configuredDomain := "example.com"
|
||||||
|
domainName := configuredDomain + "."
|
||||||
|
dnsMessageName := dnsmessage.MustNewName(domainName)
|
||||||
|
sn := makeSelfNode(t, []appctype.Conn25Attr{{
|
||||||
|
Name: "app1",
|
||||||
|
Connectors: []string{"tag:woo"},
|
||||||
|
Domains: []string{configuredDomain},
|
||||||
|
V4MagicIPPool: []netipx.IPRange{v4RangeFrom("0", "10")},
|
||||||
|
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"))},
|
||||||
|
}}, nil)
|
||||||
|
|
||||||
|
c := newConn25(logger.Discard)
|
||||||
|
clock := tstest.NewClock(tstest.ClockOpts{Start: time.Now()})
|
||||||
|
c.client.assignments.clock = clock
|
||||||
|
cfg := mustConfig(t, sn)
|
||||||
|
c.reconfig(cfg)
|
||||||
|
|
||||||
|
ipOne := netip.MustParseAddr("1.0.0.1")
|
||||||
|
ipTwo := netip.MustParseAddr("1.0.0.2")
|
||||||
|
ipTooBig := netip.MustParseAddr("1.0.0.3")
|
||||||
|
ipTooSmall := netip.MustParseAddr("1.0.0.4")
|
||||||
|
dnsResp := makeDNSResponseForSections(t,
|
||||||
|
[]dnsmessage.Question{{Name: dnsMessageName, Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET}},
|
||||||
|
[]dnsmessage.Resource{
|
||||||
|
{
|
||||||
|
Header: dnsmessage.ResourceHeader{Name: dnsMessageName, Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET, TTL: 300},
|
||||||
|
Body: &dnsmessage.AResource{A: ipOne.As4()},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: dnsmessage.ResourceHeader{Name: dnsMessageName, Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET, TTL: 60},
|
||||||
|
Body: &dnsmessage.AResource{A: ipTwo.As4()},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: dnsmessage.ResourceHeader{Name: dnsMessageName, Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET, TTL: 1000000},
|
||||||
|
Body: &dnsmessage.AResource{A: ipTooBig.As4()},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: dnsmessage.ResourceHeader{Name: dnsMessageName, Type: dnsmessage.TypeA, Class: dnsmessage.ClassINET, TTL: 1},
|
||||||
|
Body: &dnsmessage.AResource{A: ipTooSmall.As4()},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
c.mapDNSResponse(dnsResp)
|
||||||
|
|
||||||
|
assertExpiresAt := func(addr netip.Addr, want time.Time) {
|
||||||
|
t.Helper()
|
||||||
|
as, ok := c.client.assignments.lookupByDomainDst("example.com.", addr)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("setup wrong expected to find addrs")
|
||||||
|
}
|
||||||
|
got := as.expiresAt
|
||||||
|
if !got.Equal(want) {
|
||||||
|
t.Fatalf("want %v; got %v", want, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assertExpiresAt(ipOne, clock.Now().Add(300*time.Second))
|
||||||
|
assertExpiresAt(ipTwo, clock.Now().Add(60*time.Second))
|
||||||
|
assertExpiresAt(ipTooBig, clock.Now().Add(72*time.Hour)) // max we will do is 72 hours
|
||||||
|
assertExpiresAt(ipTooSmall, clock.Now().Add(60*time.Second)) // min we will do is 1 min
|
||||||
|
|
||||||
|
ipThree := netip.MustParseAddr("::1")
|
||||||
|
ipFour := netip.MustParseAddr("::2")
|
||||||
|
dnsRespV6 := 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, TTL: 301},
|
||||||
|
Body: &dnsmessage.AAAAResource{AAAA: ipThree.As16()},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Header: dnsmessage.ResourceHeader{Name: dnsMessageName, Type: dnsmessage.TypeAAAA, Class: dnsmessage.ClassINET, TTL: 61},
|
||||||
|
Body: &dnsmessage.AAAAResource{AAAA: ipFour.As16()},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
c.mapDNSResponse(dnsRespV6)
|
||||||
|
|
||||||
|
assertExpiresAt(ipThree, clock.Now().Add(301*time.Second))
|
||||||
|
assertExpiresAt(ipFour, clock.Now().Add(61*time.Second))
|
||||||
|
}
|
||||||
|
|
||||||
func TestNormalizedDNSNames(t *testing.T) {
|
func TestNormalizedDNSNames(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -1168,12 +1256,12 @@ func TestReserveAddressesDeduplicated(t *testing.T) {
|
|||||||
c.v4TransitIPPool = newIPPool(mustIPSetFromPrefix("169.254.0.0/24"))
|
c.v4TransitIPPool = newIPPool(mustIPSetFromPrefix("169.254.0.0/24"))
|
||||||
c.v6TransitIPPool = newIPPool(mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0200::/80"))
|
c.v6TransitIPPool = newIPPool(mustIPSetFromPrefix("fd7a:115c:a1e0:a99c:0200::/80"))
|
||||||
|
|
||||||
first, err := c.reserveAddresses(appName, "example.com.", tt.dst)
|
first, err := c.reserveAddresses(appName, "example.com.", tt.dst, 10)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
second, err := c.reserveAddresses(appName, "example.com.", tt.dst)
|
second, err := c.reserveAddresses(appName, "example.com.", tt.dst, 10)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -2414,18 +2502,18 @@ func TestReconfigDoesNotReissueInUseAddresses(t *testing.T) {
|
|||||||
conn25: c,
|
conn25: c,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := c.client.reserveAddresses(appName, domain, tt.dstOne)
|
_, err := c.client.reserveAddresses(appName, domain, tt.dstOne, 10)
|
||||||
if !errors.Is(err, errUninitializedIPPool) {
|
if !errors.Is(err, errUninitializedIPPool) {
|
||||||
t.Fatalf("want %v, got %v", errUninitializedIPPool, err)
|
t.Fatalf("want %v, got %v", errUninitializedIPPool, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.onSelfChange(makeNodeFromMagicRange(beforeRangeV4, beforeRangeV6))
|
ext.onSelfChange(makeNodeFromMagicRange(beforeRangeV4, beforeRangeV6))
|
||||||
beforeAddrs, err := c.client.reserveAddresses(appName, domain, tt.dstOne)
|
beforeAddrs, err := c.client.reserveAddresses(appName, domain, tt.dstOne, 10)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
ext.onSelfChange(makeNodeFromMagicRange(afterRangeV4, afterRangeV6))
|
ext.onSelfChange(makeNodeFromMagicRange(afterRangeV4, afterRangeV6))
|
||||||
afterAddrs, err := c.client.reserveAddresses(appName, domain, tt.dstTwo)
|
afterAddrs, err := c.client.reserveAddresses(appName, domain, tt.dstTwo, 10)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user