ssh: replace tempfork with tailscale/gliderssh
Brings in a newer version of Gliderlabs SSH with added socket forwarding support. Fixes #12409 Fixes #5295 Signed-off-by: Kristoffer Dalby <kristoffer@tailscale.com>
This commit is contained in:
committed by
Kristoffer Dalby
parent
82fa218c4a
commit
dd3b613787
@@ -28,8 +28,8 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
gossh "golang.org/x/crypto/ssh"
|
gliderssh "github.com/tailscale/gliderssh"
|
||||||
"tailscale.com/tempfork/gliderlabs/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
// keyTypes are the SSH key types that we either try to read from the
|
// keyTypes are the SSH key types that we either try to read from the
|
||||||
@@ -60,23 +60,23 @@ func main() {
|
|||||||
log.Fatal("no host keys")
|
log.Fatal("no host keys")
|
||||||
}
|
}
|
||||||
|
|
||||||
srv := &ssh.Server{
|
srv := &gliderssh.Server{
|
||||||
Addr: *addr,
|
Addr: *addr,
|
||||||
Version: "Tailscale",
|
Version: "Tailscale",
|
||||||
Handler: handleSessionPostSSHAuth,
|
Handler: handleSessionPostSSHAuth,
|
||||||
ServerConfigCallback: func(ctx ssh.Context) *gossh.ServerConfig {
|
ServerConfigCallback: func(ctx gliderssh.Context) *ssh.ServerConfig {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
var spac gossh.ServerPreAuthConn
|
var spac ssh.ServerPreAuthConn
|
||||||
return &gossh.ServerConfig{
|
return &ssh.ServerConfig{
|
||||||
PreAuthConnCallback: func(conn gossh.ServerPreAuthConn) {
|
PreAuthConnCallback: func(conn ssh.ServerPreAuthConn) {
|
||||||
spac = conn
|
spac = conn
|
||||||
},
|
},
|
||||||
NoClientAuth: true, // required for the NoClientAuthCallback to run
|
NoClientAuth: true, // required for the NoClientAuthCallback to run
|
||||||
NoClientAuthCallback: func(cm gossh.ConnMetadata) (*gossh.Permissions, error) {
|
NoClientAuthCallback: func(cm ssh.ConnMetadata) (*ssh.Permissions, error) {
|
||||||
spac.SendAuthBanner(fmt.Sprintf("# Banner: doing none auth at %v\r\n", time.Since(start)))
|
spac.SendAuthBanner(fmt.Sprintf("# Banner: doing none auth at %v\r\n", time.Since(start)))
|
||||||
|
|
||||||
if cm.User() == "denyme" {
|
if cm.User() == "denyme" {
|
||||||
return nil, &gossh.BannerError{
|
return nil, &ssh.BannerError{
|
||||||
Err: errors.New("denying access"),
|
Err: errors.New("denying access"),
|
||||||
Message: "denyme is not allowed to access this machine\n",
|
Message: "denyme is not allowed to access this machine\n",
|
||||||
}
|
}
|
||||||
@@ -96,7 +96,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
},
|
},
|
||||||
BannerCallback: func(cm gossh.ConnMetadata) string {
|
BannerCallback: func(cm ssh.ConnMetadata) string {
|
||||||
log.Printf("Got connection from user %q, %q from %v", cm.User(), cm.ClientVersion(), cm.RemoteAddr())
|
log.Printf("Got connection from user %q, %q from %v", cm.User(), cm.ClientVersion(), cm.RemoteAddr())
|
||||||
return fmt.Sprintf("# Banner for user %q, %q\n", cm.User(), cm.ClientVersion())
|
return fmt.Sprintf("# Banner for user %q, %q\n", cm.User(), cm.ClientVersion())
|
||||||
},
|
},
|
||||||
@@ -115,7 +115,7 @@ func main() {
|
|||||||
log.Printf("done")
|
log.Printf("done")
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleSessionPostSSHAuth(s ssh.Session) {
|
func handleSessionPostSSHAuth(s gliderssh.Session) {
|
||||||
log.Printf("Started session from user %q", s.User())
|
log.Printf("Started session from user %q", s.User())
|
||||||
fmt.Fprintf(s, "Hello user %q, it worked.\n", s.User())
|
fmt.Fprintf(s, "Hello user %q, it worked.\n", s.User())
|
||||||
|
|
||||||
@@ -143,13 +143,13 @@ func handleSessionPostSSHAuth(s ssh.Session) {
|
|||||||
s.Exit(0)
|
s.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getHostKeys(dir string) (ret []ssh.Signer, err error) {
|
func getHostKeys(dir string) (ret []gliderssh.Signer, err error) {
|
||||||
for _, typ := range keyTypes {
|
for _, typ := range keyTypes {
|
||||||
hostKey, err := hostKeyFileOrCreate(dir, typ)
|
hostKey, err := hostKeyFileOrCreate(dir, typ)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
signer, err := gossh.ParsePrivateKey(hostKey)
|
signer, err := ssh.ParsePrivateKey(hostKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
|
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
|
||||||
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
|
||||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||||
LD github.com/anmitsu/go-shlex from tailscale.com/tempfork/gliderlabs/ssh
|
LD github.com/anmitsu/go-shlex from github.com/tailscale/gliderssh
|
||||||
L github.com/aws/aws-sdk-go-v2/aws from github.com/aws/aws-sdk-go-v2/aws/defaults+
|
L github.com/aws/aws-sdk-go-v2/aws from github.com/aws/aws-sdk-go-v2/aws/defaults+
|
||||||
L github.com/aws/aws-sdk-go-v2/aws/arn from tailscale.com/ipn/store/awsstore
|
L github.com/aws/aws-sdk-go-v2/aws/arn from tailscale.com/ipn/store/awsstore
|
||||||
L github.com/aws/aws-sdk-go-v2/aws/defaults from github.com/aws/aws-sdk-go-v2/service/ssm+
|
L github.com/aws/aws-sdk-go-v2/aws/defaults from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||||
@@ -176,6 +176,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
D github.com/prometheus-community/pro-bing from tailscale.com/wgengine/netstack
|
D github.com/prometheus-community/pro-bing from tailscale.com/wgengine/netstack
|
||||||
L 💣 github.com/safchain/ethtool from tailscale.com/net/netkernelconf+
|
L 💣 github.com/safchain/ethtool from tailscale.com/net/netkernelconf+
|
||||||
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
|
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
|
||||||
|
LD github.com/tailscale/gliderssh from tailscale.com/ssh/tailssh
|
||||||
W 💣 github.com/tailscale/go-winio from tailscale.com/safesocket
|
W 💣 github.com/tailscale/go-winio from tailscale.com/safesocket
|
||||||
W 💣 github.com/tailscale/go-winio/internal/fs from github.com/tailscale/go-winio
|
W 💣 github.com/tailscale/go-winio/internal/fs from github.com/tailscale/go-winio
|
||||||
W 💣 github.com/tailscale/go-winio/internal/socket from github.com/tailscale/go-winio
|
W 💣 github.com/tailscale/go-winio/internal/socket from github.com/tailscale/go-winio
|
||||||
@@ -393,7 +394,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
|||||||
tailscale.com/syncs from tailscale.com/cmd/tailscaled+
|
tailscale.com/syncs from tailscale.com/cmd/tailscaled+
|
||||||
tailscale.com/tailcfg from tailscale.com/client/local+
|
tailscale.com/tailcfg from tailscale.com/client/local+
|
||||||
tailscale.com/tempfork/acme from tailscale.com/ipn/ipnlocal
|
tailscale.com/tempfork/acme from tailscale.com/ipn/ipnlocal
|
||||||
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
|
|
||||||
tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock
|
tailscale.com/tempfork/heap from tailscale.com/wgengine/magicsock
|
||||||
tailscale.com/tempfork/httprec from tailscale.com/feature/c2n
|
tailscale.com/tempfork/httprec from tailscale.com/feature/c2n
|
||||||
tailscale.com/tka from tailscale.com/client/local+
|
tailscale.com/tka from tailscale.com/client/local+
|
||||||
|
|||||||
@@ -163,4 +163,4 @@
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
# nix-direnv cache busting line: sha256-VsVMvTEblVx/HNbuCVxC9UgKpriRwixswUSKVGLMf3Q=
|
# nix-direnv cache busting line: sha256-PLt+IPqemF3agESg6jV8AzbiOpgL45mJ/AymcNUo7VU=
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ require (
|
|||||||
github.com/akutz/memconn v0.1.0
|
github.com/akutz/memconn v0.1.0
|
||||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa
|
||||||
github.com/andybalholm/brotli v1.1.0
|
github.com/andybalholm/brotli v1.1.0
|
||||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
|
||||||
github.com/atotto/clipboard v0.1.4
|
github.com/atotto/clipboard v0.1.4
|
||||||
github.com/aws/aws-sdk-go-v2 v1.41.0
|
github.com/aws/aws-sdk-go-v2 v1.41.0
|
||||||
github.com/aws/aws-sdk-go-v2/config v1.29.5
|
github.com/aws/aws-sdk-go-v2/config v1.29.5
|
||||||
@@ -90,6 +89,7 @@ require (
|
|||||||
github.com/studio-b12/gowebdav v0.9.0
|
github.com/studio-b12/gowebdav v0.9.0
|
||||||
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e
|
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e
|
||||||
github.com/tailscale/depaware v0.0.0-20251001183927-9c2ad255ef3f
|
github.com/tailscale/depaware v0.0.0-20251001183927-9c2ad255ef3f
|
||||||
|
github.com/tailscale/gliderssh v0.3.4-0.20260330083525-c1389c70ff89
|
||||||
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
|
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
|
||||||
github.com/tailscale/gokrazy-kernel v0.0.0-20240728225134-3d23beabda2e
|
github.com/tailscale/gokrazy-kernel v0.0.0-20240728225134-3d23beabda2e
|
||||||
github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869
|
github.com/tailscale/golang-x-crypto v0.0.0-20250404221719-a5573b049869
|
||||||
@@ -151,6 +151,7 @@ require (
|
|||||||
github.com/OpenPeeDeeP/depguard/v2 v2.2.0 // indirect
|
github.com/OpenPeeDeeP/depguard/v2 v2.2.0 // indirect
|
||||||
github.com/alecthomas/go-check-sumtype v0.1.4 // indirect
|
github.com/alecthomas/go-check-sumtype v0.1.4 // indirect
|
||||||
github.com/alexkohler/nakedret/v2 v2.0.4 // indirect
|
github.com/alexkohler/nakedret/v2 v2.0.4 // indirect
|
||||||
|
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
|
||||||
github.com/armon/go-metrics v0.4.1 // indirect
|
github.com/armon/go-metrics v0.4.1 // indirect
|
||||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||||
github.com/beevik/ntp v0.3.0 // indirect
|
github.com/beevik/ntp v0.3.0 // indirect
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
sha256-VsVMvTEblVx/HNbuCVxC9UgKpriRwixswUSKVGLMf3Q=
|
sha256-PLt+IPqemF3agESg6jV8AzbiOpgL45mJ/AymcNUo7VU=
|
||||||
|
|||||||
@@ -1130,6 +1130,8 @@ github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP
|
|||||||
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
|
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
|
||||||
github.com/tailscale/depaware v0.0.0-20251001183927-9c2ad255ef3f h1:PDPGJtm9PFBLNudHGwkfUGp/FWvP+kXXJ0D1pB35F40=
|
github.com/tailscale/depaware v0.0.0-20251001183927-9c2ad255ef3f h1:PDPGJtm9PFBLNudHGwkfUGp/FWvP+kXXJ0D1pB35F40=
|
||||||
github.com/tailscale/depaware v0.0.0-20251001183927-9c2ad255ef3f/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
|
github.com/tailscale/depaware v0.0.0-20251001183927-9c2ad255ef3f/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
|
||||||
|
github.com/tailscale/gliderssh v0.3.4-0.20260330083525-c1389c70ff89 h1:glgVc1ZYMjwN1Q/ITWeuSQyl029uayagaR2sjsifehc=
|
||||||
|
github.com/tailscale/gliderssh v0.3.4-0.20260330083525-c1389c70ff89/go.mod h1:wn16Km1EZOX4UEAyaZa3dBwfFGOJ7neck40NcwosJUw=
|
||||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
|
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
|
||||||
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
|
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
|
||||||
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41 h1:/V2rCMMWcsjYaYO2MeovLw+ClP63OtXgCF2Y1eb8+Ns=
|
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41 h1:/V2rCMMWcsjYaYO2MeovLw+ClP63OtXgCF2Y1eb8+Ns=
|
||||||
|
|||||||
@@ -103,5 +103,5 @@ Some packages may only be included on certain architectures or operating systems
|
|||||||
- [k8s.io/client-go/util/homedir](https://pkg.go.dev/k8s.io/client-go/util/homedir) ([Apache-2.0](https://github.com/kubernetes/client-go/blob/v0.34.0/LICENSE))
|
- [k8s.io/client-go/util/homedir](https://pkg.go.dev/k8s.io/client-go/util/homedir) ([Apache-2.0](https://github.com/kubernetes/client-go/blob/v0.34.0/LICENSE))
|
||||||
- [sigs.k8s.io/yaml](https://pkg.go.dev/sigs.k8s.io/yaml) ([Apache-2.0](https://github.com/kubernetes-sigs/yaml/blob/v1.6.0/LICENSE))
|
- [sigs.k8s.io/yaml](https://pkg.go.dev/sigs.k8s.io/yaml) ([Apache-2.0](https://github.com/kubernetes-sigs/yaml/blob/v1.6.0/LICENSE))
|
||||||
- [tailscale.com](https://pkg.go.dev/tailscale.com) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/LICENSE))
|
- [tailscale.com](https://pkg.go.dev/tailscale.com) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/LICENSE))
|
||||||
- [tailscale.com/tempfork/gliderlabs/ssh](https://pkg.go.dev/tailscale.com/tempfork/gliderlabs/ssh) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/tempfork/gliderlabs/ssh/LICENSE))
|
- [github.com/tailscale/gliderssh](https://pkg.go.dev/github.com/tailscale/gliderssh) ([BSD-3-Clause](https://github.com/tailscale/gliderssh/blob/HEAD/LICENSE))
|
||||||
- [tailscale.com/tempfork/spf13/cobra](https://pkg.go.dev/tailscale.com/tempfork/spf13/cobra) ([Apache-2.0](https://github.com/tailscale/tailscale/blob/HEAD/tempfork/spf13/cobra/LICENSE.txt))
|
- [tailscale.com/tempfork/spf13/cobra](https://pkg.go.dev/tailscale.com/tempfork/spf13/cobra) ([Apache-2.0](https://github.com/tailscale/tailscale/blob/HEAD/tempfork/spf13/cobra/LICENSE.txt))
|
||||||
|
|||||||
@@ -16,4 +16,4 @@
|
|||||||
) {
|
) {
|
||||||
src = ./.;
|
src = ./.;
|
||||||
}).shellNix
|
}).shellNix
|
||||||
# nix-direnv cache busting line: sha256-VsVMvTEblVx/HNbuCVxC9UgKpriRwixswUSKVGLMf3Q=
|
# nix-direnv cache busting line: sha256-PLt+IPqemF3agESg6jV8AzbiOpgL45mJ/AymcNUo7VU=
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
gossh "golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/util/mak"
|
"tailscale.com/util/mak"
|
||||||
)
|
)
|
||||||
@@ -33,8 +33,8 @@ var keyTypes = []string{"rsa", "ecdsa", "ed25519"}
|
|||||||
|
|
||||||
// getHostKeys returns the SSH host keys, using system keys when running as root
|
// getHostKeys returns the SSH host keys, using system keys when running as root
|
||||||
// and generating Tailscale-specific keys as needed.
|
// and generating Tailscale-specific keys as needed.
|
||||||
func getHostKeys(varRoot string, logf logger.Logf) ([]gossh.Signer, error) {
|
func getHostKeys(varRoot string, logf logger.Logf) ([]ssh.Signer, error) {
|
||||||
var existing map[string]gossh.Signer
|
var existing map[string]ssh.Signer
|
||||||
if os.Geteuid() == 0 {
|
if os.Geteuid() == 0 {
|
||||||
existing = getSystemHostKeys(logf)
|
existing = getSystemHostKeys(logf)
|
||||||
}
|
}
|
||||||
@@ -49,14 +49,14 @@ func getHostKeyPublicStrings(varRoot string, logf logger.Logf) ([]string, error)
|
|||||||
}
|
}
|
||||||
var keyStrings []string
|
var keyStrings []string
|
||||||
for _, signer := range signers {
|
for _, signer := range signers {
|
||||||
keyStrings = append(keyStrings, strings.TrimSpace(string(gossh.MarshalAuthorizedKey(signer.PublicKey()))))
|
keyStrings = append(keyStrings, strings.TrimSpace(string(ssh.MarshalAuthorizedKey(signer.PublicKey()))))
|
||||||
}
|
}
|
||||||
return keyStrings, nil
|
return keyStrings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getTailscaleHostKeys returns the three (rsa, ecdsa, ed25519) SSH host
|
// getTailscaleHostKeys returns the three (rsa, ecdsa, ed25519) SSH host
|
||||||
// keys, reusing the provided ones in existing if present in the map.
|
// keys, reusing the provided ones in existing if present in the map.
|
||||||
func getTailscaleHostKeys(varRoot string, existing map[string]gossh.Signer) (keys []gossh.Signer, err error) {
|
func getTailscaleHostKeys(varRoot string, existing map[string]ssh.Signer) (keys []ssh.Signer, err error) {
|
||||||
var keyDir string // lazily initialized $TAILSCALE_VAR/ssh dir.
|
var keyDir string // lazily initialized $TAILSCALE_VAR/ssh dir.
|
||||||
for _, typ := range keyTypes {
|
for _, typ := range keyTypes {
|
||||||
if s, ok := existing[typ]; ok {
|
if s, ok := existing[typ]; ok {
|
||||||
@@ -76,7 +76,7 @@ func getTailscaleHostKeys(varRoot string, existing map[string]gossh.Signer) (key
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error creating SSH host key type %q in %q: %w", typ, keyDir, err)
|
return nil, fmt.Errorf("error creating SSH host key type %q in %q: %w", typ, keyDir, err)
|
||||||
}
|
}
|
||||||
signer, err := gossh.ParsePrivateKey(hostKey)
|
signer, err := ssh.ParsePrivateKey(hostKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error parsing SSH host key type %q from %q: %w", typ, keyDir, err)
|
return nil, fmt.Errorf("error parsing SSH host key type %q from %q: %w", typ, keyDir, err)
|
||||||
}
|
}
|
||||||
@@ -137,14 +137,14 @@ func hostKeyFileOrCreate(keyDir, typ string) ([]byte, error) {
|
|||||||
return pemGen, err
|
return pemGen, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func getSystemHostKeys(logf logger.Logf) (ret map[string]gossh.Signer) {
|
func getSystemHostKeys(logf logger.Logf) (ret map[string]ssh.Signer) {
|
||||||
for _, typ := range keyTypes {
|
for _, typ := range keyTypes {
|
||||||
filename := "/etc/ssh/ssh_host_" + typ + "_key"
|
filename := "/etc/ssh/ssh_host_" + typ + "_key"
|
||||||
hostKey, err := os.ReadFile(filename)
|
hostKey, err := os.ReadFile(filename)
|
||||||
if err != nil || len(bytes.TrimSpace(hostKey)) == 0 {
|
if err != nil || len(bytes.TrimSpace(hostKey)) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
signer, err := gossh.ParsePrivateKey(hostKey)
|
signer, err := ssh.ParsePrivateKey(hostKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logf("warning: error reading host key %s: %v (generating one instead)", filename, err)
|
logf("warning: error reading host key %s: %v (generating one instead)", filename, err)
|
||||||
continue
|
continue
|
||||||
|
|||||||
+61
-61
@@ -35,13 +35,13 @@ import (
|
|||||||
|
|
||||||
"github.com/creack/pty"
|
"github.com/creack/pty"
|
||||||
"github.com/pkg/sftp"
|
"github.com/pkg/sftp"
|
||||||
|
gliderssh "github.com/tailscale/gliderssh"
|
||||||
"github.com/u-root/u-root/pkg/termios"
|
"github.com/u-root/u-root/pkg/termios"
|
||||||
gossh "golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
"tailscale.com/cmd/tailscaled/childproc"
|
"tailscale.com/cmd/tailscaled/childproc"
|
||||||
"tailscale.com/hostinfo"
|
"tailscale.com/hostinfo"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/tempfork/gliderlabs/ssh"
|
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/version/distro"
|
"tailscale.com/version/distro"
|
||||||
)
|
)
|
||||||
@@ -897,7 +897,7 @@ func (ss *sshSession) launchProcess() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resizeWindow(fd int, winCh <-chan ssh.Window) {
|
func resizeWindow(fd int, winCh <-chan gliderssh.Window) {
|
||||||
for win := range winCh {
|
for win := range winCh {
|
||||||
unix.IoctlSetWinsize(fd, syscall.TIOCSWINSZ, &unix.Winsize{
|
unix.IoctlSetWinsize(fd, syscall.TIOCSWINSZ, &unix.Winsize{
|
||||||
Row: uint16(win.Height),
|
Row: uint16(win.Height),
|
||||||
@@ -912,62 +912,62 @@ func resizeWindow(fd int, winCh <-chan ssh.Window) {
|
|||||||
// to mnemonic names expected by the termios package.
|
// to mnemonic names expected by the termios package.
|
||||||
// These are meant to be platform independent.
|
// These are meant to be platform independent.
|
||||||
var opcodeShortName = map[uint8]string{
|
var opcodeShortName = map[uint8]string{
|
||||||
gossh.VINTR: "intr",
|
ssh.VINTR: "intr",
|
||||||
gossh.VQUIT: "quit",
|
ssh.VQUIT: "quit",
|
||||||
gossh.VERASE: "erase",
|
ssh.VERASE: "erase",
|
||||||
gossh.VKILL: "kill",
|
ssh.VKILL: "kill",
|
||||||
gossh.VEOF: "eof",
|
ssh.VEOF: "eof",
|
||||||
gossh.VEOL: "eol",
|
ssh.VEOL: "eol",
|
||||||
gossh.VEOL2: "eol2",
|
ssh.VEOL2: "eol2",
|
||||||
gossh.VSTART: "start",
|
ssh.VSTART: "start",
|
||||||
gossh.VSTOP: "stop",
|
ssh.VSTOP: "stop",
|
||||||
gossh.VSUSP: "susp",
|
ssh.VSUSP: "susp",
|
||||||
gossh.VDSUSP: "dsusp",
|
ssh.VDSUSP: "dsusp",
|
||||||
gossh.VREPRINT: "rprnt",
|
ssh.VREPRINT: "rprnt",
|
||||||
gossh.VWERASE: "werase",
|
ssh.VWERASE: "werase",
|
||||||
gossh.VLNEXT: "lnext",
|
ssh.VLNEXT: "lnext",
|
||||||
gossh.VFLUSH: "flush",
|
ssh.VFLUSH: "flush",
|
||||||
gossh.VSWTCH: "swtch",
|
ssh.VSWTCH: "swtch",
|
||||||
gossh.VSTATUS: "status",
|
ssh.VSTATUS: "status",
|
||||||
gossh.VDISCARD: "discard",
|
ssh.VDISCARD: "discard",
|
||||||
gossh.IGNPAR: "ignpar",
|
ssh.IGNPAR: "ignpar",
|
||||||
gossh.PARMRK: "parmrk",
|
ssh.PARMRK: "parmrk",
|
||||||
gossh.INPCK: "inpck",
|
ssh.INPCK: "inpck",
|
||||||
gossh.ISTRIP: "istrip",
|
ssh.ISTRIP: "istrip",
|
||||||
gossh.INLCR: "inlcr",
|
ssh.INLCR: "inlcr",
|
||||||
gossh.IGNCR: "igncr",
|
ssh.IGNCR: "igncr",
|
||||||
gossh.ICRNL: "icrnl",
|
ssh.ICRNL: "icrnl",
|
||||||
gossh.IUCLC: "iuclc",
|
ssh.IUCLC: "iuclc",
|
||||||
gossh.IXON: "ixon",
|
ssh.IXON: "ixon",
|
||||||
gossh.IXANY: "ixany",
|
ssh.IXANY: "ixany",
|
||||||
gossh.IXOFF: "ixoff",
|
ssh.IXOFF: "ixoff",
|
||||||
gossh.IMAXBEL: "imaxbel",
|
ssh.IMAXBEL: "imaxbel",
|
||||||
gossh.IUTF8: "iutf8",
|
ssh.IUTF8: "iutf8",
|
||||||
gossh.ISIG: "isig",
|
ssh.ISIG: "isig",
|
||||||
gossh.ICANON: "icanon",
|
ssh.ICANON: "icanon",
|
||||||
gossh.XCASE: "xcase",
|
ssh.XCASE: "xcase",
|
||||||
gossh.ECHO: "echo",
|
ssh.ECHO: "echo",
|
||||||
gossh.ECHOE: "echoe",
|
ssh.ECHOE: "echoe",
|
||||||
gossh.ECHOK: "echok",
|
ssh.ECHOK: "echok",
|
||||||
gossh.ECHONL: "echonl",
|
ssh.ECHONL: "echonl",
|
||||||
gossh.NOFLSH: "noflsh",
|
ssh.NOFLSH: "noflsh",
|
||||||
gossh.TOSTOP: "tostop",
|
ssh.TOSTOP: "tostop",
|
||||||
gossh.IEXTEN: "iexten",
|
ssh.IEXTEN: "iexten",
|
||||||
gossh.ECHOCTL: "echoctl",
|
ssh.ECHOCTL: "echoctl",
|
||||||
gossh.ECHOKE: "echoke",
|
ssh.ECHOKE: "echoke",
|
||||||
gossh.PENDIN: "pendin",
|
ssh.PENDIN: "pendin",
|
||||||
gossh.OPOST: "opost",
|
ssh.OPOST: "opost",
|
||||||
gossh.OLCUC: "olcuc",
|
ssh.OLCUC: "olcuc",
|
||||||
gossh.ONLCR: "onlcr",
|
ssh.ONLCR: "onlcr",
|
||||||
gossh.OCRNL: "ocrnl",
|
ssh.OCRNL: "ocrnl",
|
||||||
gossh.ONOCR: "onocr",
|
ssh.ONOCR: "onocr",
|
||||||
gossh.ONLRET: "onlret",
|
ssh.ONLRET: "onlret",
|
||||||
gossh.CS7: "cs7",
|
ssh.CS7: "cs7",
|
||||||
gossh.CS8: "cs8",
|
ssh.CS8: "cs8",
|
||||||
gossh.PARENB: "parenb",
|
ssh.PARENB: "parenb",
|
||||||
gossh.PARODD: "parodd",
|
ssh.PARODD: "parodd",
|
||||||
gossh.TTY_OP_ISPEED: "tty_op_ispeed",
|
ssh.TTY_OP_ISPEED: "tty_op_ispeed",
|
||||||
gossh.TTY_OP_OSPEED: "tty_op_ospeed",
|
ssh.TTY_OP_OSPEED: "tty_op_ospeed",
|
||||||
}
|
}
|
||||||
|
|
||||||
// startWithPTY starts cmd with a pseudo-terminal attached to Stdin, Stdout and Stderr.
|
// startWithPTY starts cmd with a pseudo-terminal attached to Stdin, Stdout and Stderr.
|
||||||
@@ -1011,11 +1011,11 @@ func (ss *sshSession) startWithPTY() (ptyFile, tty *os.File, err error) {
|
|||||||
tios.Col = int(ptyReq.Window.Width)
|
tios.Col = int(ptyReq.Window.Width)
|
||||||
|
|
||||||
for c, v := range ptyReq.Modes {
|
for c, v := range ptyReq.Modes {
|
||||||
if c == gossh.TTY_OP_ISPEED {
|
if c == ssh.TTY_OP_ISPEED {
|
||||||
tios.Ispeed = int(v)
|
tios.Ispeed = int(v)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if c == gossh.TTY_OP_OSPEED {
|
if c == ssh.TTY_OP_OSPEED {
|
||||||
tios.Ospeed = int(v)
|
tios.Ospeed = int(v)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
+72
-70
@@ -30,7 +30,8 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
gossh "golang.org/x/crypto/ssh"
|
gliderssh "github.com/tailscale/gliderssh"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
"tailscale.com/envknob"
|
"tailscale.com/envknob"
|
||||||
"tailscale.com/feature"
|
"tailscale.com/feature"
|
||||||
"tailscale.com/ipn/ipnlocal"
|
"tailscale.com/ipn/ipnlocal"
|
||||||
@@ -38,7 +39,6 @@ import (
|
|||||||
"tailscale.com/net/tsdial"
|
"tailscale.com/net/tsdial"
|
||||||
"tailscale.com/sessionrecording"
|
"tailscale.com/sessionrecording"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/tempfork/gliderlabs/ssh"
|
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
"tailscale.com/types/logger"
|
"tailscale.com/types/logger"
|
||||||
"tailscale.com/types/netmap"
|
"tailscale.com/types/netmap"
|
||||||
@@ -54,10 +54,10 @@ var (
|
|||||||
sshDisableForwarding = envknob.RegisterBool("TS_SSH_DISABLE_FORWARDING")
|
sshDisableForwarding = envknob.RegisterBool("TS_SSH_DISABLE_FORWARDING")
|
||||||
sshDisablePTY = envknob.RegisterBool("TS_SSH_DISABLE_PTY")
|
sshDisablePTY = envknob.RegisterBool("TS_SSH_DISABLE_PTY")
|
||||||
|
|
||||||
// errTerminal is an empty gossh.PartialSuccessError (with no 'Next'
|
// errTerminal is an empty ssh.PartialSuccessError (with no 'Next'
|
||||||
// authentication methods that may proceed), which results in the SSH
|
// authentication methods that may proceed), which results in the SSH
|
||||||
// server immediately disconnecting the client.
|
// server immediately disconnecting the client.
|
||||||
errTerminal = &gossh.PartialSuccessError{}
|
errTerminal = &ssh.PartialSuccessError{}
|
||||||
|
|
||||||
// hookSSHLoginSuccess is called after successful SSH authentication.
|
// hookSSHLoginSuccess is called after successful SSH authentication.
|
||||||
// It is set by platform-specific code (e.g., auditd_linux.go).
|
// It is set by platform-specific code (e.g., auditd_linux.go).
|
||||||
@@ -204,7 +204,7 @@ func (srv *server) OnPolicyChange() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// conn represents a single SSH connection and its associated
|
// conn represents a single SSH connection and its associated
|
||||||
// ssh.Server.
|
// gliderssh.Server.
|
||||||
//
|
//
|
||||||
// During the lifecycle of a connection, the following are called in order:
|
// During the lifecycle of a connection, the following are called in order:
|
||||||
// Setup and discover server info
|
// Setup and discover server info
|
||||||
@@ -220,9 +220,9 @@ func (srv *server) OnPolicyChange() {
|
|||||||
// channels concurrently. At which point any of the following can be called
|
// channels concurrently. At which point any of the following can be called
|
||||||
// in any order.
|
// in any order.
|
||||||
// - c.handleSessionPostSSHAuth
|
// - c.handleSessionPostSSHAuth
|
||||||
// - c.mayForwardLocalPortTo followed by ssh.DirectTCPIPHandler
|
// - c.mayForwardLocalPortTo followed by gliderssh.DirectTCPIPHandler
|
||||||
type conn struct {
|
type conn struct {
|
||||||
*ssh.Server
|
*gliderssh.Server
|
||||||
srv *server
|
srv *server
|
||||||
|
|
||||||
insecureSkipTailscaleAuth bool // used by tests.
|
insecureSkipTailscaleAuth bool // used by tests.
|
||||||
@@ -234,9 +234,9 @@ type conn struct {
|
|||||||
idH string
|
idH string
|
||||||
connID string // ID that's shared with control
|
connID string // ID that's shared with control
|
||||||
|
|
||||||
// spac is a [gossh.ServerPreAuthConn] used for sending auth banners.
|
// spac is a [ssh.ServerPreAuthConn] used for sending auth banners.
|
||||||
// Banners cannot be sent after auth completes.
|
// Banners cannot be sent after auth completes.
|
||||||
spac gossh.ServerPreAuthConn
|
spac ssh.ServerPreAuthConn
|
||||||
|
|
||||||
// The following fields are set during clientAuth and are used for policy
|
// The following fields are set during clientAuth and are used for policy
|
||||||
// evaluation and session management. They are immutable after clientAuth
|
// evaluation and session management. They are immutable after clientAuth
|
||||||
@@ -280,7 +280,7 @@ func (c *conn) vlogf(format string, args ...any) {
|
|||||||
|
|
||||||
// errDenied is returned by auth callbacks when a connection is denied by the
|
// errDenied is returned by auth callbacks when a connection is denied by the
|
||||||
// policy. It writes the message to an auth banner and then returns an empty
|
// policy. It writes the message to an auth banner and then returns an empty
|
||||||
// gossh.PartialSuccessError in order to stop processing authentication
|
// ssh.PartialSuccessError in order to stop processing authentication
|
||||||
// attempts and immediately disconnect the client.
|
// attempts and immediately disconnect the client.
|
||||||
func (c *conn) errDenied(message string) error {
|
func (c *conn) errDenied(message string) error {
|
||||||
if message == "" {
|
if message == "" {
|
||||||
@@ -293,7 +293,7 @@ func (c *conn) errDenied(message string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// errBanner writes the given message to an auth banner and then returns an
|
// errBanner writes the given message to an auth banner and then returns an
|
||||||
// empty gossh.PartialSuccessError in order to stop processing authentication
|
// empty ssh.PartialSuccessError in order to stop processing authentication
|
||||||
// attempts and immediately disconnect the client. The contents of err is not
|
// attempts and immediately disconnect the client. The contents of err is not
|
||||||
// leaked in the auth banner, but it is logged to the server's log.
|
// leaked in the auth banner, but it is logged to the server's log.
|
||||||
func (c *conn) errBanner(message string, err error) error {
|
func (c *conn) errBanner(message string, err error) error {
|
||||||
@@ -308,7 +308,7 @@ func (c *conn) errBanner(message string, err error) error {
|
|||||||
|
|
||||||
// errUnexpected is returned by auth callbacks that encounter an unexpected
|
// errUnexpected is returned by auth callbacks that encounter an unexpected
|
||||||
// error, such as being unable to send an auth banner. It sends an empty
|
// error, such as being unable to send an auth banner. It sends an empty
|
||||||
// gossh.PartialSuccessError to tell gossh.Server to stop processing
|
// ssh.PartialSuccessError to tell ssh.Server to stop processing
|
||||||
// authentication attempts and instead disconnect immediately.
|
// authentication attempts and instead disconnect immediately.
|
||||||
func (c *conn) errUnexpected(err error) error {
|
func (c *conn) errUnexpected(err error) error {
|
||||||
c.logf("terminal error: %s", err)
|
c.logf("terminal error: %s", err)
|
||||||
@@ -319,11 +319,11 @@ func (c *conn) errUnexpected(err error) error {
|
|||||||
//
|
//
|
||||||
// If policy evaluation fails, it returns an error.
|
// If policy evaluation fails, it returns an error.
|
||||||
// If access is denied, it returns an error. This must always be an empty
|
// If access is denied, it returns an error. This must always be an empty
|
||||||
// gossh.PartialSuccessError to prevent further authentication methods from
|
// ssh.PartialSuccessError to prevent further authentication methods from
|
||||||
// being tried.
|
// being tried.
|
||||||
func (c *conn) clientAuth(cm gossh.ConnMetadata) (perms *gossh.Permissions, retErr error) {
|
func (c *conn) clientAuth(cm ssh.ConnMetadata) (perms *ssh.Permissions, retErr error) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if pse, ok := retErr.(*gossh.PartialSuccessError); ok {
|
if pse, ok := retErr.(*ssh.PartialSuccessError); ok {
|
||||||
if pse.Next.GSSAPIWithMICConfig != nil ||
|
if pse.Next.GSSAPIWithMICConfig != nil ||
|
||||||
pse.Next.KeyboardInteractiveCallback != nil ||
|
pse.Next.KeyboardInteractiveCallback != nil ||
|
||||||
pse.Next.PasswordCallback != nil ||
|
pse.Next.PasswordCallback != nil ||
|
||||||
@@ -336,7 +336,7 @@ func (c *conn) clientAuth(cm gossh.ConnMetadata) (perms *gossh.Permissions, retE
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
if c.insecureSkipTailscaleAuth {
|
if c.insecureSkipTailscaleAuth {
|
||||||
return &gossh.Permissions{}, nil
|
return &ssh.Permissions{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := c.setInfo(cm); err != nil {
|
if err := c.setInfo(cm); err != nil {
|
||||||
@@ -384,7 +384,7 @@ func (c *conn) clientAuth(cm gossh.ConnMetadata) (perms *gossh.Permissions, retE
|
|||||||
}
|
}
|
||||||
c.finalAction = action
|
c.finalAction = action
|
||||||
c.authCompleted.Store(true)
|
c.authCompleted.Store(true)
|
||||||
return &gossh.Permissions{}, nil
|
return &ssh.Permissions{}, nil
|
||||||
case action.Reject:
|
case action.Reject:
|
||||||
metricTerminalReject.Add(1)
|
metricTerminalReject.Add(1)
|
||||||
c.finalAction = action
|
c.finalAction = action
|
||||||
@@ -417,14 +417,14 @@ func (c *conn) clientAuth(cm gossh.ConnMetadata) (perms *gossh.Permissions, retE
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServerConfig implements ssh.ServerConfigCallback.
|
// ServerConfig implements gliderssh.ServerConfigCallback.
|
||||||
func (c *conn) ServerConfig(ctx ssh.Context) *gossh.ServerConfig {
|
func (c *conn) ServerConfig(ctx gliderssh.Context) *ssh.ServerConfig {
|
||||||
return &gossh.ServerConfig{
|
return &ssh.ServerConfig{
|
||||||
PreAuthConnCallback: func(spac gossh.ServerPreAuthConn) {
|
PreAuthConnCallback: func(spac ssh.ServerPreAuthConn) {
|
||||||
c.spac = spac
|
c.spac = spac
|
||||||
},
|
},
|
||||||
NoClientAuth: true, // required for the NoClientAuthCallback to run
|
NoClientAuth: true, // required for the NoClientAuthCallback to run
|
||||||
NoClientAuthCallback: func(cm gossh.ConnMetadata) (*gossh.Permissions, error) {
|
NoClientAuthCallback: func(cm ssh.ConnMetadata) (*ssh.Permissions, error) {
|
||||||
// First perform client authentication, which can potentially
|
// First perform client authentication, which can potentially
|
||||||
// involve multiple steps (for example prompting user to log in to
|
// involve multiple steps (for example prompting user to log in to
|
||||||
// Tailscale admin panel to confirm identity).
|
// Tailscale admin panel to confirm identity).
|
||||||
@@ -438,10 +438,10 @@ func (c *conn) ServerConfig(ctx ssh.Context) *gossh.ServerConfig {
|
|||||||
// specify a username ending in "+password" to force password auth.
|
// specify a username ending in "+password" to force password auth.
|
||||||
// The actual value of the password doesn't matter.
|
// The actual value of the password doesn't matter.
|
||||||
if strings.HasSuffix(cm.User(), forcePasswordSuffix) {
|
if strings.HasSuffix(cm.User(), forcePasswordSuffix) {
|
||||||
return nil, &gossh.PartialSuccessError{
|
return nil, &ssh.PartialSuccessError{
|
||||||
Next: gossh.ServerAuthCallbacks{
|
Next: ssh.ServerAuthCallbacks{
|
||||||
PasswordCallback: func(_ gossh.ConnMetadata, password []byte) (*gossh.Permissions, error) {
|
PasswordCallback: func(_ ssh.ConnMetadata, password []byte) (*ssh.Permissions, error) {
|
||||||
return &gossh.Permissions{}, nil
|
return &ssh.Permissions{}, nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -449,14 +449,14 @@ func (c *conn) ServerConfig(ctx ssh.Context) *gossh.ServerConfig {
|
|||||||
|
|
||||||
return perms, nil
|
return perms, nil
|
||||||
},
|
},
|
||||||
PasswordCallback: func(cm gossh.ConnMetadata, pword []byte) (*gossh.Permissions, error) {
|
PasswordCallback: func(cm ssh.ConnMetadata, pword []byte) (*ssh.Permissions, error) {
|
||||||
// Some clients don't request 'none' authentication. Instead, they
|
// Some clients don't request 'none' authentication. Instead, they
|
||||||
// immediately supply a password. We humor them by accepting the
|
// immediately supply a password. We humor them by accepting the
|
||||||
// password, but authenticate as usual, ignoring the actual value of
|
// password, but authenticate as usual, ignoring the actual value of
|
||||||
// the password.
|
// the password.
|
||||||
return c.clientAuth(cm)
|
return c.clientAuth(cm)
|
||||||
},
|
},
|
||||||
PublicKeyCallback: func(cm gossh.ConnMetadata, key gossh.PublicKey) (*gossh.Permissions, error) {
|
PublicKeyCallback: func(cm ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
|
||||||
// Some clients don't request 'none' authentication. Instead, they
|
// Some clients don't request 'none' authentication. Instead, they
|
||||||
// immediately supply a public key. We humor them by accepting the
|
// immediately supply a public key. We humor them by accepting the
|
||||||
// key, but authenticate as usual, ignoring the actual content of
|
// key, but authenticate as usual, ignoring the actual content of
|
||||||
@@ -479,9 +479,9 @@ func (srv *server) newConn() (*conn, error) {
|
|||||||
c := &conn{srv: srv}
|
c := &conn{srv: srv}
|
||||||
now := srv.now()
|
now := srv.now()
|
||||||
c.connID = fmt.Sprintf("ssh-conn-%s-%02x", now.UTC().Format("20060102T150405"), randBytes(5))
|
c.connID = fmt.Sprintf("ssh-conn-%s-%02x", now.UTC().Format("20060102T150405"), randBytes(5))
|
||||||
fwdHandler := &ssh.ForwardedTCPHandler{}
|
fwdHandler := &gliderssh.ForwardedTCPHandler{}
|
||||||
streamLocalFwdHandler := &ssh.ForwardedUnixHandler{}
|
streamLocalFwdHandler := &gliderssh.ForwardedUnixHandler{}
|
||||||
c.Server = &ssh.Server{
|
c.Server = &gliderssh.Server{
|
||||||
Version: "Tailscale",
|
Version: "Tailscale",
|
||||||
ServerConfigCallback: c.ServerConfig,
|
ServerConfigCallback: c.ServerConfig,
|
||||||
|
|
||||||
@@ -492,14 +492,14 @@ func (srv *server) newConn() (*conn, error) {
|
|||||||
LocalUnixForwardingCallback: c.mayForwardLocalUnixTo,
|
LocalUnixForwardingCallback: c.mayForwardLocalUnixTo,
|
||||||
ReverseUnixForwardingCallback: c.mayReverseUnixForwardTo,
|
ReverseUnixForwardingCallback: c.mayReverseUnixForwardTo,
|
||||||
|
|
||||||
SubsystemHandlers: map[string]ssh.SubsystemHandler{
|
SubsystemHandlers: map[string]gliderssh.SubsystemHandler{
|
||||||
"sftp": c.handleSessionPostSSHAuth,
|
"sftp": c.handleSessionPostSSHAuth,
|
||||||
},
|
},
|
||||||
ChannelHandlers: map[string]ssh.ChannelHandler{
|
ChannelHandlers: map[string]gliderssh.ChannelHandler{
|
||||||
"direct-tcpip": ssh.DirectTCPIPHandler,
|
"direct-tcpip": gliderssh.DirectTCPIPHandler,
|
||||||
"direct-streamlocal@openssh.com": ssh.DirectStreamLocalHandler,
|
"direct-streamlocal@openssh.com": gliderssh.DirectStreamLocalHandler,
|
||||||
},
|
},
|
||||||
RequestHandlers: map[string]ssh.RequestHandler{
|
RequestHandlers: map[string]gliderssh.RequestHandler{
|
||||||
"tcpip-forward": fwdHandler.HandleSSHRequest,
|
"tcpip-forward": fwdHandler.HandleSSHRequest,
|
||||||
"cancel-tcpip-forward": fwdHandler.HandleSSHRequest,
|
"cancel-tcpip-forward": fwdHandler.HandleSSHRequest,
|
||||||
"streamlocal-forward@openssh.com": streamLocalFwdHandler.HandleSSHRequest,
|
"streamlocal-forward@openssh.com": streamLocalFwdHandler.HandleSSHRequest,
|
||||||
@@ -507,9 +507,9 @@ func (srv *server) newConn() (*conn, error) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
ss := c.Server
|
ss := c.Server
|
||||||
maps.Copy(ss.RequestHandlers, ssh.DefaultRequestHandlers)
|
maps.Copy(ss.RequestHandlers, gliderssh.DefaultRequestHandlers)
|
||||||
maps.Copy(ss.ChannelHandlers, ssh.DefaultChannelHandlers)
|
maps.Copy(ss.ChannelHandlers, gliderssh.DefaultChannelHandlers)
|
||||||
maps.Copy(ss.SubsystemHandlers, ssh.DefaultSubsystemHandlers)
|
maps.Copy(ss.SubsystemHandlers, gliderssh.DefaultSubsystemHandlers)
|
||||||
keys, err := getHostKeys(srv.lb.TailscaleVarRoot(), srv.logf)
|
keys, err := getHostKeys(srv.lb.TailscaleVarRoot(), srv.logf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -523,7 +523,7 @@ func (srv *server) newConn() (*conn, error) {
|
|||||||
// mayReversePortPortForwardTo reports whether the ctx should be allowed to port forward
|
// mayReversePortPortForwardTo reports whether the ctx should be allowed to port forward
|
||||||
// to the specified host and port.
|
// to the specified host and port.
|
||||||
// TODO(bradfitz/maisem): should we have more checks on host/port?
|
// TODO(bradfitz/maisem): should we have more checks on host/port?
|
||||||
func (c *conn) mayReversePortForwardTo(ctx ssh.Context, destinationHost string, destinationPort uint32) bool {
|
func (c *conn) mayReversePortForwardTo(ctx gliderssh.Context, destinationHost string, destinationPort uint32) bool {
|
||||||
if sshDisableForwarding() {
|
if sshDisableForwarding() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -537,7 +537,7 @@ func (c *conn) mayReversePortForwardTo(ctx ssh.Context, destinationHost string,
|
|||||||
// mayForwardLocalPortTo reports whether the ctx should be allowed to port forward
|
// mayForwardLocalPortTo reports whether the ctx should be allowed to port forward
|
||||||
// to the specified host and port.
|
// to the specified host and port.
|
||||||
// TODO(bradfitz/maisem): should we have more checks on host/port?
|
// TODO(bradfitz/maisem): should we have more checks on host/port?
|
||||||
func (c *conn) mayForwardLocalPortTo(ctx ssh.Context, destinationHost string, destinationPort uint32) bool {
|
func (c *conn) mayForwardLocalPortTo(ctx gliderssh.Context, destinationHost string, destinationPort uint32) bool {
|
||||||
if sshDisableForwarding() {
|
if sshDisableForwarding() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -548,42 +548,44 @@ func (c *conn) mayForwardLocalPortTo(ctx ssh.Context, destinationHost string, de
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// mayForwardLocalUnixTo reports whether the ctx should be allowed to forward
|
// mayForwardLocalUnixTo is the server-side handler for
|
||||||
// to the specified Unix domain socket path. This is the server-side handler for
|
// direct-streamlocal@openssh.com (SSH -L with Unix sockets). It returns a
|
||||||
// direct-streamlocal@openssh.com (SSH -L with Unix sockets).
|
// connection to the specified Unix domain socket path if forwarding is
|
||||||
func (c *conn) mayForwardLocalUnixTo(ctx ssh.Context, socketPath string) (net.Conn, error) {
|
// permitted, or an error if not.
|
||||||
|
func (c *conn) mayForwardLocalUnixTo(ctx gliderssh.Context, socketPath string) (net.Conn, error) {
|
||||||
if sshDisableForwarding() {
|
if sshDisableForwarding() {
|
||||||
return nil, ssh.ErrRejected
|
return nil, gliderssh.ErrRejected
|
||||||
}
|
}
|
||||||
if c.finalAction != nil && c.finalAction.AllowLocalPortForwarding {
|
if c.finalAction != nil && c.finalAction.AllowLocalPortForwarding {
|
||||||
metricLocalPortForward.Add(1)
|
metricLocalPortForward.Add(1)
|
||||||
cb := ssh.NewLocalUnixForwardingCallback(c.unixForwardingOptions())
|
cb := gliderssh.NewLocalUnixForwardingCallback(c.unixForwardingOptions())
|
||||||
return cb(ctx, socketPath)
|
return cb(ctx, socketPath)
|
||||||
}
|
}
|
||||||
return nil, ssh.ErrRejected
|
return nil, gliderssh.ErrRejected
|
||||||
}
|
}
|
||||||
|
|
||||||
// mayReverseUnixForwardTo reports whether the ctx should be allowed to create
|
// mayReverseUnixForwardTo is the server-side handler for
|
||||||
// a reverse Unix domain socket forward. This is the server-side handler for
|
// streamlocal-forward@openssh.com (SSH -R with Unix sockets). It returns a
|
||||||
// streamlocal-forward@openssh.com (SSH -R with Unix sockets).
|
// listener for the specified Unix domain socket path if reverse forwarding is
|
||||||
func (c *conn) mayReverseUnixForwardTo(ctx ssh.Context, socketPath string) (net.Listener, error) {
|
// permitted, or an error if not.
|
||||||
|
func (c *conn) mayReverseUnixForwardTo(ctx gliderssh.Context, socketPath string) (net.Listener, error) {
|
||||||
if sshDisableForwarding() {
|
if sshDisableForwarding() {
|
||||||
return nil, ssh.ErrRejected
|
return nil, gliderssh.ErrRejected
|
||||||
}
|
}
|
||||||
if c.finalAction != nil && c.finalAction.AllowRemotePortForwarding {
|
if c.finalAction != nil && c.finalAction.AllowRemotePortForwarding {
|
||||||
metricRemotePortForward.Add(1)
|
metricRemotePortForward.Add(1)
|
||||||
cb := ssh.NewReverseUnixForwardingCallback(c.unixForwardingOptions())
|
cb := gliderssh.NewReverseUnixForwardingCallback(c.unixForwardingOptions())
|
||||||
return cb(ctx, socketPath)
|
return cb(ctx, socketPath)
|
||||||
}
|
}
|
||||||
return nil, ssh.ErrRejected
|
return nil, gliderssh.ErrRejected
|
||||||
}
|
}
|
||||||
|
|
||||||
// unixForwardingOptions returns the Unix forwarding options scoped to the
|
// unixForwardingOptions returns the Unix forwarding options scoped to the
|
||||||
// authenticated local user. Socket paths are restricted to the user's home
|
// authenticated local user. Socket paths are restricted to the user's home
|
||||||
// directory, /tmp, and /run/user/<uid>.
|
// directory, /tmp, and /run/user/<uid>.
|
||||||
func (c *conn) unixForwardingOptions() ssh.UnixForwardingOptions {
|
func (c *conn) unixForwardingOptions() gliderssh.UnixForwardingOptions {
|
||||||
return ssh.UnixForwardingOptions{
|
return gliderssh.UnixForwardingOptions{
|
||||||
AllowedDirectories: ssh.UserSocketDirectories(c.localUser.HomeDir, c.localUser.Uid),
|
AllowedDirectories: gliderssh.UserSocketDirectories(c.localUser.HomeDir, c.localUser.Uid),
|
||||||
BindUnlink: true,
|
BindUnlink: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -635,7 +637,7 @@ func toIPPort(a net.Addr) (ipp netip.AddrPort) {
|
|||||||
|
|
||||||
// connInfo populates the sshConnInfo from the provided arguments,
|
// connInfo populates the sshConnInfo from the provided arguments,
|
||||||
// validating only that they represent a known Tailscale identity.
|
// validating only that they represent a known Tailscale identity.
|
||||||
func (c *conn) setInfo(cm gossh.ConnMetadata) error {
|
func (c *conn) setInfo(cm ssh.ConnMetadata) error {
|
||||||
if c.info != nil {
|
if c.info != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -685,7 +687,7 @@ func (c *conn) evaluatePolicy() (_ *tailcfg.SSHAction, localUser string, acceptE
|
|||||||
// handleSessionPostSSHAuth runs an SSH session after the SSH-level authentication,
|
// handleSessionPostSSHAuth runs an SSH session after the SSH-level authentication,
|
||||||
// but not necessarily before all the Tailscale-level extra verification has
|
// but not necessarily before all the Tailscale-level extra verification has
|
||||||
// completed. It also handles SFTP requests.
|
// completed. It also handles SFTP requests.
|
||||||
func (c *conn) handleSessionPostSSHAuth(s ssh.Session) {
|
func (c *conn) handleSessionPostSSHAuth(s gliderssh.Session) {
|
||||||
// Do this check after auth, but before starting the session.
|
// Do this check after auth, but before starting the session.
|
||||||
switch s.Subsystem() {
|
switch s.Subsystem() {
|
||||||
case "sftp":
|
case "sftp":
|
||||||
@@ -734,7 +736,7 @@ func (c *conn) expandDelegateURLLocked(actionURL string) string {
|
|||||||
|
|
||||||
// sshSession is an accepted Tailscale SSH session.
|
// sshSession is an accepted Tailscale SSH session.
|
||||||
type sshSession struct {
|
type sshSession struct {
|
||||||
ssh.Session
|
gliderssh.Session
|
||||||
sharedID string // ID that's shared with control
|
sharedID string // ID that's shared with control
|
||||||
logf logger.Logf
|
logf logger.Logf
|
||||||
|
|
||||||
@@ -747,8 +749,8 @@ type sshSession struct {
|
|||||||
cmd *exec.Cmd
|
cmd *exec.Cmd
|
||||||
wrStdin io.WriteCloser
|
wrStdin io.WriteCloser
|
||||||
rdStdout io.ReadCloser
|
rdStdout io.ReadCloser
|
||||||
rdStderr io.ReadCloser // rdStderr is nil for pty sessions
|
rdStderr io.ReadCloser // rdStderr is nil for pty sessions
|
||||||
ptyReq *ssh.Pty // non-nil for pty sessions
|
ptyReq *gliderssh.Pty // non-nil for pty sessions
|
||||||
|
|
||||||
// childPipes is a list of pipes that need to be closed when the process exits.
|
// childPipes is a list of pipes that need to be closed when the process exits.
|
||||||
// For pty sessions, this is the tty fd.
|
// For pty sessions, this is the tty fd.
|
||||||
@@ -772,7 +774,7 @@ func (ss *sshSession) vlogf(format string, args ...any) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *conn) newSSHSession(s ssh.Session) *sshSession {
|
func (c *conn) newSSHSession(s gliderssh.Session) *sshSession {
|
||||||
sharedID := fmt.Sprintf("sess-%s-%02x", c.srv.now().UTC().Format("20060102T150405"), randBytes(5))
|
sharedID := fmt.Sprintf("sess-%s-%02x", c.srv.now().UTC().Format("20060102T150405"), randBytes(5))
|
||||||
c.logf("starting session: %v", sharedID)
|
c.logf("starting session: %v", sharedID)
|
||||||
ctx, cancel := context.WithCancelCause(s.Context())
|
ctx, cancel := context.WithCancelCause(s.Context())
|
||||||
@@ -907,10 +909,10 @@ func (c *conn) detachSession(ss *sshSession) {
|
|||||||
var errSessionDone = errors.New("session is done")
|
var errSessionDone = errors.New("session is done")
|
||||||
|
|
||||||
// handleSSHAgentForwarding starts a Unix socket listener and in the background
|
// handleSSHAgentForwarding starts a Unix socket listener and in the background
|
||||||
// forwards agent connections between the listener and the ssh.Session.
|
// forwards agent connections between the listener and the gliderssh.Session.
|
||||||
// On success, it assigns ss.agentListener.
|
// On success, it assigns ss.agentListener.
|
||||||
func (ss *sshSession) handleSSHAgentForwarding(s ssh.Session, lu *userMeta) error {
|
func (ss *sshSession) handleSSHAgentForwarding(s gliderssh.Session, lu *userMeta) error {
|
||||||
if !ssh.AgentRequested(ss) || !ss.conn.finalAction.AllowAgentForwarding {
|
if !gliderssh.AgentRequested(ss) || !ss.conn.finalAction.AllowAgentForwarding {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if sshDisableForwarding() {
|
if sshDisableForwarding() {
|
||||||
@@ -920,7 +922,7 @@ func (ss *sshSession) handleSSHAgentForwarding(s ssh.Session, lu *userMeta) erro
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
ss.logf("ssh: agent forwarding requested")
|
ss.logf("ssh: agent forwarding requested")
|
||||||
ln, err := ssh.NewAgentListener()
|
ln, err := gliderssh.NewAgentListener()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -952,7 +954,7 @@ func (ss *sshSession) handleSSHAgentForwarding(s ssh.Session, lu *userMeta) erro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
go ssh.ForwardAgentConnections(ln, s)
|
go gliderssh.ForwardAgentConnections(ln, s)
|
||||||
ss.agentListener = ln
|
ss.agentListener = ln
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -1325,7 +1327,7 @@ func (ss *sshSession) startNewRecording() (_ *recording, err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var w ssh.Window
|
var w gliderssh.Window
|
||||||
if ptyReq, _, isPtyReq := ss.Pty(); isPtyReq {
|
if ptyReq, _, isPtyReq := ss.Pty(); isPtyReq {
|
||||||
w = ptyReq.Window
|
w = ptyReq.Window
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ import (
|
|||||||
"github.com/bramvdbogaerde/go-scp"
|
"github.com/bramvdbogaerde/go-scp"
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
"github.com/pkg/sftp"
|
"github.com/pkg/sftp"
|
||||||
|
gliderssh "github.com/tailscale/gliderssh"
|
||||||
"golang.org/x/crypto/ssh"
|
"golang.org/x/crypto/ssh"
|
||||||
"golang.org/x/crypto/ssh/agent"
|
"golang.org/x/crypto/ssh/agent"
|
||||||
"tailscale.com/net/tsdial"
|
"tailscale.com/net/tsdial"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
glider "tailscale.com/tempfork/gliderlabs/ssh"
|
|
||||||
"tailscale.com/types/key"
|
"tailscale.com/types/key"
|
||||||
"tailscale.com/types/netmap"
|
"tailscale.com/types/netmap"
|
||||||
"tailscale.com/util/set"
|
"tailscale.com/util/set"
|
||||||
@@ -360,11 +360,11 @@ func TestSSHAgentForwarding(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Run an SSH server that accepts connections from that client SSH key.
|
// Run an SSH server that accepts connections from that client SSH key.
|
||||||
gs := glider.Server{
|
gs := gliderssh.Server{
|
||||||
Handler: func(s glider.Session) {
|
Handler: func(s gliderssh.Session) {
|
||||||
io.WriteString(s, "Hello world\n")
|
io.WriteString(s, "Hello world\n")
|
||||||
},
|
},
|
||||||
PublicKeyHandler: func(ctx glider.Context, key glider.PublicKey) error {
|
PublicKeyHandler: func(ctx gliderssh.Context, key gliderssh.PublicKey) error {
|
||||||
// Note - this is not meant to be cryptographically secure, it's
|
// Note - this is not meant to be cryptographically secure, it's
|
||||||
// just checking that SSH agent forwarding is forwarding the right
|
// just checking that SSH agent forwarding is forwarding the right
|
||||||
// key.
|
// key.
|
||||||
@@ -464,6 +464,233 @@ client.exec_command('pwd')
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestLocalUnixForwarding tests direct-streamlocal@openssh.com, which is what
|
||||||
|
// podman remote (issue #12409) and VSCode Remote (issue #5295) use to reach
|
||||||
|
// Unix domain sockets on the remote host through SSH. The client opens a
|
||||||
|
// channel to a Unix socket path on the server, and data is proxied through.
|
||||||
|
func TestLocalUnixForwarding(t *testing.T) {
|
||||||
|
debugTest.Store(true)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
debugTest.Store(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a Unix socket server in /tmp that simulates a service like
|
||||||
|
// podman's API socket at /run/user/<uid>/podman/podman.sock.
|
||||||
|
socketDir, err := os.MkdirTemp("", "tailssh-test-")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { os.RemoveAll(socketDir) })
|
||||||
|
socketPath := filepath.Join(socketDir, "test-service.sock")
|
||||||
|
|
||||||
|
ul, err := net.Listen("unix", socketPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { ul.Close() })
|
||||||
|
|
||||||
|
// The service echoes back whatever it receives, like an API server would.
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
conn, err := ul.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
defer conn.Close()
|
||||||
|
io.Copy(conn, conn)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Start Tailscale SSH server with local port forwarding enabled.
|
||||||
|
addr := testServerWithOpts(t, testServerOpts{
|
||||||
|
username: "testuser",
|
||||||
|
allowLocalPortForwarding: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Connect to the Tailscale SSH server.
|
||||||
|
cl, err := ssh.Dial("tcp", addr, &ssh.ClientConfig{
|
||||||
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { cl.Close() })
|
||||||
|
|
||||||
|
// Open a direct-streamlocal@openssh.com channel to the Unix socket,
|
||||||
|
// exactly as podman remote does.
|
||||||
|
conn, err := cl.Dial("unix", socketPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to dial unix socket through SSH: %s", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
// Send data through the tunnel and verify it echoes back.
|
||||||
|
want := "GET /_ping HTTP/1.1\r\nHost: d\r\n\r\n"
|
||||||
|
_, err = io.WriteString(conn, want)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to write through tunnel: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := make([]byte, len(want))
|
||||||
|
_, err = io.ReadFull(conn, got)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read through tunnel: %s", err)
|
||||||
|
}
|
||||||
|
if string(got) != want {
|
||||||
|
t.Errorf("got %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestReverseUnixForwarding tests streamlocal-forward@openssh.com, which tools
|
||||||
|
// like VSCode Remote and Zed use to create Unix domain sockets on the remote
|
||||||
|
// host that forward connections back to the client through SSH.
|
||||||
|
func TestReverseUnixForwarding(t *testing.T) {
|
||||||
|
debugTest.Store(true)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
debugTest.Store(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start Tailscale SSH server with remote port forwarding enabled.
|
||||||
|
addr := testServerWithOpts(t, testServerOpts{
|
||||||
|
username: "testuser",
|
||||||
|
allowRemotePortForwarding: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Connect to the Tailscale SSH server.
|
||||||
|
cl, err := ssh.Dial("tcp", addr, &ssh.ClientConfig{
|
||||||
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { cl.Close() })
|
||||||
|
|
||||||
|
// Request reverse forwarding -- the server creates a Unix socket and
|
||||||
|
// forwards incoming connections back through the SSH tunnel.
|
||||||
|
socketDir, err := os.MkdirTemp("", "tailssh-test-")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { os.RemoveAll(socketDir) })
|
||||||
|
remoteSocketPath := filepath.Join(socketDir, "reverse.sock")
|
||||||
|
|
||||||
|
ln, err := cl.ListenUnix(remoteSocketPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to request reverse unix forwarding: %s", err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { ln.Close() })
|
||||||
|
|
||||||
|
// Verify the socket file was created on the server side.
|
||||||
|
if _, err := os.Stat(remoteSocketPath); err != nil {
|
||||||
|
t.Fatalf("reverse forwarded socket not created: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accept a connection from the tunnel (client side) and write data.
|
||||||
|
want := "hello from reverse tunnel"
|
||||||
|
go func() {
|
||||||
|
conn, err := ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
io.WriteString(conn, want)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Connect directly to the socket on the server side, simulating a
|
||||||
|
// local process connecting to the VSCode/Zed IPC socket.
|
||||||
|
conn, err := net.Dial("unix", remoteSocketPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to connect to reverse forwarded socket: %s", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
|
got, err := io.ReadAll(conn)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read from reverse forwarded socket: %s", err)
|
||||||
|
}
|
||||||
|
if string(got) != want {
|
||||||
|
t.Errorf("got %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUnixForwardingDenied verifies that Unix socket forwarding is rejected
|
||||||
|
// when the SSH policy does not permit port forwarding.
|
||||||
|
func TestUnixForwardingDenied(t *testing.T) {
|
||||||
|
debugTest.Store(true)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
debugTest.Store(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Start server with forwarding disabled (the default policy).
|
||||||
|
addr := testServerWithOpts(t, testServerOpts{
|
||||||
|
username: "testuser",
|
||||||
|
})
|
||||||
|
|
||||||
|
cl, err := ssh.Dial("tcp", addr, &ssh.ClientConfig{
|
||||||
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { cl.Close() })
|
||||||
|
|
||||||
|
// Direct Unix socket forwarding should be rejected.
|
||||||
|
_, err = cl.Dial("unix", "/tmp/anything.sock")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected direct unix forwarding to be rejected, but it succeeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reverse Unix socket forwarding should also be rejected.
|
||||||
|
socketDir, err := os.MkdirTemp("", "tailssh-test-")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { os.RemoveAll(socketDir) })
|
||||||
|
|
||||||
|
_, err = cl.ListenUnix(filepath.Join(socketDir, "denied.sock"))
|
||||||
|
if err == nil {
|
||||||
|
t.Error("expected reverse unix forwarding to be rejected, but it succeeded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUnixForwardingPathRestriction verifies that socket paths outside the
|
||||||
|
// allowed directories (home, /tmp, /run/user/<uid>) are rejected even when
|
||||||
|
// forwarding is permitted by policy.
|
||||||
|
func TestUnixForwardingPathRestriction(t *testing.T) {
|
||||||
|
debugTest.Store(true)
|
||||||
|
t.Cleanup(func() {
|
||||||
|
debugTest.Store(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
addr := testServerWithOpts(t, testServerOpts{
|
||||||
|
username: "testuser",
|
||||||
|
allowLocalPortForwarding: true,
|
||||||
|
allowRemotePortForwarding: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
cl, err := ssh.Dial("tcp", addr, &ssh.ClientConfig{
|
||||||
|
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { cl.Close() })
|
||||||
|
|
||||||
|
// Paths outside allowed directories should be rejected.
|
||||||
|
restrictedPaths := []string{
|
||||||
|
"/var/run/docker.sock",
|
||||||
|
"/etc/evil.sock",
|
||||||
|
}
|
||||||
|
for _, path := range restrictedPaths {
|
||||||
|
_, err := cl.Dial("unix", path)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected direct forwarding to %q to be rejected, but it succeeded", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func fallbackToSUAvailable() bool {
|
func fallbackToSUAvailable() bool {
|
||||||
if runtime.GOOS != "linux" {
|
if runtime.GOOS != "linux" {
|
||||||
return false
|
return false
|
||||||
@@ -582,6 +809,47 @@ func testServer(t *testing.T, username string, forceV1Behavior bool, allowSendEn
|
|||||||
return l.Addr().String()
|
return l.Addr().String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type testServerOpts struct {
|
||||||
|
username string
|
||||||
|
forceV1Behavior bool
|
||||||
|
allowSendEnv bool
|
||||||
|
allowLocalPortForwarding bool
|
||||||
|
allowRemotePortForwarding bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func testServerWithOpts(t *testing.T, opts testServerOpts) string {
|
||||||
|
t.Helper()
|
||||||
|
srv := &server{
|
||||||
|
lb: &testBackend{
|
||||||
|
localUser: opts.username,
|
||||||
|
forceV1Behavior: opts.forceV1Behavior,
|
||||||
|
allowSendEnv: opts.allowSendEnv,
|
||||||
|
allowLocalPortForwarding: opts.allowLocalPortForwarding,
|
||||||
|
allowRemotePortForwarding: opts.allowRemotePortForwarding,
|
||||||
|
},
|
||||||
|
logf: log.Printf,
|
||||||
|
tailscaledPath: os.Getenv("TAILSCALED_PATH"),
|
||||||
|
timeNow: time.Now,
|
||||||
|
}
|
||||||
|
|
||||||
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { l.Close() })
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
conn, err := l.Accept()
|
||||||
|
if err == nil {
|
||||||
|
go srv.HandleSSHConn(&addressFakingConn{conn})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return l.Addr().String()
|
||||||
|
}
|
||||||
|
|
||||||
func testSession(t *testing.T, forceV1Behavior bool, allowSendEnv bool, sendEnv map[string]string) *session {
|
func testSession(t *testing.T, forceV1Behavior bool, allowSendEnv bool, sendEnv map[string]string) *session {
|
||||||
cl := testClient(t, forceV1Behavior, allowSendEnv)
|
cl := testClient(t, forceV1Behavior, allowSendEnv)
|
||||||
return testSessionFor(t, cl, sendEnv)
|
return testSessionFor(t, cl, sendEnv)
|
||||||
@@ -639,9 +907,11 @@ func generateClientKey(t *testing.T, privateKeyFile string) (ssh.Signer, *rsa.Pr
|
|||||||
|
|
||||||
// testBackend implements ipnLocalBackend
|
// testBackend implements ipnLocalBackend
|
||||||
type testBackend struct {
|
type testBackend struct {
|
||||||
localUser string
|
localUser string
|
||||||
forceV1Behavior bool
|
forceV1Behavior bool
|
||||||
allowSendEnv bool
|
allowSendEnv bool
|
||||||
|
allowLocalPortForwarding bool
|
||||||
|
allowRemotePortForwarding bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tb *testBackend) ShouldRunSSH() bool {
|
func (tb *testBackend) ShouldRunSSH() bool {
|
||||||
@@ -661,7 +931,12 @@ func (tb *testBackend) NetMap() *netmap.NetworkMap {
|
|||||||
Rules: []*tailcfg.SSHRule{
|
Rules: []*tailcfg.SSHRule{
|
||||||
{
|
{
|
||||||
Principals: []*tailcfg.SSHPrincipal{{Any: true}},
|
Principals: []*tailcfg.SSHPrincipal{{Any: true}},
|
||||||
Action: &tailcfg.SSHAction{Accept: true, AllowAgentForwarding: true},
|
Action: &tailcfg.SSHAction{
|
||||||
|
Accept: true,
|
||||||
|
AllowAgentForwarding: true,
|
||||||
|
AllowLocalPortForwarding: tb.allowLocalPortForwarding,
|
||||||
|
AllowRemotePortForwarding: tb.allowRemotePortForwarding,
|
||||||
|
},
|
||||||
SSHUsers: map[string]string{"*": tb.localUser},
|
SSHUsers: map[string]string{"*": tb.localUser},
|
||||||
AcceptEnv: []string{"GIT_*", "EXACT_MATCH", "TEST?NG"},
|
AcceptEnv: []string{"GIT_*", "EXACT_MATCH", "TEST?NG"},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import (
|
|||||||
"testing/synctest"
|
"testing/synctest"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
gliderssh "github.com/tailscale/gliderssh"
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
"golang.org/x/net/http2/h2c"
|
"golang.org/x/net/http2/h2c"
|
||||||
"tailscale.com/cmd/testwrapper/flakytest"
|
"tailscale.com/cmd/testwrapper/flakytest"
|
||||||
@@ -42,7 +43,6 @@ import (
|
|||||||
"tailscale.com/net/tsdial"
|
"tailscale.com/net/tsdial"
|
||||||
"tailscale.com/sessionrecording"
|
"tailscale.com/sessionrecording"
|
||||||
"tailscale.com/tailcfg"
|
"tailscale.com/tailcfg"
|
||||||
"tailscale.com/tempfork/gliderlabs/ssh"
|
|
||||||
testssh "tailscale.com/tempfork/sshtest/ssh"
|
testssh "tailscale.com/tempfork/sshtest/ssh"
|
||||||
"tailscale.com/tsd"
|
"tailscale.com/tsd"
|
||||||
"tailscale.com/tstest"
|
"tailscale.com/tstest"
|
||||||
@@ -688,9 +688,9 @@ func TestSSHRecordingNonInteractive(t *testing.T) {
|
|||||||
t.Skipf("skipping on %q; only runs on linux and darwin", runtime.GOOS)
|
t.Skipf("skipping on %q; only runs on linux and darwin", runtime.GOOS)
|
||||||
}
|
}
|
||||||
var recording []byte
|
var recording []byte
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
done := make(chan struct{})
|
||||||
recordingServer := mockRecordingServer(t, func(w http.ResponseWriter, r *http.Request) {
|
recordingServer := mockRecordingServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||||
defer cancel()
|
defer close(done)
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.(http.Flusher).Flush()
|
w.(http.Flusher).Flush()
|
||||||
|
|
||||||
@@ -758,7 +758,11 @@ func TestSSHRecordingNonInteractive(t *testing.T) {
|
|||||||
}
|
}
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
<-ctx.Done() // wait for recording to finish
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(30 * time.Second):
|
||||||
|
t.Fatal("timed out waiting for recording")
|
||||||
|
}
|
||||||
var ch sessionrecording.CastHeader
|
var ch sessionrecording.CastHeader
|
||||||
if err := json.NewDecoder(bytes.NewReader(recording)).Decode(&ch); err != nil {
|
if err := json.NewDecoder(bytes.NewReader(recording)).Decode(&ch); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -1094,7 +1098,7 @@ func TestSSH(t *testing.T) {
|
|||||||
sc.finalAction = sc.action0
|
sc.finalAction = sc.action0
|
||||||
sc.authCompleted.Store(true)
|
sc.authCompleted.Store(true)
|
||||||
|
|
||||||
sc.Handler = func(s ssh.Session) {
|
sc.Handler = func(s gliderssh.Session) {
|
||||||
sc.newSSHSession(s).run()
|
sc.newSSHSession(s).run()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ RUN if echo "$BASE" | grep "ubuntu:"; then rm -Rf /home/testuser; fi
|
|||||||
RUN TAILSCALED_PATH=`pwd`tailscaled ./tailssh.test -test.v -test.run TestIntegrationSSH
|
RUN TAILSCALED_PATH=`pwd`tailscaled ./tailssh.test -test.v -test.run TestIntegrationSSH
|
||||||
RUN if echo "$BASE" | grep "ubuntu:"; then rm -Rf /home/testuser; fi
|
RUN if echo "$BASE" | grep "ubuntu:"; then rm -Rf /home/testuser; fi
|
||||||
RUN TAILSCALED_PATH=`pwd`tailscaled ./tailssh.test -test.v -test.run TestIntegrationParamiko
|
RUN TAILSCALED_PATH=`pwd`tailscaled ./tailssh.test -test.v -test.run TestIntegrationParamiko
|
||||||
|
RUN TAILSCALED_PATH=`pwd`tailscaled ./tailssh.test -test.v -test.run TestLocalUnixForwarding
|
||||||
|
RUN TAILSCALED_PATH=`pwd`tailscaled ./tailssh.test -test.v -test.run TestReverseUnixForwarding
|
||||||
|
RUN TAILSCALED_PATH=`pwd`tailscaled ./tailssh.test -test.v -test.run TestUnixForwardingDenied
|
||||||
|
RUN TAILSCALED_PATH=`pwd`tailscaled ./tailssh.test -test.v -test.run TestUnixForwardingPathRestriction
|
||||||
|
|
||||||
RUN echo "Then run tests as non-root user testuser and make sure tests still pass."
|
RUN echo "Then run tests as non-root user testuser and make sure tests still pass."
|
||||||
RUN touch /tmp/tailscalessh.log
|
RUN touch /tmp/tailscalessh.log
|
||||||
|
|||||||
Reference in New Issue
Block a user