|
|
|
|
@ -6,10 +6,13 @@ package ipnlocal |
|
|
|
|
|
|
|
|
|
import ( |
|
|
|
|
"context" |
|
|
|
|
"crypto/tls" |
|
|
|
|
"encoding/base64" |
|
|
|
|
"encoding/json" |
|
|
|
|
"errors" |
|
|
|
|
"fmt" |
|
|
|
|
"io" |
|
|
|
|
"math" |
|
|
|
|
"net" |
|
|
|
|
"net/http" |
|
|
|
|
"net/netip" |
|
|
|
|
@ -25,6 +28,7 @@ import ( |
|
|
|
|
"sync/atomic" |
|
|
|
|
"time" |
|
|
|
|
|
|
|
|
|
"go4.org/mem" |
|
|
|
|
"go4.org/netipx" |
|
|
|
|
"golang.org/x/exp/slices" |
|
|
|
|
"tailscale.com/client/tailscale/apitype" |
|
|
|
|
@ -62,6 +66,7 @@ import ( |
|
|
|
|
"tailscale.com/util/multierr" |
|
|
|
|
"tailscale.com/util/osshare" |
|
|
|
|
"tailscale.com/util/systemd" |
|
|
|
|
"tailscale.com/util/uniq" |
|
|
|
|
"tailscale.com/version" |
|
|
|
|
"tailscale.com/version/distro" |
|
|
|
|
"tailscale.com/wgengine" |
|
|
|
|
@ -135,8 +140,9 @@ type LocalBackend struct { |
|
|
|
|
sshAtomicBool atomic.Bool |
|
|
|
|
shutdownCalled bool // if Shutdown has been called
|
|
|
|
|
|
|
|
|
|
filterAtomic atomic.Pointer[filter.Filter] |
|
|
|
|
containsViaIPFuncAtomic syncs.AtomicValue[func(netip.Addr) bool] |
|
|
|
|
filterAtomic atomic.Pointer[filter.Filter] |
|
|
|
|
containsViaIPFuncAtomic syncs.AtomicValue[func(netip.Addr) bool] |
|
|
|
|
shouldInterceptTCPPortAtomic syncs.AtomicValue[func(uint16) bool] |
|
|
|
|
|
|
|
|
|
// The mutex protects the following elements.
|
|
|
|
|
mu sync.Mutex |
|
|
|
|
@ -192,6 +198,10 @@ type LocalBackend struct { |
|
|
|
|
directFileDoFinalRename bool // false on macOS, true on several NAS platforms
|
|
|
|
|
componentLogUntil map[string]componentLogState |
|
|
|
|
|
|
|
|
|
// ServeConfig fields. (also guarded by mu)
|
|
|
|
|
lastServeConfJSON mem.RO // last JSON that was parsed into serveConfig
|
|
|
|
|
serveConfig ipn.ServeConfig |
|
|
|
|
|
|
|
|
|
// statusLock must be held before calling statusChanged.Wait() or
|
|
|
|
|
// statusChanged.Broadcast().
|
|
|
|
|
statusLock sync.Mutex |
|
|
|
|
@ -257,6 +267,8 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, diale |
|
|
|
|
// Default filter blocks everything and logs nothing, until Start() is called.
|
|
|
|
|
b.setFilter(filter.NewAllowNone(logf, &netipx.IPSet{})) |
|
|
|
|
|
|
|
|
|
b.setTCPPortsIntercepted(nil) |
|
|
|
|
|
|
|
|
|
b.statusChanged = sync.NewCond(&b.statusLock) |
|
|
|
|
b.e.SetStatusCallback(b.setWgengineStatus) |
|
|
|
|
|
|
|
|
|
@ -1142,6 +1154,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
b.setAtomicValuesFromPrefs(b.prefs) |
|
|
|
|
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
wantRunning := b.prefs.WantRunning() |
|
|
|
|
@ -1906,6 +1919,7 @@ func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs) (err |
|
|
|
|
// value instead of making up a new one.
|
|
|
|
|
b.logf("using frontend prefs: %s", prefs.Pretty()) |
|
|
|
|
b.prefs = prefs.Clone().View() |
|
|
|
|
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked() |
|
|
|
|
b.writeServerModeStartState(b.userID, b.prefs) |
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
@ -1926,6 +1940,7 @@ func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs) (err |
|
|
|
|
prefs.WantRunning = false |
|
|
|
|
b.logf("using backend prefs; created empty state for %q: %s", key, prefs.Pretty()) |
|
|
|
|
b.prefs = prefs.View() |
|
|
|
|
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked() |
|
|
|
|
return nil |
|
|
|
|
case err != nil: |
|
|
|
|
return fmt.Errorf("backend prefs: store.ReadState(%q): %v", key, err) |
|
|
|
|
@ -1952,10 +1967,49 @@ func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs) (err |
|
|
|
|
b.prefs = prefs.View() |
|
|
|
|
|
|
|
|
|
b.setAtomicValuesFromPrefs(b.prefs) |
|
|
|
|
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked() |
|
|
|
|
|
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// setTCPPortsIntercepted populates b.shouldInterceptTCPPortAtomic with an
|
|
|
|
|
// efficient func for ShouldInterceptTCPPort to use, which is called on every
|
|
|
|
|
// incoming packet.
|
|
|
|
|
func (b *LocalBackend) setTCPPortsIntercepted(ports []uint16) { |
|
|
|
|
slices.Sort(ports) |
|
|
|
|
uniq.ModifySlice(&ports) |
|
|
|
|
b.logf("localbackend: handling TCP ports = %v", ports) |
|
|
|
|
var f func(uint16) bool |
|
|
|
|
switch len(ports) { |
|
|
|
|
case 0: |
|
|
|
|
f = func(uint16) bool { return false } |
|
|
|
|
case 1: |
|
|
|
|
f = func(p uint16) bool { return ports[0] == p } |
|
|
|
|
case 2: |
|
|
|
|
f = func(p uint16) bool { return ports[0] == p || ports[1] == p } |
|
|
|
|
case 3: |
|
|
|
|
f = func(p uint16) bool { return ports[0] == p || ports[1] == p || ports[2] == p } |
|
|
|
|
default: |
|
|
|
|
if len(ports) > 16 { |
|
|
|
|
m := map[uint16]bool{} |
|
|
|
|
for _, p := range ports { |
|
|
|
|
m[p] = true |
|
|
|
|
} |
|
|
|
|
f = func(p uint16) bool { return m[p] } |
|
|
|
|
} else { |
|
|
|
|
f = func(p uint16) bool { |
|
|
|
|
for _, x := range ports { |
|
|
|
|
if p == x { |
|
|
|
|
return true |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return false |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
b.shouldInterceptTCPPortAtomic.Store(f) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// setAtomicValuesFromPrefs populates sshAtomicBool and containsViaIPFuncAtomic
|
|
|
|
|
// from the prefs p, which may be nil.
|
|
|
|
|
func (b *LocalBackend) setAtomicValuesFromPrefs(p ipn.PrefsView) { |
|
|
|
|
@ -1963,6 +2017,7 @@ func (b *LocalBackend) setAtomicValuesFromPrefs(p ipn.PrefsView) { |
|
|
|
|
|
|
|
|
|
if !p.Valid() { |
|
|
|
|
b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(nil)) |
|
|
|
|
b.setTCPPortsIntercepted(nil) |
|
|
|
|
} else { |
|
|
|
|
b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(p.AdvertiseRoutes().Filter(tsaddr.IsViaPrefix))) |
|
|
|
|
} |
|
|
|
|
@ -2283,8 +2338,8 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) ipn |
|
|
|
|
// anyway. No-op if no exit node resolution is needed.
|
|
|
|
|
findExitNodeIDLocked(newp, netMap) |
|
|
|
|
b.prefs = newp.View() |
|
|
|
|
|
|
|
|
|
b.setAtomicValuesFromPrefs(b.prefs) |
|
|
|
|
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked() |
|
|
|
|
b.inServerMode = b.prefs.ForceDaemon() |
|
|
|
|
// We do this to avoid holding the lock while doing everything else.
|
|
|
|
|
|
|
|
|
|
@ -3281,6 +3336,7 @@ func (b *LocalBackend) ResetForClientDisconnect() { |
|
|
|
|
b.authURLSticky = "" |
|
|
|
|
b.activeLogin = "" |
|
|
|
|
b.setAtomicValuesFromPrefs(b.prefs) |
|
|
|
|
b.setTCPPortsIntercepted(nil) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (b *LocalBackend) ShouldRunSSH() bool { return b.sshAtomicBool.Load() && envknob.CanSSHD() } |
|
|
|
|
@ -3402,6 +3458,7 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) { |
|
|
|
|
} |
|
|
|
|
b.capFileSharing = fs |
|
|
|
|
|
|
|
|
|
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked() |
|
|
|
|
if nm == nil { |
|
|
|
|
b.nodeByAddr = nil |
|
|
|
|
return |
|
|
|
|
@ -3436,6 +3493,41 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) { |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// setTCPPortsInterceptedFromNetmapAndPrefsLocked calls setTCPPortsIntercepted with
|
|
|
|
|
// the ports that tailscaled should handle as a function of b.netMap and b.prefs.
|
|
|
|
|
//
|
|
|
|
|
// b.mu must be held.
|
|
|
|
|
func (b *LocalBackend) setTCPPortsInterceptedFromNetmapAndPrefsLocked() { |
|
|
|
|
handlePorts := make([]uint16, 0, 4) |
|
|
|
|
|
|
|
|
|
prefs := b.prefs |
|
|
|
|
if prefs.Valid() && prefs.RunSSH() && envknob.CanSSHD() { |
|
|
|
|
handlePorts = append(handlePorts, 22) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
nm := b.netMap |
|
|
|
|
if nm != nil && nm.SelfNode != nil { |
|
|
|
|
profileID := fmt.Sprintf("node-%s", nm.SelfNode.StableID) // TODO(maisem,bradfitz): something else?
|
|
|
|
|
confKey := ipn.ServeConfigKey(profileID) |
|
|
|
|
if confj, err := b.store.ReadState(confKey); err == nil { |
|
|
|
|
if !b.lastServeConfJSON.Equal(mem.B(confj)) { |
|
|
|
|
b.lastServeConfJSON = mem.B(confj) |
|
|
|
|
var conf ipn.ServeConfig |
|
|
|
|
if err := json.Unmarshal(confj, &conf); err != nil { |
|
|
|
|
b.logf("invalid ServeConfig %q in StateStore: %v", confKey, err) |
|
|
|
|
} |
|
|
|
|
b.serveConfig = conf |
|
|
|
|
} |
|
|
|
|
for p := range b.serveConfig.TCP { |
|
|
|
|
if p > 0 && p <= math.MaxUint16 { |
|
|
|
|
handlePorts = append(handlePorts, uint16(p)) |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
b.setTCPPortsIntercepted(handlePorts) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// operatorUserName returns the current pref's OperatorUser's name, or the
|
|
|
|
|
// empty string if none.
|
|
|
|
|
func (b *LocalBackend) operatorUserName() string { |
|
|
|
|
@ -3976,5 +4068,59 @@ func (b *LocalBackend) SetDevStateStore(key, value string) error { |
|
|
|
|
} |
|
|
|
|
err := b.store.WriteState(ipn.StateKey(key), []byte(value)) |
|
|
|
|
b.logf("SetDevStateStore(%q, %q) = %v", key, value, err) |
|
|
|
|
return err |
|
|
|
|
|
|
|
|
|
if err != nil { |
|
|
|
|
return err |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
b.mu.Lock() |
|
|
|
|
defer b.mu.Unlock() |
|
|
|
|
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked() |
|
|
|
|
|
|
|
|
|
return nil |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// ShouldInterceptTCPPort reports whether the given TCP port number to a
|
|
|
|
|
// Tailscale IP (not a subnet router, service IP, etc) should be intercepted by
|
|
|
|
|
// Tailscaled and handled in-process.
|
|
|
|
|
func (b *LocalBackend) ShouldInterceptTCPPort(port uint16) bool { |
|
|
|
|
return b.shouldInterceptTCPPortAtomic.Load()(port) |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
var runDevWebServer = envknob.RegisterBool("TS_DEV_WEBSERVER") |
|
|
|
|
|
|
|
|
|
func (b *LocalBackend) HandleInterceptedTCPConn(c net.Conn) { |
|
|
|
|
if !runDevWebServer() { |
|
|
|
|
b.logf("localbackend: closing TCP conn from %v to %v", c.RemoteAddr(), c.LocalAddr()) |
|
|
|
|
c.Close() |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// TODO(bradfitz): look up how; sniff SNI if ambiguous
|
|
|
|
|
hs := &http.Server{ |
|
|
|
|
TLSConfig: &tls.Config{ |
|
|
|
|
GetCertificate: b.getTLSServeCert, |
|
|
|
|
}, |
|
|
|
|
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
|
|
|
|
io.WriteString(w, "<h1>hello world</h1>this is tailscaled") |
|
|
|
|
}), |
|
|
|
|
} |
|
|
|
|
hs.ServeTLS(netutil.NewOneConnListener(c, nil), "", "") |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
func (b *LocalBackend) getTLSServeCert(hi *tls.ClientHelloInfo) (*tls.Certificate, error) { |
|
|
|
|
if hi == nil || hi.ServerName == "" { |
|
|
|
|
return nil, errors.New("no SNI ServerName") |
|
|
|
|
} |
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute) |
|
|
|
|
defer cancel() |
|
|
|
|
pair, err := b.GetCertPEM(ctx, hi.ServerName) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, err |
|
|
|
|
} |
|
|
|
|
cert, err := tls.X509KeyPair(pair.CertPEM, pair.KeyPEM) |
|
|
|
|
if err != nil { |
|
|
|
|
return nil, err |
|
|
|
|
} |
|
|
|
|
return &cert, nil |
|
|
|
|
} |
|
|
|
|
|