From 43fbc0d58833719a7994cec2cb3b0174ca126724 Mon Sep 17 00:00:00 2001 From: Fran Bull Date: Tue, 26 Mar 2024 13:59:00 -0700 Subject: [PATCH] wip --- appc/appconnector.go | 5 ++++ appc/appctest/appctest.go | 10 ++++++++ appc/routeinfo/routeinfo.go | 25 ++++++++++++++++++++ ipn/ipnlocal/local.go | 44 ++++++++++++++++++++++++++++++++++ ipn/ipnlocal/local_test.go | 47 +++++++++++++++++++++++++++++++++++++ 5 files changed, 131 insertions(+) create mode 100644 appc/routeinfo/routeinfo.go diff --git a/appc/appconnector.go b/appc/appconnector.go index 8c3e2c160..b96800f53 100644 --- a/appc/appconnector.go +++ b/appc/appconnector.go @@ -18,6 +18,7 @@ import ( xmaps "golang.org/x/exp/maps" "golang.org/x/net/dns/dnsmessage" + "tailscale.com/appc/routeinfo" "tailscale.com/types/logger" "tailscale.com/types/views" "tailscale.com/util/dnsname" @@ -34,6 +35,10 @@ type RouteAdvertiser interface { // UnadvertiseRoute removes any matching route advertisements. UnadvertiseRoute(...netip.Prefix) error + + // Store/ReadRouteInfo persists and retreives RouteInfo to stable storage + StoreRouteInfo(*routeinfo.RouteInfo) error + ReadRouteInfo() (*routeinfo.RouteInfo, error) } // AppConnector is an implementation of an AppConnector that performs diff --git a/appc/appctest/appctest.go b/appc/appctest/appctest.go index d62c0e233..aaf2e4595 100644 --- a/appc/appctest/appctest.go +++ b/appc/appctest/appctest.go @@ -6,6 +6,8 @@ package appctest import ( "net/netip" "slices" + + "tailscale.com/appc/routeinfo" ) // RouteCollector is a test helper that collects the list of routes advertised @@ -32,6 +34,14 @@ func (rc *RouteCollector) UnadvertiseRoute(toRemove ...netip.Prefix) error { return nil } +func (rc *RouteCollector) StoreRouteInfo(ri *routeinfo.RouteInfo) error { + return nil +} + +func (rc *RouteCollector) ReadRouteInfo() (*routeinfo.RouteInfo, error) { + return nil, nil +} + // RemovedRoutes returns the list of routes that were removed. func (rc *RouteCollector) RemovedRoutes() []netip.Prefix { return rc.removedRoutes diff --git a/appc/routeinfo/routeinfo.go b/appc/routeinfo/routeinfo.go new file mode 100644 index 000000000..8858ca771 --- /dev/null +++ b/appc/routeinfo/routeinfo.go @@ -0,0 +1,25 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package routeinfo + +import ( + "net/netip" + "time" +) + +type RouteInfo struct { + // routes set with --advertise-routes + Local []netip.Prefix + // routes from the 'routes' section of an app connector acl + Control []netip.Prefix + // routes discovered by observing dns lookups for configured domains + Discovered map[string]*DatedRoutes +} + +type DatedRoutes struct { + // routes discovered for a domain, and when they were last seen in a dns query + Routes map[netip.Prefix]time.Time + // the time at which we last expired old routes + LastCleanup time.Time +} diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index a8faac51c..9cde9acab 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -35,6 +35,7 @@ import ( xmaps "golang.org/x/exp/maps" "gvisor.dev/gvisor/pkg/tcpip" "tailscale.com/appc" + "tailscale.com/appc/routeinfo" "tailscale.com/client/tailscale/apitype" "tailscale.com/clientupdate" "tailscale.com/control/controlclient" @@ -6250,6 +6251,49 @@ func (b *LocalBackend) UnadvertiseRoute(toRemove ...netip.Prefix) error { return err } +// namespace a key with the profile manager's current profile key, if any +func namespaceKeyForCurrentProfile(pm *profileManager, key ipn.StateKey) ipn.StateKey { + return pm.CurrentProfile().Key + "||" + key +} + +const routeInfoStateStoreKey ipn.StateKey = "_routeInfo" + +// StoreRouteInfo implements the appc.RouteAdvertiser interface. It stores +// RouteInfo to StateStore per profile. +func (b *LocalBackend) StoreRouteInfo(ri *routeinfo.RouteInfo) error { + b.mu.Lock() + defer b.mu.Unlock() + if b.pm.CurrentProfile().ID == "" { + return nil + } + key := namespaceKeyForCurrentProfile(b.pm, routeInfoStateStoreKey) + bs, err := json.Marshal(ri) + if err != nil { + return err + } + return b.pm.WriteState(key, bs) +} + +// ReadRouteInfo implements the appc.RouteAdvertiser interface. It reads +// RouteInfo from StateStore per profile. +func (b *LocalBackend) ReadRouteInfo() (*routeinfo.RouteInfo, error) { + b.mu.Lock() + defer b.mu.Unlock() + if b.pm.CurrentProfile().ID == "" { + return &routeinfo.RouteInfo{}, nil + } + key := namespaceKeyForCurrentProfile(b.pm, routeInfoStateStoreKey) + bs, err := b.pm.Store().ReadState(key) + if err != nil { + return nil, err + } + ri := &routeinfo.RouteInfo{} + if err := json.Unmarshal(bs, ri); err != nil { + return nil, err + } + return ri, nil +} + // seamlessRenewalEnabled reports whether seamless key renewals are enabled // (i.e. we saw our self node with the SeamlessKeyRenewal attr in a netmap). // This enables beta functionality of renewing node keys without breaking diff --git a/ipn/ipnlocal/local_test.go b/ipn/ipnlocal/local_test.go index a595cb771..b27267c62 100644 --- a/ipn/ipnlocal/local_test.go +++ b/ipn/ipnlocal/local_test.go @@ -23,6 +23,7 @@ import ( "golang.org/x/net/dns/dnsmessage" "tailscale.com/appc" "tailscale.com/appc/appctest" + "tailscale.com/appc/routeinfo" "tailscale.com/control/controlclient" "tailscale.com/drive" "tailscale.com/drive/driveimpl" @@ -2634,3 +2635,49 @@ func (b *LocalBackend) SetPrefsForTest(newp *ipn.Prefs) { defer unlock() b.setPrefsLockedOnEntry(newp, unlock) } +func TestReadWriteRouteInfo(t *testing.T) { + // test can read what's written + prefix1 := netip.MustParsePrefix("1.2.3.4/32") + prefix2 := netip.MustParsePrefix("1.2.3.5/32") + prefix3 := netip.MustParsePrefix("1.2.3.6/32") + now := time.Now() + discovered := make(map[string]*routeinfo.DatedRoutes) + routes := make(map[netip.Prefix]time.Time) + routes[prefix3] = now + discovered["example.com"] = &routeinfo.DatedRoutes{ + LastCleanup: now, + Routes: routes, + } + b := newTestBackend(t) + ri := routeinfo.RouteInfo{ + Local: []netip.Prefix{prefix1}, + Control: []netip.Prefix{prefix2}, + Discovered: discovered, + } + if err := b.StoreRouteInfo(&ri); err != nil { + t.Fatal(err) + } + readRi, err := b.ReadRouteInfo() + if err != nil { + t.Fatal(err) + } + if len(readRi.Local) != 1 || len(readRi.Control) != 1 || len(readRi.Discovered) != 1 { + t.Fatal("read Ri expected to be same shape as ri") + } + if readRi.Local[0] != ri.Local[0] { + t.Fatalf("wanted %v, got %v", ri.Local[0], readRi.Local[0]) + } + if readRi.Control[0] != ri.Control[0] { + t.Fatalf("wanted %v, got %v", ri.Control[0], readRi.Control[0]) + } + dr := readRi.Discovered["example.com"] + if dr.LastCleanup.Compare(now) != 0 { + t.Fatalf("wanted %v, got %v", now, dr.LastCleanup) + } + if len(dr.Routes) != 1 { + t.Fatalf("read Ri expected to be same shape as ri") + } + if dr.Routes[prefix3].Compare(routes[prefix3]) != 0 { + t.Fatalf("wanted %v, got %v", routes[prefix3], dr.Routes[prefix3]) + } +}