Compare commits

...

22 Commits

Author SHA1 Message Date
Denton Gentry
5a60f1ffe3 VERSION.txt: this is v1.26.2
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-07-05 12:13:16 -07:00
Brad Fitzpatrick
9685781e42 go.mod: bump wireguard-go to set CLOEXEC on tun/netlink fds
To get:
c31a7b1ab4

Diff:
95b48cdb39..c31a7b1ab4

Fixes #4992

Change-Id: I077586fefcde923a2721712dd54548f97d010f72
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit c93fd0d22b)
2022-07-05 09:56:51 -07:00
mattn
d6e7f41151 cmd/tailscale: make ssh command prefer Windows ssh.exe over PATH
use C:\Windows\System32\OpenSSH\ssh.exe (#4933)

Change-Id: Iad73b27b23b746de2c7e994572a54e7e1c370588
Signed-off-by: Yasuhiro Matsumoto <mattn.jp@gmail.com>
(cherry picked from commit 1d04e01d1e)
2022-07-05 09:56:51 -07:00
Mihai Parparita
2b4140ee46 wgengine/router: avoid unncessary routing configuration changes
The iOS and macOS networking extension API only exposes a single setter
for the entire routing and DNS configuration, and does not appear to
do any kind of diffing or deltas when applying changes. This results
in spurious "network changed" errors in Chrome, even when the
`OneCGNATRoute` flag from df9ce972c7 is
used (because we're setting the same configuration repeatedly).

Since we already keep track of the current routing and DNS configuration
in CallbackRouter, use that to detect if they're actually changing, and
only invoke the platform setter if it's actually necessary.

Updates #3102

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
(cherry picked from commit 06aa141632)
2022-07-05 09:56:51 -07:00
Brad Fitzpatrick
029508d8be ssh/tailssh: fix Tailscale SSH to Linux Arch machines
See https://github.com/tailscale/tailscale/issues/4924#issuecomment-1168201823

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.

Thanks to @eddiezane for debugging!

Updates #4924

Change-Id: I8d33e0afb2dfb99517bcea2f9d5d0c6247519b3c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 3b1f99ded1)
2022-07-05 09:56:51 -07:00
Denton Gentry
b39592f68a net/portmapper: make pcpCodeNotAuthorized log more descriptive
If PCP is present but disabled, turning it on might help
get direct connections.

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
(cherry picked from commit de4c635e54)
2022-07-05 09:56:51 -07:00
Jordan Whited
e7d5d90037 cmd/tailscale: move call to cli.CleanUpArgs() from main() into cli.Run() (#4954)
Not all distributions build from package main.

Signed-off-by: Jordan Whited <jordan@tailscale.com>
(cherry picked from commit bef6e2831a)
2022-07-05 09:56:51 -07:00
Maisem Ali
e4d881923f ssh/tailssh: fix logging typo
Signed-off-by: Maisem Ali <maisem@tailscale.com>
(cherry picked from commit 40503ef07a)
2022-07-05 09:56:51 -07:00
soypete
8a3319905b cmd/tailscale: make up respect explicitly empty --operator= value
Fixes #3808

Signed-off-by: soypete <miriah@tailscale.com>
(cherry picked from commit 412c4c55e2)
2022-07-05 09:56:51 -07:00
Maisem Ali
2a8de4a7ee ssh/tailssh: always use current time for policy evaluation
Whenever the SSH policy changes we revaluate all open connections to
make sure they still have access. This check was using the wrong
timestamp and would match against expired policies, however this really
isn't a problem today as we don't have policy that would be impacted by
this check. Fixing it for future use.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
(cherry picked from commit c434e47f2d)
2022-07-05 09:56:51 -07:00
Maisem Ali
70a1780320 ssh/tailssh: allow multiple sessions on the same conn
Fixes #4920
Fixes tailscale/corp#5633
Updates #4479

Signed-off-by: Maisem Ali <maisem@tailscale.com>
(cherry picked from commit a7d2024e35)
2022-07-05 09:56:51 -07:00
Adam Eijdenberg
5e34bd61c8 ssh/tailssh: limit setgroups to 16 on macOS
Fixes #4938

Signed-off-by: Adam Eijdenberg <adam@continusec.com>
(cherry picked from commit 9294a14a37)
2022-07-05 09:56:51 -07:00
Adam Eijdenberg
0f8e4b22b1 ssh/tailssh: fix /usr/bin/login args on macOS
Fixes #4931

Signed-off-by: Adam Eijdenberg <adam@continusec.com>
(cherry picked from commit 7f807fef6c)
2022-07-05 09:56:51 -07:00
Denton Gentry
5b81baa7d3 VERSION.txt: this is v1.26.1
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-06-17 20:58:34 -07:00
Brad Fitzpatrick
32f26a723b cmd/tailscale/cli, ipn/ipnlocal: give SSH tips when off/unconfigured
Updates #3802

Change-Id: I6b9a3175f68a6daa670f912561f2c2ececc07770
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 467eb2eca0)
2022-06-17 19:43:38 -07:00
Brad Fitzpatrick
c1795c6af9 tailcfg: define some Node.Capabilities about SSH, its config
Updates #3802

Change-Id: Icb4ccbc6bd1c6304013bfc553d04007844a5c0bf
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 99ed54926b)
2022-06-17 19:43:34 -07:00
Brad Fitzpatrick
4ae7eff7c2 util/deephash: fix map hashing when key & element have the same type
Regression from 09afb8e35b, in which the
same reflect.Value scratch value was being used as the map iterator
copy destination.

