From 57856fc0d5cbffdb81d28b0dd94e5ab2110fd58c Mon Sep 17 00:00:00 2001 From: Irbe Krumina Date: Tue, 23 Jul 2024 18:50:55 +0300 Subject: [PATCH] ipn,wgengine/magicsock: allow setting static node endpoints via tailscaled configfile (#12882) wgengine/magicsock,ipn: allow setting static node endpoints via tailscaled config file. Adds a new StaticEndpoints field to tailscaled config that can be used to statically configure the endpoints that the node advertizes. This field will replace TS_DEBUG_PRETENDPOINTS env var that can be used to achieve the same. Additionally adds some functionality that ensures that endpoints are updated when configfile is reloaded. Also, refactor configuring/reconfiguring components to use the same functionality when configfile is parsed the first time or subsequent times (after reload). Previously a configfile reload did not result in resetting of prefs. Now it does- but does not yet tell the relevant components to consume the new prefs. This is to be done in a follow-up. Updates tailscale/tailscale#12578 Signed-off-by: Irbe Krumina --- ipn/conf.go | 4 +++ ipn/ipnlocal/local.go | 63 +++++++++++++++++++++++++-------- wgengine/magicsock/magicsock.go | 35 ++++++++++++++++-- 3 files changed, 85 insertions(+), 17 deletions(-) diff --git a/ipn/conf.go b/ipn/conf.go index ed6e3c871..6a67f4004 100644 --- a/ipn/conf.go +++ b/ipn/conf.go @@ -42,6 +42,10 @@ type ConfigVAlpha struct { AutoUpdate *AutoUpdatePrefs `json:",omitempty"` ServeConfigTemp *ServeConfig `json:",omitempty"` // TODO(bradfitz,maisem): make separate stable type for this + // StaticEndpoints are additional, user-defined endpoints that this node + // should advertise amongst its wireguard endpoints. + StaticEndpoints []netip.AddrPort `json:",omitempty"` + // TODO(bradfitz,maisem): future something like: // Profile map[string]*Config // keyed by alice@gmail.com, corp.com (TailnetSID) } diff --git a/ipn/ipnlocal/local.go b/ipn/ipnlocal/local.go index 4fb74b75c..ede29156b 100644 --- a/ipn/ipnlocal/local.go +++ b/ipn/ipnlocal/local.go @@ -25,6 +25,7 @@ import ( "os" "os/exec" "path/filepath" + "reflect" "runtime" "slices" "sort" @@ -391,18 +392,6 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo sds.SetDialer(dialer.SystemDial) } - if sys.InitialConfig != nil { - p := pm.CurrentPrefs().AsStruct() - mp, err := sys.InitialConfig.Parsed.ToPrefs() - if err != nil { - return nil, err - } - p.ApplyEdits(&mp) - if err := pm.SetPrefs(p.View(), ipn.NetworkProfile{}); err != nil { - return nil, err - } - } - envknob.LogCurrent(logf) osshare.SetFileSharingEnabled(false, logf) @@ -417,7 +406,6 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo statsLogf: logger.LogOnChange(logf, 5*time.Minute, clock.Now), sys: sys, health: sys.HealthTracker(), - conf: sys.InitialConfig, e: e, dialer: dialer, store: store, @@ -434,6 +422,12 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, sys *tsd.System, lo } mConn.SetNetInfoCallback(b.setNetInfo) + if sys.InitialConfig != nil { + if err := b.setConfigLocked(sys.InitialConfig); err != nil { + return nil, err + } + } + netMon := sys.NetMon.Get() b.sockstatLogger, err = sockstatlog.NewLogger(logpolicy.LogsDir(logf), logf, logID, netMon, sys.HealthTracker()) if err != nil { @@ -616,11 +610,50 @@ func (b *LocalBackend) ReloadConfig() (ok bool, err error) { if err != nil { return false, err } - b.conf = conf - // TODO(bradfitz): apply things + if err := b.setConfigLocked(conf); err != nil { + return false, fmt.Errorf("error setting config: %w", err) + } + return true, nil } +func (b *LocalBackend) setConfigLocked(conf *conffile.Config) error { + + // TODO(irbekrm): notify the relevant components to consume any prefs + // updates. Currently only initial configfile settings are applied + // immediately. + p := b.pm.CurrentPrefs().AsStruct() + mp, err := conf.Parsed.ToPrefs() + if err != nil { + return fmt.Errorf("error parsing config to prefs: %w", err) + } + p.ApplyEdits(&mp) + if err := b.pm.SetPrefs(p.View(), ipn.NetworkProfile{}); err != nil { + return err + } + + defer func() { + b.conf = conf + }() + + if conf.Parsed.StaticEndpoints == nil && (b.conf == nil || b.conf.Parsed.StaticEndpoints == nil) { + return nil + } + + // Ensure that magicsock conn has the up to date static wireguard + // endpoints. Setting the endpoints here triggers an asynchronous update + // of the node's advertised endpoints. + if b.conf == nil && len(conf.Parsed.StaticEndpoints) != 0 || !reflect.DeepEqual(conf.Parsed.StaticEndpoints, b.conf.Parsed.StaticEndpoints) { + ms, ok := b.sys.MagicSock.GetOK() + if !ok { + b.logf("[unexpected] ReloadConfig: MagicSock not set") + } else { + ms.SetStaticEndpoints(views.SliceOf(conf.Parsed.StaticEndpoints)) + } + } + return nil +} + var assumeNetworkUpdateForTest = envknob.RegisterBool("TS_ASSUME_NETWORK_UP_FOR_TEST") // pauseOrResumeControlClientLocked pauses b.cc if there is no network available diff --git a/wgengine/magicsock/magicsock.go b/wgengine/magicsock/magicsock.go index 66264de32..cd7fb23da 100644 --- a/wgengine/magicsock/magicsock.go +++ b/wgengine/magicsock/magicsock.go @@ -14,6 +14,7 @@ import ( "io" "net" "net/netip" + "reflect" "runtime" "strconv" "strings" @@ -311,6 +312,12 @@ type Conn struct { // lastEPERMRebind tracks the last time a rebind was performed // after experiencing a syscall.EPERM. lastEPERMRebind syncs.AtomicValue[time.Time] + + // staticEndpoints are user set endpoints that this node should + // advertise amongst its wireguard endpoints. It is user's + // responsibility to ensure that traffic from these endpoints is routed + // to the node. + staticEndpoints views.Slice[netip.AddrPort] } // SetDebugLoggingEnabled controls whether spammy debug logging is enabled. @@ -636,6 +643,22 @@ func (c *Conn) setEndpoints(endpoints []tailcfg.Endpoint) (changed bool) { return true } +// SetStaticEndpoints sets static endpoints to the provided value and triggers +// an asynchronous update of the endpoints that this node advertises. +// Static endpoints are endpoints explicitly configured by user. +func (c *Conn) SetStaticEndpoints(ep views.Slice[netip.AddrPort]) { + c.mu.Lock() + if reflect.DeepEqual(c.staticEndpoints.AsSlice(), ep.AsSlice()) { + return + } + c.staticEndpoints = ep + c.mu.Unlock() + // Technically this is not a reSTUNning, but ReSTUN does what we need at + // this point- calls updateEndpoints or queues an update if there is + // already an in-progress update. + c.ReSTUN("static-endpoint-change") +} + // setNetInfoHavePortMap updates NetInfo.HavePortMap to true. func (c *Conn) setNetInfoHavePortMap() { c.mu.Lock() @@ -845,8 +868,10 @@ func (c *Conn) DiscoPublicKey() key.DiscoPublic { return c.discoPublic } -// determineEndpoints returns the machine's endpoint addresses. It -// does a STUN lookup (via netcheck) to determine its public address. +// determineEndpoints returns the machine's endpoint addresses. It does a STUN +// lookup (via netcheck) to determine its public address. Additionally any +// static enpoints provided by user are always added to the returned endpoints +// without validating if the node can be reached via those endpoints. // // c.mu must NOT be held. func (c *Conn) determineEndpoints(ctx context.Context) ([]tailcfg.Endpoint, error) { @@ -943,6 +968,10 @@ func (c *Conn) determineEndpoints(ctx context.Context) ([]tailcfg.Endpoint, erro // re-run. eps = c.endpointTracker.update(time.Now(), eps) + for i := range c.staticEndpoints.Len() { + addAddr(c.staticEndpoints.At(i), tailcfg.EndpointExplicitConf) + } + if localAddr := c.pconn4.LocalAddr(); localAddr.IP.IsUnspecified() { ips, loopback, err := netmon.LocalAddresses() if err != nil { @@ -2359,6 +2388,8 @@ func (c *Conn) onPortMapChanged() { c.ReSTUN("portmap-changed") } // ReSTUN triggers an address discovery. // The provided why string is for debug logging only. +// If Conn.staticEndpoints have been updated, calling ReSTUN will also result in +// the new endpoints being advertised. func (c *Conn) ReSTUN(why string) { c.mu.Lock() defer c.mu.Unlock()