Compare commits
22 Commits
tiny/insta
...
v1.26.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a60f1ffe3 | ||
|
|
9685781e42 | ||
|
|
d6e7f41151 | ||
|
|
2b4140ee46 | ||
|
|
029508d8be | ||
|
|
b39592f68a | ||
|
|
e7d5d90037 | ||
|
|
e4d881923f | ||
|
|
8a3319905b | ||
|
|
2a8de4a7ee | ||
|
|
70a1780320 | ||
|
|
5e34bd61c8 | ||
|
|
0f8e4b22b1 | ||
|
|
5b81baa7d3 | ||
|
|
32f26a723b | ||
|
|
c1795c6af9 | ||
|
|
4ae7eff7c2 | ||
|
|
d76cce4ee7 | ||
|
|
7968313a33 | ||
|
|
4e6a465d8c | ||
|
|
89ac48af9d | ||
|
|
9fc6551b4e |
@@ -1 +1 @@
|
||||
1.25.0
|
||||
1.26.2
|
||||
|
||||
@@ -129,6 +129,8 @@ var localClient tailscale.LocalClient
|
||||
|
||||
// Run runs the CLI. The args do not include the binary name.
|
||||
func Run(args []string) (err error) {
|
||||
args = CleanUpArgs(args)
|
||||
|
||||
if len(args) == 1 && (args[0] == "-V" || args[0] == "--version") {
|
||||
args = []string{"version"}
|
||||
}
|
||||
|
||||
@@ -493,14 +493,16 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
applyImplicitPrefs(newPrefs, tt.curPrefs, tt.curUser)
|
||||
var got string
|
||||
if err := checkForAccidentalSettingReverts(newPrefs, tt.curPrefs, upCheckEnv{
|
||||
upEnv := upCheckEnv{
|
||||
goos: goos,
|
||||
flagSet: flagSet,
|
||||
curExitNodeIP: tt.curExitNodeIP,
|
||||
distro: tt.distro,
|
||||
}); err != nil {
|
||||
user: tt.curUser,
|
||||
}
|
||||
applyImplicitPrefs(newPrefs, tt.curPrefs, upEnv)
|
||||
var got string
|
||||
if err := checkForAccidentalSettingReverts(newPrefs, tt.curPrefs, upEnv); err != nil {
|
||||
got = err.Error()
|
||||
}
|
||||
if strings.TrimSpace(got) != tt.want {
|
||||
@@ -784,6 +786,10 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
curPrefs *ipn.Prefs
|
||||
env upCheckEnv // empty goos means "linux"
|
||||
|
||||
// checkUpdatePrefsMutations, if non-nil, is run with the new prefs after
|
||||
// updatePrefs might've mutated them (from applyImplicitPrefs).
|
||||
checkUpdatePrefsMutations func(t *testing.T, newPrefs *ipn.Prefs)
|
||||
|
||||
wantSimpleUp bool
|
||||
wantJustEditMP *ipn.MaskedPrefs
|
||||
wantErrSubtr string
|
||||
@@ -885,6 +891,28 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
},
|
||||
{
|
||||
// Issue 3808: explicitly empty --operator= should clear value.
|
||||
name: "explicit_empty_operator",
|
||||
flags: []string{"--operator="},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
CorpDNS: true,
|
||||
AllowSingleHosts: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
OperatorUser: "somebody",
|
||||
},
|
||||
env: upCheckEnv{user: "somebody", backendState: "Running"},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
OperatorUserSet: true,
|
||||
WantRunningSet: true,
|
||||
},
|
||||
checkUpdatePrefsMutations: func(t *testing.T, prefs *ipn.Prefs) {
|
||||
if prefs.OperatorUser != "" {
|
||||
t.Errorf("operator sent to backend should be empty; got %q", prefs.OperatorUser)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -909,6 +937,9 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
}
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tt.checkUpdatePrefsMutations != nil {
|
||||
tt.checkUpdatePrefsMutations(t, newPrefs)
|
||||
}
|
||||
if simpleUp != tt.wantSimpleUp {
|
||||
t.Fatalf("simpleUp=%v, want %v", simpleUp, tt.wantSimpleUp)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"runtime"
|
||||
"strconv"
|
||||
@@ -23,11 +25,14 @@ import (
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/control/controlhttp"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/paths"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
var debugCmd = &ffcli.Command{
|
||||
@@ -118,6 +123,17 @@ var debugCmd = &ffcli.Command{
|
||||
Exec: runVia,
|
||||
ShortHelp: "convert between site-specific IPv4 CIDRs and IPv6 'via' routes",
|
||||
},
|
||||
{
|
||||
Name: "ts2021",
|
||||
Exec: runTS2021,
|
||||
ShortHelp: "debug ts2021 protocol connectivity",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("ts2021")
|
||||
fs.StringVar(&ts2021Args.host, "host", "controlplane.tailscale.com", "hostname of control plane")
|
||||
fs.IntVar(&ts2021Args.version, "version", int(tailcfg.CurrentCapabilityVersion), "protocol version")
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -425,3 +441,67 @@ func runVia(ctx context.Context, args []string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var ts2021Args struct {
|
||||
host string // "controlplane.tailscale.com"
|
||||
version int // 27 or whatever
|
||||
}
|
||||
|
||||
func runTS2021(ctx context.Context, args []string) error {
|
||||
log.SetOutput(os.Stdout)
|
||||
log.SetFlags(log.Ltime | log.Lmicroseconds)
|
||||
|
||||
machinePrivate := key.NewMachine()
|
||||
var dialer net.Dialer
|
||||
|
||||
var keys struct {
|
||||
PublicKey key.MachinePublic
|
||||
}
|
||||
keysURL := "https://" + ts2021Args.host + "/key?v=" + strconv.Itoa(ts2021Args.version)
|
||||
log.Printf("Fetching keys from %s ...", keysURL)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", keysURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("Do: %v", err)
|
||||
return err
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
log.Printf("Status: %v", res.Status)
|
||||
return errors.New(res.Status)
|
||||
}
|
||||
if err := json.NewDecoder(res.Body).Decode(&keys); err != nil {
|
||||
log.Printf("JSON: %v", err)
|
||||
return fmt.Errorf("decoding /keys JSON: %w", err)
|
||||
}
|
||||
res.Body.Close()
|
||||
|
||||
dialFunc := func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
log.Printf("Dial(%q, %q) ...", network, address)
|
||||
c, err := dialer.DialContext(ctx, network, address)
|
||||
if err != nil {
|
||||
log.Printf("Dial(%q, %q) = %v", network, address, err)
|
||||
} else {
|
||||
log.Printf("Dial(%q, %q) = %v / %v", network, address, c.LocalAddr(), c.RemoteAddr())
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
conn, err := controlhttp.Dial(ctx, net.JoinHostPort(ts2021Args.host, "80"), machinePrivate, keys.PublicKey, uint16(ts2021Args.version), dialFunc)
|
||||
log.Printf("controlhttp.Dial = %p, %v", conn, err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("did noise handshake")
|
||||
|
||||
gotPeer := conn.Peer()
|
||||
if gotPeer != keys.PublicKey {
|
||||
log.Printf("peer = %v, want %v", gotPeer, keys.PublicKey)
|
||||
return errors.New("key mismatch")
|
||||
}
|
||||
|
||||
log.Printf("final underlying conn: %v / %v", conn.LocalAddr(), conn.RemoteAddr())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@@ -63,7 +62,7 @@ func runSSH(ctx context.Context, args []string) error {
|
||||
hostForSSH = v
|
||||
}
|
||||
|
||||
ssh, err := exec.LookPath("ssh")
|
||||
ssh, err := findSSH()
|
||||
if err != nil {
|
||||
// TODO(bradfitz): use Go's crypto/ssh client instead
|
||||
// of failing. But for now:
|
||||
|
||||
@@ -10,9 +10,14 @@ package cli
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func findSSH() (string, error) {
|
||||
return exec.LookPath("ssh")
|
||||
}
|
||||
|
||||
func execSSH(ssh string, argv []string) error {
|
||||
if err := syscall.Exec(ssh, argv, os.Environ()); err != nil {
|
||||
return err
|
||||
|
||||
@@ -8,6 +8,10 @@ import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
func findSSH() (string, error) {
|
||||
return "", errors.New("Not implemented")
|
||||
}
|
||||
|
||||
func execSSH(ssh string, argv []string) error {
|
||||
return errors.New("Not implemented")
|
||||
}
|
||||
|
||||
@@ -8,8 +8,21 @@ import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func findSSH() (string, error) {
|
||||
// use C:\Windows\System32\OpenSSH\ssh.exe since unexpected behavior
|
||||
// occured with ssh.exe provided by msys2/cygwin and other environments.
|
||||
if systemRoot := os.Getenv("SystemRoot"); systemRoot != "" {
|
||||
exe := filepath.Join(systemRoot, "System32", "OpenSSH", "ssh.exe")
|
||||
if st, err := os.Stat(exe); err == nil && !st.IsDir() {
|
||||
return exe, nil
|
||||
}
|
||||
}
|
||||
return exec.LookPath("ssh")
|
||||
}
|
||||
|
||||
func execSSH(ssh string, argv []string) error {
|
||||
// Don't use syscall.Exec on Windows, it's not fully implemented.
|
||||
cmd := exec.Command(ssh, argv[1:]...)
|
||||
|
||||
@@ -362,7 +362,7 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
// without changing any settings.
|
||||
func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, justEditMP *ipn.MaskedPrefs, err error) {
|
||||
if !env.upArgs.reset {
|
||||
applyImplicitPrefs(prefs, curPrefs, env.user)
|
||||
applyImplicitPrefs(prefs, curPrefs, env)
|
||||
|
||||
if err := checkForAccidentalSettingReverts(prefs, curPrefs, env); err != nil {
|
||||
return false, nil, err
|
||||
@@ -404,7 +404,7 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
|
||||
return simpleUp, justEditMP, nil
|
||||
}
|
||||
|
||||
func runUp(ctx context.Context, args []string) error {
|
||||
func runUp(ctx context.Context, args []string) (retErr error) {
|
||||
if len(args) > 0 {
|
||||
fatalf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
@@ -481,6 +481,12 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if retErr == nil {
|
||||
checkSSHUpWarnings(ctx)
|
||||
}
|
||||
}()
|
||||
|
||||
simpleUp, justEditMP, err := updatePrefs(prefs, curPrefs, env)
|
||||
if err != nil {
|
||||
fatalf("%s", err)
|
||||
@@ -676,6 +682,28 @@ func runUp(ctx context.Context, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
func checkSSHUpWarnings(ctx context.Context) {
|
||||
if !upArgs.runSSH {
|
||||
return
|
||||
}
|
||||
st, err := localClient.Status(ctx)
|
||||
if err != nil {
|
||||
// Ignore. Don't spam more.
|
||||
return
|
||||
}
|
||||
if len(st.Health) == 0 {
|
||||
return
|
||||
}
|
||||
if len(st.Health) == 1 && strings.Contains(st.Health[0], "SSH") {
|
||||
printf("%s\n", st.Health[0])
|
||||
return
|
||||
}
|
||||
printf("# Health check:\n")
|
||||
for _, m := range st.Health {
|
||||
printf(" - %s\n", m)
|
||||
}
|
||||
}
|
||||
|
||||
func printUpDoneJSON(state ipn.State, errorString string) {
|
||||
js := &upOutputJSON{BackendState: state.String(), Error: errorString}
|
||||
data, err := json.MarshalIndent(js, "", " ")
|
||||
@@ -853,13 +881,20 @@ func checkForAccidentalSettingReverts(newPrefs, curPrefs *ipn.Prefs, env upCheck
|
||||
return errors.New(sb.String())
|
||||
}
|
||||
|
||||
// applyImplicitPrefs mutates prefs to add implicit preferences. Currently
|
||||
// this is just the operator user, which only needs to be set if it doesn't
|
||||
// applyImplicitPrefs mutates prefs to add implicit preferences for the user operator.
|
||||
// If the operator flag is passed no action is taken, otherwise this only needs to be set if it doesn't
|
||||
// match the current user.
|
||||
//
|
||||
// curUser is os.Getenv("USER"). It's pulled out for testability.
|
||||
func applyImplicitPrefs(prefs, oldPrefs *ipn.Prefs, curUser string) {
|
||||
if prefs.OperatorUser == "" && oldPrefs.OperatorUser == curUser {
|
||||
func applyImplicitPrefs(prefs, oldPrefs *ipn.Prefs, env upCheckEnv) {
|
||||
explicitOperator := false
|
||||
env.flagSet.Visit(func(f *flag.Flag) {
|
||||
if f.Name == "operator" {
|
||||
explicitOperator = true
|
||||
}
|
||||
})
|
||||
|
||||
if prefs.OperatorUser == "" && oldPrefs.OperatorUser == env.user && !explicitOperator {
|
||||
prefs.OperatorUser = oldPrefs.OperatorUser
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
|
||||
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
|
||||
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
|
||||
L github.com/klauspost/compress/flate from nhooyr.io/websocket
|
||||
github.com/klauspost/compress/flate from nhooyr.io/websocket
|
||||
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
|
||||
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
|
||||
@@ -31,14 +31,16 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
go4.org/unsafe/assume-no-moving-gc from go4.org/intern
|
||||
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
|
||||
inet.af/netaddr from tailscale.com/cmd/tailscale/cli+
|
||||
L nhooyr.io/websocket from tailscale.com/derp/derphttp+
|
||||
L nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
||||
L nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
|
||||
nhooyr.io/websocket from tailscale.com/derp/derphttp+
|
||||
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
|
||||
nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
|
||||
tailscale.com from tailscale.com/version
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
||||
tailscale.com/control/controlbase from tailscale.com/control/controlhttp
|
||||
tailscale.com/control/controlhttp from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/control/controlknobs from tailscale.com/net/portmapper
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck
|
||||
@@ -48,21 +50,22 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/ipn from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+
|
||||
💣 tailscale.com/metrics from tailscale.com/derp
|
||||
tailscale.com/net/dnscache from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/dnscache from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/dnsfallback from tailscale.com/control/controlhttp
|
||||
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
|
||||
💣 tailscale.com/net/interfaces from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/net/neterror from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/netknob from tailscale.com/net/netns
|
||||
tailscale.com/net/netns from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/netutil from tailscale.com/client/tailscale
|
||||
tailscale.com/net/netutil from tailscale.com/client/tailscale+
|
||||
tailscale.com/net/packet from tailscale.com/wgengine/filter
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck
|
||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||
L tailscale.com/net/wsconn from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/wsconn from tailscale.com/derp/derphttp+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/syncs from tailscale.com/net/interfaces+
|
||||
@@ -93,12 +96,13 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/wgengine/filter from tailscale.com/types/netmap
|
||||
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/blake2s from tailscale.com/control/controlbase
|
||||
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls
|
||||
golang.org/x/crypto/chacha20poly1305 from crypto/tls+
|
||||
golang.org/x/crypto/cryptobyte from crypto/ecdsa+
|
||||
golang.org/x/crypto/cryptobyte/asn1 from crypto/ecdsa+
|
||||
golang.org/x/crypto/curve25519 from crypto/tls+
|
||||
golang.org/x/crypto/hkdf from crypto/tls
|
||||
golang.org/x/crypto/hkdf from crypto/tls+
|
||||
golang.org/x/crypto/nacl/box from tailscale.com/types/key
|
||||
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
|
||||
func main() {
|
||||
args := os.Args[1:]
|
||||
args = cli.CleanUpArgs(args)
|
||||
if name, _ := os.Executable(); strings.HasSuffix(filepath.Base(name), ".cgi") {
|
||||
args = []string{"web", "-cgi"}
|
||||
}
|
||||
|
||||
6
go.mod
6
go.mod
@@ -57,11 +57,11 @@ require (
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
|
||||
golang.org/x/tools v0.1.11-0.20220413170336-afc6aad76eb1
|
||||
golang.zx2c4.com/wireguard v0.0.0-20220317000134-95b48cdb3961
|
||||
golang.zx2c4.com/wireguard v0.0.0-20220703234212-c31a7b1ab478
|
||||
golang.zx2c4.com/wireguard/windows v0.4.10
|
||||
gvisor.dev/gvisor v0.0.0-20220407223209-21871174d445
|
||||
honnef.co/go/tools v0.4.0-0.dev.0.20220404092545-59d7a2877f83
|
||||
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6
|
||||
inet.af/netaddr v0.0.0-20220617031823-097006376321
|
||||
inet.af/peercred v0.0.0-20210906144145-0893ea02156a
|
||||
inet.af/wf v0.0.0-20211204062712-86aaea0a7310
|
||||
nhooyr.io/websocket v1.8.7
|
||||
@@ -258,7 +258,7 @@ require (
|
||||
github.com/xanzy/ssh-agent v0.3.1 // indirect
|
||||
github.com/yeya24/promlinter v0.1.0 // indirect
|
||||
go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 // indirect
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20220328175248-053ad81199eb // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
|
||||
11
go.sum
11
go.sum
@@ -1231,8 +1231,9 @@ go4.org/mem v0.0.0-20210711025021-927187094b94 h1:OAAkygi2Js191AJP1Ds42MhJRgeofe
|
||||
go4.org/mem v0.0.0-20210711025021-927187094b94/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37 h1:Tx9kY6yUkLge/pFG7IEMwDZy6CS2ajFc9TvQdPCW0uA=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 h1:FyBZqvoA/jbNzuAWLQE2kG820zMAkcilx6BMjGbL/E4=
|
||||
go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E=
|
||||
golang.org/x/crypto v0.0.0-20180501155221-613d6eafa307/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@@ -1655,8 +1656,8 @@ golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8T
|
||||
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY=
|
||||
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20210905140043-2ef39d47540c/go.mod h1:laHzsbfMhGSobUmruXWAyMKKHSqvIcrqZJMyHD+/3O8=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20220317000134-95b48cdb3961 h1:oIXcKhP1Ge6cRqdpQuldl0hf4mjIsNaXojabghlHuTs=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20220317000134-95b48cdb3961/go.mod h1:bVQfyl2sCM/QIIGHpWbFGfHPuDvqnCNkT6MQLTCjO/U=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20220703234212-c31a7b1ab478 h1:vDy//hdR+GnROE3OdYbQKt9rdtNdHkDtONvpRwmls/0=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20220703234212-c31a7b1ab478/go.mod h1:bVQfyl2sCM/QIIGHpWbFGfHPuDvqnCNkT6MQLTCjO/U=
|
||||
golang.zx2c4.com/wireguard/windows v0.4.10 h1:HmjzJnb+G4NCdX+sfjsQlsxGPuYaThxRbZUZFLyR0/s=
|
||||
golang.zx2c4.com/wireguard/windows v0.4.10/go.mod h1:v7w/8FC48tTBm1IzScDVPEEb0/GjLta+T0ybpP9UWRg=
|
||||
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
|
||||
@@ -1855,8 +1856,8 @@ howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCU
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
inet.af/netaddr v0.0.0-20210515010201-ad03edc7c841/go.mod h1:z0nx+Dh+7N7CC8V5ayHtHGpZpxLQZZxkIaaz6HN65Ls=
|
||||
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6 h1:acCzuUSQ79tGsM/O50VRFySfMm19IoMKL+sZztZkCxw=
|
||||
inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6/go.mod h1:y3MGhcFMlh0KZPMuXXow8mpjxxAk3yoDNsp4cQz54i8=
|
||||
inet.af/netaddr v0.0.0-20220617031823-097006376321 h1:B4dC8ySKTQXasnjDTMsoCMf1sQG4WsMej0WXaHxunmU=
|
||||
inet.af/netaddr v0.0.0-20220617031823-097006376321/go.mod h1:OIezDfdzOgFhuw4HuWapWq2e9l0H9tK4F1j+ETRtF3k=
|
||||
inet.af/peercred v0.0.0-20210906144145-0893ea02156a h1:qdkS8Q5/i10xU2ArJMKYhVa1DORzBfYS/qA2UK2jheg=
|
||||
inet.af/peercred v0.0.0-20210906144145-0893ea02156a/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU=
|
||||
inet.af/wf v0.0.0-20211204062712-86aaea0a7310 h1:0jKHTf+W75kYRyg5bto1UT+r18QmAz2u/5pAs/fx4zo=
|
||||
|
||||
@@ -416,6 +416,9 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
|
||||
s.Health = append(s.Health, err.Error())
|
||||
}
|
||||
}
|
||||
if m := b.sshOnButUnusableHealthCheckMessageLocked(); m != "" {
|
||||
s.Health = append(s.Health, m)
|
||||
}
|
||||
if b.netMap != nil {
|
||||
s.CertDomains = append([]string(nil), b.netMap.DNS.CertDomains...)
|
||||
s.MagicDNSSuffix = b.netMap.MagicDNSSuffix()
|
||||
@@ -1826,39 +1829,88 @@ func (b *LocalBackend) CheckPrefs(p *ipn.Prefs) error {
|
||||
}
|
||||
|
||||
func (b *LocalBackend) checkPrefsLocked(p *ipn.Prefs) error {
|
||||
var errs []error
|
||||
if p.Hostname == "badhostname.tailscale." {
|
||||
// Keep this one just for testing.
|
||||
return errors.New("bad hostname [test]")
|
||||
errs = append(errs, errors.New("bad hostname [test]"))
|
||||
}
|
||||
if p.RunSSH {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
if distro.Get() == distro.Synology && !envknob.UseWIPCode() {
|
||||
return errors.New("The Tailscale SSH server does not run on Synology.")
|
||||
}
|
||||
// otherwise okay
|
||||
case "darwin":
|
||||
// okay only in tailscaled mode for now.
|
||||
if version.IsSandboxedMacOS() {
|
||||
return errors.New("The Tailscale SSH server does not run in sandboxed Tailscale GUI builds.")
|
||||
}
|
||||
if !envknob.UseWIPCode() {
|
||||
return errors.New("The Tailscale SSH server is disabled on macOS tailscaled by default. To try, set env TAILSCALE_USE_WIP_CODE=1")
|
||||
}
|
||||
default:
|
||||
return errors.New("The Tailscale SSH server is not supported on " + runtime.GOOS)
|
||||
if err := b.checkSSHPrefsLocked(p); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return multierr.New(errs...)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) checkSSHPrefsLocked(p *ipn.Prefs) error {
|
||||
if !p.RunSSH {
|
||||
return nil
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
if distro.Get() == distro.Synology && !envknob.UseWIPCode() {
|
||||
return errors.New("The Tailscale SSH server does not run on Synology.")
|
||||
}
|
||||
if !canSSH {
|
||||
return errors.New("The Tailscale SSH server has been administratively disabled.")
|
||||
// otherwise okay
|
||||
case "darwin":
|
||||
// okay only in tailscaled mode for now.
|
||||
if version.IsSandboxedMacOS() {
|
||||
return errors.New("The Tailscale SSH server does not run in sandboxed Tailscale GUI builds.")
|
||||
}
|
||||
if b.netMap != nil && b.netMap.SSHPolicy == nil &&
|
||||
envknob.SSHPolicyFile() == "" && !envknob.SSHIgnoreTailnetPolicy() {
|
||||
return errors.New("Unable to enable local Tailscale SSH server; not enabled/configured on Tailnet.")
|
||||
if !envknob.UseWIPCode() {
|
||||
return errors.New("The Tailscale SSH server is disabled on macOS tailscaled by default. To try, set env TAILSCALE_USE_WIP_CODE=1")
|
||||
}
|
||||
default:
|
||||
return errors.New("The Tailscale SSH server is not supported on " + runtime.GOOS)
|
||||
}
|
||||
if !canSSH {
|
||||
return errors.New("The Tailscale SSH server has been administratively disabled.")
|
||||
}
|
||||
if envknob.SSHIgnoreTailnetPolicy() || envknob.SSHPolicyFile() != "" {
|
||||
return nil
|
||||
}
|
||||
if b.netMap != nil {
|
||||
if !hasCapability(b.netMap, tailcfg.CapabilitySSH) {
|
||||
if b.isDefaultServerLocked() {
|
||||
return errors.New("Unable to enable local Tailscale SSH server; not enabled on Tailnet. See https://tailscale.com/s/ssh")
|
||||
}
|
||||
return errors.New("Unable to enable local Tailscale SSH server; not enabled on Tailnet.")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) sshOnButUnusableHealthCheckMessageLocked() (healthMessage string) {
|
||||
if b.prefs == nil || !b.prefs.RunSSH {
|
||||
return ""
|
||||
}
|
||||
if envknob.SSHIgnoreTailnetPolicy() || envknob.SSHPolicyFile() != "" {
|
||||
return "development SSH policy in use"
|
||||
}
|
||||
nm := b.netMap
|
||||
if nm == nil {
|
||||
return ""
|
||||
}
|
||||
if nm.SSHPolicy != nil && len(nm.SSHPolicy.Rules) > 0 {
|
||||
return ""
|
||||
}
|
||||
isDefault := b.isDefaultServerLocked()
|
||||
isAdmin := hasCapability(nm, tailcfg.CapabilityAdmin)
|
||||
|
||||
if !isAdmin {
|
||||
return "Tailscale SSH enabled, but access controls don't allow anyone to access this device. Ask your admin to update your tailnet's ACLs to allow access."
|
||||
}
|
||||
if !isDefault {
|
||||
return "Tailscale SSH enabled, but access controls don't allow anyone to access this device. Update your tailnet's ACLs to allow access."
|
||||
}
|
||||
return "Tailscale SSH enabled, but access controls don't allow anyone to access this device. Update your tailnet's ACLs at https://tailscale.com/s/ssh-policy"
|
||||
}
|
||||
|
||||
func (b *LocalBackend) isDefaultServerLocked() bool {
|
||||
if b.prefs == nil {
|
||||
return true // assume true until set otherwise
|
||||
}
|
||||
return b.prefs.ControlURLOrDefault() == ipn.DefaultControlURL
|
||||
}
|
||||
|
||||
func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
|
||||
b.mu.Lock()
|
||||
p0 := b.prefs.Clone()
|
||||
|
||||
@@ -115,6 +115,9 @@ func parsePCPMapResponse(resp []byte) (*pcpMapping, error) {
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("Invalid PCP common header")
|
||||
}
|
||||
if res.ResultCode == pcpCodeNotAuthorized {
|
||||
return nil, fmt.Errorf("PCP is implemented but not enabled in the router")
|
||||
}
|
||||
if res.ResultCode != pcpCodeOK {
|
||||
return nil, fmt.Errorf("PCP response not ok, code %d", res.ResultCode)
|
||||
}
|
||||
|
||||
@@ -206,7 +206,7 @@ func beIncubator(args []string) error {
|
||||
// If we are trying to launch a login shell, just exec into login
|
||||
// instead. We can only do this if a TTY was requested, otherwise login
|
||||
// exits immediately, which breaks things likes mosh and VSCode.
|
||||
return unix.Exec(ia.loginCmdPath, []string{ia.loginCmdPath, "-f", ia.localUser, "-h", ia.remoteIP, "-p"}, os.Environ())
|
||||
return unix.Exec(ia.loginCmdPath, ia.loginArgs(), os.Environ())
|
||||
}
|
||||
|
||||
// Inform the system that we are about to log someone in.
|
||||
@@ -225,7 +225,8 @@ func beIncubator(args []string) error {
|
||||
}
|
||||
groupIDs = append(groupIDs, int(gid))
|
||||
}
|
||||
if err := syscall.Setgroups(groupIDs); err != nil {
|
||||
|
||||
if err := setGroups(groupIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
if egid := os.Getegid(); egid != ia.gid {
|
||||
|
||||
21
ssh/tailssh/incubator_darwin.go
Normal file
21
ssh/tailssh/incubator_darwin.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package tailssh
|
||||
|
||||
import "syscall"
|
||||
|
||||
func (ia *incubatorArgs) loginArgs() []string {
|
||||
return []string{ia.loginCmdPath, "-fp", "-h", ia.remoteIP, ia.localUser}
|
||||
}
|
||||
|
||||
func setGroups(groupIDs []int) error {
|
||||
// darwin returns "invalid argument" if more than 16 groups are passed to syscall.Setgroups
|
||||
// some info can be found here:
|
||||
// https://opensource.apple.com/source/samba/samba-187.8/patches/support-darwin-initgroups-syscall.auto.html
|
||||
// this fix isn't great, as anyone reading this has probably just wasted hours figuring out why
|
||||
// some permissions thing isn't working, due to some arbitrary group ordering, but it at least allows
|
||||
// this to work for more things than it previously did.
|
||||
return syscall.Setgroups(groupIDs[:16])
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -173,3 +174,24 @@ func maybeStartLoginSessionLinux(logf logger.Logf, ia incubatorArgs) (func() err
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func fileExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (ia *incubatorArgs) loginArgs() []string {
|
||||
if distro.Get() == distro.Arch && !fileExists("/etc/pam.d/remote") {
|
||||
// See https://github.com/tailscale/tailscale/issues/4924
|
||||
//
|
||||
// Arch uses a different login binary that makes the -h flag set the PAM
|
||||
// service to "remote". So if they don't have that configured, don't
|
||||
// pass -h.
|
||||
return []string{ia.loginCmdPath, "-f", ia.localUser, "-p"}
|
||||
}
|
||||
return []string{ia.loginCmdPath, "-f", ia.localUser, "-h", ia.remoteIP, "-p"}
|
||||
}
|
||||
|
||||
func setGroups(groupIDs []int) error {
|
||||
return syscall.Setgroups(groupIDs)
|
||||
}
|
||||
|
||||
@@ -62,15 +62,14 @@ type server struct {
|
||||
sessionWaitGroup sync.WaitGroup
|
||||
|
||||
// mu protects the following
|
||||
mu sync.Mutex
|
||||
activeSessionByH map[string]*sshSession // ssh.SessionID (DH H) => session
|
||||
activeSessionBySharedID map[string]*sshSession // yyymmddThhmmss-XXXXX => session
|
||||
fetchPublicKeysCache map[string]pubKeyCacheEntry // by https URL
|
||||
shutdownCalled bool
|
||||
mu sync.Mutex
|
||||
activeConns map[*conn]bool // set; value is always true
|
||||
fetchPublicKeysCache map[string]pubKeyCacheEntry // by https URL
|
||||
shutdownCalled bool
|
||||
}
|
||||
|
||||
func (srv *server) now() time.Time {
|
||||
if srv.timeNow != nil {
|
||||
if srv != nil && srv.timeNow != nil {
|
||||
return srv.timeNow()
|
||||
}
|
||||
return time.Now()
|
||||
@@ -91,14 +90,28 @@ func init() {
|
||||
})
|
||||
}
|
||||
|
||||
func (srv *server) trackActiveConn(c *conn, add bool) {
|
||||
srv.mu.Lock()
|
||||
defer srv.mu.Unlock()
|
||||
if add {
|
||||
mak.Set(&srv.activeConns, c, true)
|
||||
return
|
||||
}
|
||||
delete(srv.activeConns, c)
|
||||
}
|
||||
|
||||
// HandleSSHConn handles a Tailscale SSH connection from c.
|
||||
func (srv *server) HandleSSHConn(c net.Conn) error {
|
||||
// This is the entry point for all SSH connections.
|
||||
// When this returns, the connection is closed.
|
||||
func (srv *server) HandleSSHConn(nc net.Conn) error {
|
||||
metricIncomingConnections.Add(1)
|
||||
ss, err := srv.newConn()
|
||||
c, err := srv.newConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ss.HandleConn(c)
|
||||
srv.trackActiveConn(c, true) // add
|
||||
defer srv.trackActiveConn(c, false) // remove
|
||||
c.HandleConn(nc)
|
||||
|
||||
// Return nil to signal to netstack's interception that it doesn't need to
|
||||
// log. If ss.HandleConn had problems, it can log itself (ideally on an
|
||||
@@ -110,11 +123,13 @@ func (srv *server) HandleSSHConn(c net.Conn) error {
|
||||
func (srv *server) Shutdown() {
|
||||
srv.mu.Lock()
|
||||
srv.shutdownCalled = true
|
||||
for _, s := range srv.activeSessionByH {
|
||||
s.ctx.CloseWithError(userVisibleError{
|
||||
fmt.Sprintf("Tailscale SSH is shutting down.\r\n"),
|
||||
context.Canceled,
|
||||
})
|
||||
for c := range srv.activeConns {
|
||||
for _, s := range c.sessions {
|
||||
s.ctx.CloseWithError(userVisibleError{
|
||||
fmt.Sprintf("Tailscale SSH is shutting down.\r\n"),
|
||||
context.Canceled,
|
||||
})
|
||||
}
|
||||
}
|
||||
srv.mu.Unlock()
|
||||
srv.sessionWaitGroup.Wait()
|
||||
@@ -125,8 +140,8 @@ func (srv *server) Shutdown() {
|
||||
func (srv *server) OnPolicyChange() {
|
||||
srv.mu.Lock()
|
||||
defer srv.mu.Unlock()
|
||||
for _, s := range srv.activeSessionByH {
|
||||
go s.checkStillValid()
|
||||
for c := range srv.activeConns {
|
||||
go c.checkStillValid()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,25 +150,29 @@ func (srv *server) OnPolicyChange() {
|
||||
type conn struct {
|
||||
*ssh.Server
|
||||
|
||||
// now is the time to consider the present moment for the
|
||||
// purposes of rule evaluation.
|
||||
now time.Time
|
||||
insecureSkipTailscaleAuth bool // used by tests.
|
||||
|
||||
connID string // ID that's shared with control
|
||||
action0 *tailcfg.SSHAction // first matching action
|
||||
srv *server
|
||||
info *sshConnInfo // set by setInfo
|
||||
localUser *user.User // set by checkAuth
|
||||
userGroupIDs []string // set by checkAuth
|
||||
|
||||
insecureSkipTailscaleAuth bool // used by tests.
|
||||
mu sync.Mutex // protects the following
|
||||
// idH is the RFC4253 sec8 hash H. It is used to identify the connection,
|
||||
// and is shared among all sessions. It should not be shared outside
|
||||
// process. It is confusingly referred to as SessionID by the gliderlabs/ssh
|
||||
// library.
|
||||
idH string
|
||||
pubKey gossh.PublicKey // set by authorizeSession
|
||||
finalAction *tailcfg.SSHAction // set by authorizeSession
|
||||
finalActionErr error // set by authorizeSession
|
||||
sessions []*sshSession
|
||||
}
|
||||
|
||||
func (c *conn) logf(format string, args ...any) {
|
||||
if c.info == nil {
|
||||
c.srv.logf(format, args...)
|
||||
return
|
||||
}
|
||||
format = fmt.Sprintf("%v: %v", c.info.String(), format)
|
||||
format = fmt.Sprintf("%v: %v", c.connID, format)
|
||||
c.srv.logf(format, args...)
|
||||
}
|
||||
|
||||
@@ -247,21 +266,23 @@ func (c *conn) ServerConfig(ctx ssh.Context) *gossh.ServerConfig {
|
||||
|
||||
func (srv *server) newConn() (*conn, error) {
|
||||
srv.mu.Lock()
|
||||
shutdownCalled := srv.shutdownCalled
|
||||
srv.mu.Unlock()
|
||||
if shutdownCalled {
|
||||
if srv.shutdownCalled {
|
||||
srv.mu.Unlock()
|
||||
// Stop accepting new connections.
|
||||
// Connections in the auth phase are handled in handleConnPostSSHAuth.
|
||||
// Existing sessions are terminated by Shutdown.
|
||||
return nil, gossh.ErrDenied
|
||||
}
|
||||
c := &conn{srv: srv, now: srv.now()}
|
||||
srv.mu.Unlock()
|
||||
c := &conn{srv: srv}
|
||||
now := srv.now()
|
||||
c.connID = fmt.Sprintf("conn-%s-%02x", now.UTC().Format("20060102T150405"), randBytes(5))
|
||||
c.Server = &ssh.Server{
|
||||
Version: "Tailscale",
|
||||
Handler: c.handleConnPostSSHAuth,
|
||||
Handler: c.handleSessionPostSSHAuth,
|
||||
RequestHandlers: map[string]ssh.RequestHandler{},
|
||||
SubsystemHandlers: map[string]ssh.SubsystemHandler{
|
||||
"sftp": c.handleConnPostSSHAuth,
|
||||
"sftp": c.handleSessionPostSSHAuth,
|
||||
},
|
||||
|
||||
// Note: the direct-tcpip channel handler and LocalPortForwardingCallback
|
||||
@@ -270,7 +291,7 @@ func (srv *server) newConn() (*conn, error) {
|
||||
ChannelHandlers: map[string]ssh.ChannelHandler{
|
||||
"direct-tcpip": ssh.DirectTCPIPHandler,
|
||||
},
|
||||
LocalPortForwardingCallback: srv.mayForwardLocalPortTo,
|
||||
LocalPortForwardingCallback: c.mayForwardLocalPortTo,
|
||||
|
||||
PublicKeyHandler: c.PublicKeyHandler,
|
||||
ServerConfigCallback: c.ServerConfig,
|
||||
@@ -298,16 +319,12 @@ func (srv *server) newConn() (*conn, error) {
|
||||
// mayForwardLocalPortTo reports whether the ctx should be allowed to port forward
|
||||
// to the specified host and port.
|
||||
// TODO(bradfitz/maisem): should we have more checks on host/port?
|
||||
func (srv *server) mayForwardLocalPortTo(ctx ssh.Context, destinationHost string, destinationPort uint32) bool {
|
||||
ss, ok := srv.getSessionForContext(ctx)
|
||||
if !ok {
|
||||
return false
|
||||
func (c *conn) mayForwardLocalPortTo(ctx ssh.Context, destinationHost string, destinationPort uint32) bool {
|
||||
if c.finalAction != nil && c.finalAction.AllowLocalPortForwarding {
|
||||
metricLocalPortForward.Add(1)
|
||||
return true
|
||||
}
|
||||
if !ss.action.AllowLocalPortForwarding {
|
||||
return false
|
||||
}
|
||||
metricLocalPortForward.Add(1)
|
||||
return true
|
||||
return false
|
||||
}
|
||||
|
||||
// havePubKeyPolicy reports whether any policy rule may provide access by means
|
||||
@@ -401,6 +418,7 @@ func (c *conn) setInfo(cm gossh.ConnMetadata) error {
|
||||
ci.uprof = &uprof
|
||||
|
||||
c.info = ci
|
||||
c.logf("handling conn: %v", ci.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -516,32 +534,47 @@ func (srv *server) fetchPublicKeysURL(url string) ([]string, error) {
|
||||
return lines, err
|
||||
}
|
||||
|
||||
// handleConnPostSSHAuth runs an SSH session after the SSH-level authentication,
|
||||
// but not necessarily before all the Tailscale-level extra verification has
|
||||
// completed. It also handles SFTP requests.
|
||||
func (c *conn) handleConnPostSSHAuth(s ssh.Session) {
|
||||
if s.PublicKey() != nil {
|
||||
metricPublicKeyConnections.Add(1)
|
||||
func (c *conn) authorizeSession(s ssh.Session) (_ *contextReader, ok bool) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
idH := s.Context().(ssh.Context).SessionID()
|
||||
if c.idH == "" {
|
||||
c.idH = idH
|
||||
} else if c.idH != idH {
|
||||
c.logf("ssh: session ID mismatch: %q != %q", c.idH, idH)
|
||||
s.Exit(1)
|
||||
return nil, false
|
||||
}
|
||||
sshUser := s.User()
|
||||
cr := &contextReader{r: s}
|
||||
action, err := c.resolveTerminalAction(s, cr)
|
||||
action, err := c.resolveTerminalActionLocked(s, cr)
|
||||
if err != nil {
|
||||
c.logf("resolveTerminalAction: %v", err)
|
||||
io.WriteString(s.Stderr(), "Access Denied: failed during authorization check.\r\n")
|
||||
s.Exit(1)
|
||||
return
|
||||
return nil, false
|
||||
}
|
||||
if action.Reject || !action.Accept {
|
||||
c.logf("access denied for %v", c.info.uprof.LoginName)
|
||||
s.Exit(1)
|
||||
return nil, false
|
||||
}
|
||||
return cr, true
|
||||
}
|
||||
|
||||
// handleSessionPostSSHAuth runs an SSH session after the SSH-level authentication,
|
||||
// but not necessarily before all the Tailscale-level extra verification has
|
||||
// completed. It also handles SFTP requests.
|
||||
func (c *conn) handleSessionPostSSHAuth(s ssh.Session) {
|
||||
// Now that we have passed the SSH-level authentication, we can start the
|
||||
// Tailscale-level extra verification. This means that we are going to
|
||||
// evaluate the policy provided by control against the incoming SSH session.
|
||||
cr, ok := c.authorizeSession(s)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if s.PublicKey() != nil {
|
||||
metricPublicKeyAccepts.Add(1)
|
||||
}
|
||||
|
||||
if cr.HasOutstandingRead() {
|
||||
// There was some buffered input while we were waiting for the policy
|
||||
// decision.
|
||||
s = contextReaderSesssion{s, cr}
|
||||
}
|
||||
|
||||
@@ -555,20 +588,37 @@ func (c *conn) handleConnPostSSHAuth(s ssh.Session) {
|
||||
return
|
||||
}
|
||||
|
||||
ss := c.newSSHSession(s, action)
|
||||
ss.logf("handling new SSH connection from %v (%v) to ssh-user %q", c.info.uprof.LoginName, c.info.src.IP(), sshUser)
|
||||
ss.logf("access granted to %v as ssh-user %q", c.info.uprof.LoginName, sshUser)
|
||||
ss := c.newSSHSession(s)
|
||||
ss.logf("handling new SSH connection from %v (%v) to ssh-user %q", c.info.uprof.LoginName, c.info.src.IP(), c.localUser.Username)
|
||||
ss.logf("access granted to %v as ssh-user %q", c.info.uprof.LoginName, c.localUser.Username)
|
||||
ss.run()
|
||||
}
|
||||
|
||||
// resolveTerminalAction either returns action0 (if it's Accept or Reject) or
|
||||
// resolveTerminalActionLocked either returns action0 (if it's Accept or Reject) or
|
||||
// else loops, fetching new SSHActions from the control plane.
|
||||
//
|
||||
// Any action with a Message in the chain will be printed to s.
|
||||
//
|
||||
// The returned SSHAction will be either Reject or Accept.
|
||||
func (c *conn) resolveTerminalAction(s ssh.Session, cr *contextReader) (*tailcfg.SSHAction, error) {
|
||||
action := c.action0
|
||||
//
|
||||
// c.mu must be held.
|
||||
func (c *conn) resolveTerminalActionLocked(s ssh.Session, cr *contextReader) (action *tailcfg.SSHAction, err error) {
|
||||
if c.finalAction != nil || c.finalActionErr != nil {
|
||||
return c.finalAction, c.finalActionErr
|
||||
}
|
||||
|
||||
if s.PublicKey() != nil {
|
||||
metricPublicKeyConnections.Add(1)
|
||||
}
|
||||
defer func() {
|
||||
c.finalAction = action
|
||||
c.finalActionErr = err
|
||||
c.pubKey = s.PublicKey()
|
||||
if c.pubKey != nil && action.Accept {
|
||||
metricPublicKeyAccepts.Add(1)
|
||||
}
|
||||
}()
|
||||
action = c.action0
|
||||
|
||||
var awaitReadOnce sync.Once // to start Reads on cr
|
||||
var sawInterrupt syncs.AtomicBool
|
||||
@@ -672,13 +722,11 @@ func (c *conn) expandPublicKeyURL(pubKeyURL string) string {
|
||||
// sshSession is an accepted Tailscale SSH session.
|
||||
type sshSession struct {
|
||||
ssh.Session
|
||||
idH string // the RFC4253 sec8 hash H; don't share outside process
|
||||
sharedID string // ID that's shared with control
|
||||
logf logger.Logf
|
||||
|
||||
ctx *sshContext // implements context.Context
|
||||
conn *conn
|
||||
action *tailcfg.SSHAction
|
||||
agentListener net.Listener // non-nil if agent-forwarding requested+allowed
|
||||
|
||||
// initialized by launchProcess:
|
||||
@@ -699,22 +747,21 @@ func (ss *sshSession) vlogf(format string, args ...interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) newSSHSession(s ssh.Session, action *tailcfg.SSHAction) *sshSession {
|
||||
sharedID := fmt.Sprintf("%s-%02x", c.now.UTC().Format("20060102T150405"), randBytes(5))
|
||||
func (c *conn) newSSHSession(s ssh.Session) *sshSession {
|
||||
sharedID := fmt.Sprintf("sess-%s-%02x", c.srv.now().UTC().Format("20060102T150405"), randBytes(5))
|
||||
c.logf("starting session: %v", sharedID)
|
||||
return &sshSession{
|
||||
Session: s,
|
||||
idH: s.Context().(ssh.Context).SessionID(),
|
||||
sharedID: sharedID,
|
||||
ctx: newSSHContext(),
|
||||
conn: c,
|
||||
logf: logger.WithPrefix(c.srv.logf, "ssh-session("+sharedID+"): "),
|
||||
action: action,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) isStillValid(pubKey ssh.PublicKey) bool {
|
||||
a, localUser, err := c.evaluatePolicy(pubKey)
|
||||
// isStillValid reports whether the conn is still valid.
|
||||
func (c *conn) isStillValid() bool {
|
||||
a, localUser, err := c.evaluatePolicy(c.pubKey)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -724,18 +771,20 @@ func (c *conn) isStillValid(pubKey ssh.PublicKey) bool {
|
||||
return c.localUser.Username == localUser
|
||||
}
|
||||
|
||||
// checkStillValid checks that the session is still valid per the latest SSHPolicy.
|
||||
// If not, it terminates the session.
|
||||
func (ss *sshSession) checkStillValid() {
|
||||
if ss.conn.isStillValid(ss.PublicKey()) {
|
||||
// checkStillValid checks that the conn is still valid per the latest SSHPolicy.
|
||||
// If not, it terminates all sessions associated with the conn.
|
||||
func (c *conn) checkStillValid() {
|
||||
if c.isStillValid() {
|
||||
return
|
||||
}
|
||||
metricPolicyChangeKick.Add(1)
|
||||
ss.logf("session no longer valid per new SSH policy; closing")
|
||||
ss.ctx.CloseWithError(userVisibleError{
|
||||
fmt.Sprintf("Access revoked.\r\n"),
|
||||
context.Canceled,
|
||||
})
|
||||
c.logf("session no longer valid per new SSH policy; closing")
|
||||
for _, s := range c.sessions {
|
||||
s.ctx.CloseWithError(userVisibleError{
|
||||
fmt.Sprintf("Access revoked.\r\n"),
|
||||
context.Canceled,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (c *conn) fetchSSHAction(ctx context.Context, url string) (*tailcfg.SSHAction, error) {
|
||||
@@ -798,41 +847,27 @@ func (ss *sshSession) killProcessOnContextDone() {
|
||||
})
|
||||
}
|
||||
|
||||
// sessionAction returns the SSHAction associated with the session.
|
||||
func (srv *server) getSessionForContext(sctx ssh.Context) (ss *sshSession, ok bool) {
|
||||
srv.mu.Lock()
|
||||
defer srv.mu.Unlock()
|
||||
ss, ok = srv.activeSessionByH[sctx.SessionID()]
|
||||
return
|
||||
}
|
||||
|
||||
// startSessionLocked registers ss as an active session.
|
||||
// It must be called with srv.mu held.
|
||||
func (srv *server) startSessionLocked(ss *sshSession) {
|
||||
srv.sessionWaitGroup.Add(1)
|
||||
if ss.idH == "" {
|
||||
panic("empty idH")
|
||||
}
|
||||
func (c *conn) startSessionLocked(ss *sshSession) {
|
||||
c.srv.sessionWaitGroup.Add(1)
|
||||
if ss.sharedID == "" {
|
||||
panic("empty sharedID")
|
||||
}
|
||||
if _, dup := srv.activeSessionByH[ss.idH]; dup {
|
||||
panic("dup idH")
|
||||
}
|
||||
if _, dup := srv.activeSessionBySharedID[ss.sharedID]; dup {
|
||||
panic("dup sharedID")
|
||||
}
|
||||
mak.Set(&srv.activeSessionByH, ss.idH, ss)
|
||||
mak.Set(&srv.activeSessionBySharedID, ss.sharedID, ss)
|
||||
c.sessions = append(c.sessions, ss)
|
||||
}
|
||||
|
||||
// endSession unregisters s from the list of active sessions.
|
||||
func (srv *server) endSession(ss *sshSession) {
|
||||
defer srv.sessionWaitGroup.Done()
|
||||
srv.mu.Lock()
|
||||
defer srv.mu.Unlock()
|
||||
delete(srv.activeSessionByH, ss.idH)
|
||||
delete(srv.activeSessionBySharedID, ss.sharedID)
|
||||
func (c *conn) endSession(ss *sshSession) {
|
||||
defer c.srv.sessionWaitGroup.Done()
|
||||
c.srv.mu.Lock()
|
||||
defer c.srv.mu.Unlock()
|
||||
for i, s := range c.sessions {
|
||||
if s == ss {
|
||||
c.sessions = append(c.sessions[:i], c.sessions[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var errSessionDone = errors.New("session is done")
|
||||
@@ -841,7 +876,7 @@ var errSessionDone = errors.New("session is done")
|
||||
// forwards agent connections between the listener and the ssh.Session.
|
||||
// On success, it assigns ss.agentListener.
|
||||
func (ss *sshSession) handleSSHAgentForwarding(s ssh.Session, lu *user.User) error {
|
||||
if !ssh.AgentRequested(ss) || !ss.action.AllowAgentForwarding {
|
||||
if !ssh.AgentRequested(ss) || !ss.conn.finalAction.AllowAgentForwarding {
|
||||
return nil
|
||||
}
|
||||
ss.logf("ssh: agent forwarding requested")
|
||||
@@ -906,15 +941,15 @@ func (ss *sshSession) run() {
|
||||
ss.Exit(1)
|
||||
return
|
||||
}
|
||||
srv.startSessionLocked(ss)
|
||||
ss.conn.startSessionLocked(ss)
|
||||
srv.mu.Unlock()
|
||||
|
||||
defer srv.endSession(ss)
|
||||
defer ss.conn.endSession(ss)
|
||||
|
||||
if ss.action.SessionDuration != 0 {
|
||||
t := time.AfterFunc(ss.action.SessionDuration, func() {
|
||||
if ss.conn.finalAction.SessionDuration != 0 {
|
||||
t := time.AfterFunc(ss.conn.finalAction.SessionDuration, func() {
|
||||
ss.ctx.CloseWithError(userVisibleError{
|
||||
fmt.Sprintf("Session timeout of %v elapsed.", ss.action.SessionDuration),
|
||||
fmt.Sprintf("Session timeout of %v elapsed.", ss.conn.finalAction.SessionDuration),
|
||||
context.DeadlineExceeded,
|
||||
})
|
||||
})
|
||||
@@ -1049,7 +1084,7 @@ func (c *conn) ruleExpired(r *tailcfg.SSHRule) bool {
|
||||
if r.RuleExpires == nil {
|
||||
return false
|
||||
}
|
||||
return r.RuleExpires.Before(c.now)
|
||||
return r.RuleExpires.Before(c.srv.now())
|
||||
}
|
||||
|
||||
func (c *conn) evalSSHPolicy(pol *tailcfg.SSHPolicy, pubKey gossh.PublicKey) (a *tailcfg.SSHAction, localUser string, ok bool) {
|
||||
|
||||
@@ -179,7 +179,6 @@ func TestMatchRule(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := &conn{
|
||||
now: time.Unix(200, 0),
|
||||
info: tt.ci,
|
||||
}
|
||||
got, gotUser, err := c.matchRule(tt.rule, nil)
|
||||
@@ -238,9 +237,10 @@ func TestSSH(t *testing.T) {
|
||||
node: &tailcfg.Node{},
|
||||
uprof: &tailcfg.UserProfile{},
|
||||
}
|
||||
sc.finalAction = &tailcfg.SSHAction{Accept: true}
|
||||
|
||||
sc.Handler = func(s ssh.Session) {
|
||||
sc.newSSHSession(s, &tailcfg.SSHAction{Accept: true}).run()
|
||||
sc.newSSHSession(s).run()
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp4", "127.0.0.1:0")
|
||||
|
||||
@@ -878,13 +878,20 @@ type MapRequest struct {
|
||||
// start-up before their first real endpoint update.
|
||||
ReadOnly bool `json:",omitempty"`
|
||||
|
||||
// OmitPeers is whether the client is okay with the Peers list
|
||||
// being omitted in the response. (For example, a client on
|
||||
// start up using ReadOnly to get the DERP map.)
|
||||
// OmitPeers is whether the client is okay with the Peers list being omitted
|
||||
// in the response.
|
||||
//
|
||||
// The behavior of OmitPeers being true varies based on Stream and ReadOnly:
|
||||
//
|
||||
// If OmitPeers is true, Stream is false, and ReadOnly is false,
|
||||
// then the server will let clients update their endpoints without
|
||||
// breaking existing long-polling (Stream == true) connections.
|
||||
// In this case, the server can omit the entire response; the client
|
||||
// only checks the HTTP response status code.
|
||||
//
|
||||
// If OmitPeers is true, Stream is false, but ReadOnly is true,
|
||||
// then all the response fields are included. (This is what the client does
|
||||
// when initially fetching the DERP map.)
|
||||
OmitPeers bool `json:",omitempty"`
|
||||
|
||||
// DebugFlags is a list of strings specifying debugging and
|
||||
@@ -1467,6 +1474,8 @@ const (
|
||||
|
||||
CapabilityFileSharing = "https://tailscale.com/cap/file-sharing"
|
||||
CapabilityAdmin = "https://tailscale.com/cap/is-admin"
|
||||
CapabilitySSH = "https://tailscale.com/cap/ssh" // feature enabled/available
|
||||
CapabilitySSHRuleIn = "https://tailscale.com/cap/ssh-rule-in" // some SSH rule reach this node
|
||||
|
||||
// Inter-node capabilities.
|
||||
|
||||
|
||||
@@ -98,10 +98,14 @@ func (s Sum) String() string {
|
||||
}
|
||||
|
||||
var (
|
||||
once sync.Once
|
||||
seed uint64
|
||||
seedOnce sync.Once
|
||||
seed uint64
|
||||
)
|
||||
|
||||
func initSeed() {
|
||||
seed = uint64(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
func (h *hasher) sum() (s Sum) {
|
||||
h.bw.Flush()
|
||||
// Sum into scratch & copy out, as hash.Hash is an interface
|
||||
@@ -121,9 +125,7 @@ func Hash(v any) (s Sum) {
|
||||
h := hasherPool.Get().(*hasher)
|
||||
defer hasherPool.Put(h)
|
||||
h.reset()
|
||||
once.Do(func() {
|
||||
seed = uint64(time.Now().UnixNano())
|
||||
})
|
||||
seedOnce.Do(initSeed)
|
||||
h.hashUint64(seed)
|
||||
h.hashValue(reflect.ValueOf(v))
|
||||
return h.sum()
|
||||
@@ -298,9 +300,9 @@ func (h *hasher) hashValue(v reflect.Value) {
|
||||
}
|
||||
|
||||
type mapHasher struct {
|
||||
h hasher
|
||||
val valueCache // re-usable values for map iteration
|
||||
iter reflect.MapIter // re-usable map iterator
|
||||
h hasher
|
||||
valKey, valElem valueCache // re-usable values for map iteration
|
||||
iter reflect.MapIter // re-usable map iterator
|
||||
}
|
||||
|
||||
var mapHasherPool = &sync.Pool{
|
||||
@@ -334,15 +336,19 @@ func (h *hasher) hashMap(v reflect.Value) {
|
||||
defer iter.Reset(reflect.Value{}) // avoid pinning v from mh.iter when we return
|
||||
|
||||
var sum Sum
|
||||
k := mh.val.get(v.Type().Key())
|
||||
e := mh.val.get(v.Type().Elem())
|
||||
if v.IsNil() {
|
||||
sum.sum[0] = 1 // something non-zero
|
||||
}
|
||||
|
||||
k := mh.valKey.get(v.Type().Key())
|
||||
e := mh.valElem.get(v.Type().Elem())
|
||||
mh.h.visitStack = h.visitStack // always use the parent's visit stack to avoid cycles
|
||||
for iter.Next() {
|
||||
k.SetIterKey(iter)
|
||||
e.SetIterValue(iter)
|
||||
mh.h.reset()
|
||||
mh.h.hashValue(k)
|
||||
mh.h.hashValue(v)
|
||||
mh.h.hashValue(e)
|
||||
sum.xor(mh.h.sum())
|
||||
}
|
||||
h.bw.Write(append(h.scratch[:0], sum.sum[:]...)) // append into scratch to avoid heap allocation
|
||||
|
||||
@@ -11,9 +11,11 @@ import (
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"testing"
|
||||
"testing/quick"
|
||||
|
||||
"go4.org/mem"
|
||||
"inet.af/netaddr"
|
||||
@@ -132,6 +134,49 @@ func TestDeepHash(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Tests that we actually hash map elements. Whoops.
|
||||
func TestIssue4868(t *testing.T) {
|
||||
m1 := map[int]string{1: "foo"}
|
||||
m2 := map[int]string{1: "bar"}
|
||||
if Hash(m1) == Hash(m2) {
|
||||
t.Error("bogus")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssue4871(t *testing.T) {
|
||||
m1 := map[string]string{"": "", "x": "foo"}
|
||||
m2 := map[string]string{}
|
||||
if h1, h2 := Hash(m1), Hash(m2); h1 == h2 {
|
||||
t.Errorf("bogus: h1=%x, h2=%x", h1, h2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNilVsEmptymap(t *testing.T) {
|
||||
m1 := map[string]string(nil)
|
||||
m2 := map[string]string{}
|
||||
if h1, h2 := Hash(m1), Hash(m2); h1 == h2 {
|
||||
t.Errorf("bogus: h1=%x, h2=%x", h1, h2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMapFraming(t *testing.T) {
|
||||
m1 := map[string]string{"foo": "", "fo": "o"}
|
||||
m2 := map[string]string{}
|
||||
if h1, h2 := Hash(m1), Hash(m2); h1 == h2 {
|
||||
t.Errorf("bogus: h1=%x, h2=%x", h1, h2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestQuick(t *testing.T) {
|
||||
initSeed()
|
||||
err := quick.Check(func(v, w map[string]string) bool {
|
||||
return (Hash(v) == Hash(w)) == reflect.DeepEqual(v, w)
|
||||
}, &quick.Config{MaxCount: 1000, Rand: rand.New(rand.NewSource(int64(seed)))})
|
||||
if err != nil {
|
||||
t.Fatalf("seed=%v, err=%v", seed, err)
|
||||
}
|
||||
}
|
||||
|
||||
func getVal() []any {
|
||||
return []any{
|
||||
&wgcfg.Config{
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
// CallbackRouter is an implementation of both Router and dns.OSConfigurator.
|
||||
// When either network or DNS settings are changed, SetBoth is called with both configs.
|
||||
// Mainly used as a shim for OSes that want to set both network and
|
||||
// DNS configuration simultaneously (iOS, android).
|
||||
// DNS configuration simultaneously (Mac, iOS, Android).
|
||||
type CallbackRouter struct {
|
||||
SetBoth func(rcfg *Config, dcfg *dns.OSConfig) error
|
||||
SplitDNS bool
|
||||
@@ -39,6 +39,9 @@ func (r *CallbackRouter) Up() error {
|
||||
func (r *CallbackRouter) Set(rcfg *Config) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.rcfg.Equal(rcfg) {
|
||||
return nil
|
||||
}
|
||||
r.rcfg = rcfg
|
||||
return r.SetBoth(r.rcfg, r.dcfg)
|
||||
}
|
||||
@@ -47,6 +50,9 @@ func (r *CallbackRouter) Set(rcfg *Config) error {
|
||||
func (r *CallbackRouter) SetDNS(dcfg dns.OSConfig) error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.dcfg != nil && r.dcfg.Equal(dcfg) {
|
||||
return nil
|
||||
}
|
||||
r.dcfg = &dcfg
|
||||
return r.SetBoth(r.rcfg, r.dcfg)
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"golang.zx2c4.com/wireguard/tun"
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -72,6 +74,16 @@ type Config struct {
|
||||
NetfilterMode preftype.NetfilterMode // how much to manage netfilter rules
|
||||
}
|
||||
|
||||
func (a *Config) Equal(b *Config) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
if (a == nil) != (b == nil) {
|
||||
return false
|
||||
}
|
||||
return reflect.DeepEqual(a, b)
|
||||
}
|
||||
|
||||
// shutdownConfig is a routing configuration that removes all router
|
||||
// state from the OS. It's the config used when callers pass in a nil
|
||||
// Config.
|
||||
|
||||
@@ -2,12 +2,15 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux || windows
|
||||
// +build linux windows
|
||||
|
||||
package router
|
||||
|
||||
import "inet.af/netaddr"
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"inet.af/netaddr"
|
||||
"tailscale.com/types/preftype"
|
||||
)
|
||||
|
||||
func mustCIDRs(ss ...string) []netaddr.IPPrefix {
|
||||
var ret []netaddr.IPPrefix
|
||||
@@ -16,3 +19,127 @@ func mustCIDRs(ss ...string) []netaddr.IPPrefix {
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func TestConfigEqual(t *testing.T) {
|
||||
testedFields := []string{
|
||||
"LocalAddrs", "Routes", "LocalRoutes", "SubnetRoutes",
|
||||
"SNATSubnetRoutes", "NetfilterMode",
|
||||
}
|
||||
configType := reflect.TypeOf(Config{})
|
||||
configFields := []string{}
|
||||
for i := 0; i < configType.NumField(); i++ {
|
||||
configFields = append(configFields, configType.Field(i).Name)
|
||||
}
|
||||
if !reflect.DeepEqual(configFields, testedFields) {
|
||||
t.Errorf("Config.Equal check might be out of sync\nfields: %q\nhandled: %q\n",
|
||||
configFields, testedFields)
|
||||
}
|
||||
|
||||
nets := func(strs ...string) (ns []netaddr.IPPrefix) {
|
||||
for _, s := range strs {
|
||||
n, err := netaddr.ParseIPPrefix(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ns = append(ns, n)
|
||||
}
|
||||
return ns
|
||||
}
|
||||
tests := []struct {
|
||||
a, b *Config
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
nil,
|
||||
nil,
|
||||
true,
|
||||
},
|
||||
{
|
||||
&Config{},
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
{
|
||||
nil,
|
||||
&Config{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Config{},
|
||||
&Config{},
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
&Config{LocalAddrs: nets("100.1.27.82/32")},
|
||||
&Config{LocalAddrs: nets("100.2.19.82/32")},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Config{LocalAddrs: nets("100.1.27.82/32")},
|
||||
&Config{LocalAddrs: nets("100.1.27.82/32")},
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
&Config{Routes: nets("100.1.27.0/24")},
|
||||
&Config{Routes: nets("100.2.19.0/24")},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Config{Routes: nets("100.2.19.0/24")},
|
||||
&Config{Routes: nets("100.2.19.0/24")},
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
&Config{LocalRoutes: nets("100.1.27.0/24")},
|
||||
&Config{LocalRoutes: nets("100.2.19.0/24")},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Config{LocalRoutes: nets("100.1.27.0/24")},
|
||||
&Config{LocalRoutes: nets("100.1.27.0/24")},
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
&Config{SubnetRoutes: nets("100.1.27.0/24")},
|
||||
&Config{SubnetRoutes: nets("100.2.19.0/24")},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Config{SubnetRoutes: nets("100.1.27.0/24")},
|
||||
&Config{SubnetRoutes: nets("100.1.27.0/24")},
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
&Config{SNATSubnetRoutes: false},
|
||||
&Config{SNATSubnetRoutes: true},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Config{SNATSubnetRoutes: false},
|
||||
&Config{SNATSubnetRoutes: false},
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
&Config{NetfilterMode: preftype.NetfilterOff},
|
||||
&Config{NetfilterMode: preftype.NetfilterNoDivert},
|
||||
false,
|
||||
},
|
||||
{
|
||||
&Config{NetfilterMode: preftype.NetfilterNoDivert},
|
||||
&Config{NetfilterMode: preftype.NetfilterNoDivert},
|
||||
true,
|
||||
},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
got := tt.a.Equal(tt.b)
|
||||
if got != tt.want {
|
||||
t.Errorf("%d. Equal = %v; want %v", i, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user