Also: make nil and empty maps hash differently, add test.

Fixes #4871

Co-authored-by: Josh Bleecher Snyder <josharian@gmail.com>
Change-Id: I67f42524bc81f694c1b7259d6682200125ea4a66
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 757ecf7e80)
2022-06-17 19:43:17 -07:00
Brad Fitzpatrick
d76cce4ee7 go.mod: bump go4.org/unsafe/assume-no-moving-gc for Go 1.19beta1
Otherwise we crash at startup with Go 1.19beta1.

Updates #4872

Change-Id: I371df4146735f7e066efd2edd48c1a305906c13d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 22c544bca7)
2022-06-17 19:42:32 -07:00
Brad Fitzpatrick
7968313a33 util/deephash: fix map hashing to actually hash elements
Fixes #4868

Change-Id: I574fd139cb7f7033dd93527344e6aa0e625477c7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 36ea837736)
2022-06-17 19:42:28 -07:00
Brad Fitzpatrick
4e6a465d8c tailcfg: clarify some of the MapRequest variants
Change-Id: Ia09bd69856e372c3a6b64cda7ddb34029b32a11b
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 4005134263)
2022-06-17 19:42:24 -07:00
Brad Fitzpatrick
89ac48af9d cmd/tailscale: add 'debug ts2021' Noise connectivity subcommand
Updates #3488

Change-Id: I9272e68f66c4cf36fb98dd1248a74d3817447690
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit d3643fa151)
2022-06-17 19:42:10 -07:00
Denton Gentry
9fc6551b4e VERSION.txt: this is v1.26.0
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-06-06 15:59:36 -07:00
26 changed files with 701 additions and 189 deletions

View File

@@ -1 +1 @@
1.25.0
1.26.2

View File

@@ -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"}
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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:

View File

@@ -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

View File

@@ -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")
}

View File

@@ -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:]...)

View File

@@ -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
}
}

View File

@@ -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+

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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()

View File

@@ -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)
}

View File

@@ -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 {

View 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])
}

View File

@@ -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)
}

View File

@@ -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) {

View File

@@ -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")

View File

@@ -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.

View File

@@ -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

View File

@@ -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{

View File

@@ -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)
}

View File

@@ -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.

View File

@@ -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)
}
}
}