diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt
index d8142048d..04710e868 100644
--- a/cmd/tailscaled/depaware.txt
+++ b/cmd/tailscaled/depaware.txt
@@ -3,7 +3,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/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
- LD github.com/anmitsu/go-shlex from github.com/tailscale/ssh
+ LD github.com/anmitsu/go-shlex from tailscale.com/tempfork/gliderlabs/ssh
L github.com/aws/aws-sdk-go-v2 from github.com/aws/aws-sdk-go-v2/internal/ini
L github.com/aws/aws-sdk-go-v2/aws from github.com/aws/aws-sdk-go-v2/aws/middleware+
L github.com/aws/aws-sdk-go-v2/aws/arn from tailscale.com/ipn/store/awsstore
@@ -90,6 +90,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
W github.com/pkg/errors from github.com/tailscale/certstore
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
+ LD github.com/tailscale/golang-x-crypto/chacha20 from github.com/tailscale/golang-x-crypto/ssh
+ LD 💣 github.com/tailscale/golang-x-crypto/internal/subtle from github.com/tailscale/golang-x-crypto/chacha20
+ LD github.com/tailscale/golang-x-crypto/ssh from tailscale.com/ipn/ipnlocal+
+ LD github.com/tailscale/golang-x-crypto/ssh/internal/bcrypt_pbkdf from github.com/tailscale/golang-x-crypto/ssh
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
github.com/tailscale/goupnp/dcps/internetgateway2 from tailscale.com/net/portmapper
github.com/tailscale/goupnp/httpu from github.com/tailscale/goupnp+
@@ -97,7 +101,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
L 💣 github.com/tailscale/netlink from tailscale.com/wgengine/router
- LD github.com/tailscale/ssh from tailscale.com/ssh/tailssh
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
LD github.com/u-root/u-root/pkg/termios from tailscale.com/ssh/tailssh
L github.com/u-root/uio/rand from github.com/insomniacslk/dhcp/dhcpv4
@@ -230,6 +233,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/wgengine/netstack
tailscale.com/syncs from tailscale.com/control/controlknobs+
tailscale.com/tailcfg from tailscale.com/client/tailscale+
+ LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/tstime from tailscale.com/wgengine/magicsock
💣 tailscale.com/tstime/mono from tailscale.com/net/tstun+
@@ -281,19 +285,19 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/crypto/acme from tailscale.com/ipn/localapi
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box
golang.org/x/crypto/blake2s from golang.zx2c4.com/wireguard/device+
- LD golang.org/x/crypto/blowfish from golang.org/x/crypto/ssh/internal/bcrypt_pbkdf
+ LD golang.org/x/crypto/blowfish from golang.org/x/crypto/ssh/internal/bcrypt_pbkdf+
golang.org/x/crypto/chacha20 from golang.org/x/crypto/chacha20poly1305+
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+
- LD golang.org/x/crypto/ed25519 from golang.org/x/crypto/ssh
+ LD golang.org/x/crypto/ed25519 from golang.org/x/crypto/ssh+
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/poly1305 from golang.zx2c4.com/wireguard/device
+ golang.org/x/crypto/poly1305 from golang.zx2c4.com/wireguard/device+
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
- LD golang.org/x/crypto/ssh from github.com/tailscale/ssh+
+ LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh
golang.org/x/net/bpf from github.com/mdlayher/genetlink+
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from golang.org/x/net/http2+
diff --git a/cmd/tsshd/tsshd.go b/cmd/tsshd/tsshd.go
index 8da41798a..e19438a2c 100644
--- a/cmd/tsshd/tsshd.go
+++ b/cmd/tsshd/tsshd.go
@@ -31,11 +31,11 @@
"unsafe"
"github.com/creack/pty"
- "github.com/tailscale/ssh"
- gossh "golang.org/x/crypto/ssh"
+ gossh "github.com/tailscale/golang-x-crypto/ssh"
"inet.af/netaddr"
"tailscale.com/net/interfaces"
"tailscale.com/net/tsaddr"
+ "tailscale.com/tempfork/gliderlabs/ssh"
)
var (
diff --git a/go.mod b/go.mod
index 2fe4aefbe..2cf49038a 100644
--- a/go.mod
+++ b/go.mod
@@ -7,6 +7,7 @@ require (
github.com/akutz/memconn v0.1.0
github.com/alessio/shellescape v1.4.1
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74
+ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
github.com/aws/aws-sdk-go-v2 v1.11.2
github.com/aws/aws-sdk-go-v2/config v1.11.0
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.7.4
@@ -16,6 +17,7 @@ require (
github.com/creack/pty v1.1.17
github.com/dave/jennifer v1.4.1
github.com/frankban/quicktest v1.14.0
+ github.com/gliderlabs/ssh v0.3.3
github.com/go-ole/go-ole v1.2.6
github.com/godbus/dbus/v5 v5.0.6
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
@@ -42,7 +44,6 @@ require (
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
github.com/tailscale/hujson v0.0.0-20211105212140-3a0adc019d83
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85
- github.com/tailscale/ssh v0.3.4-0.20220313013419-be8b7add4057
github.com/tcnksm/go-httpstat v0.2.0
github.com/toqueteos/webbrowser v1.2.0
github.com/u-root/u-root v0.8.0
@@ -80,7 +81,6 @@ require (
github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3 // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/alexkohler/prealloc v1.0.0 // indirect
- github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/ashanbrown/forbidigo v1.2.0 // indirect
github.com/ashanbrown/makezero v0.0.0-20210520155254-b6261585ddde // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.0.0 // indirect
@@ -116,7 +116,6 @@ require (
github.com/fatih/structtag v1.2.0 // indirect
github.com/fsnotify/fsnotify v1.5.1 // indirect
github.com/fzipp/gocyclo v0.3.1 // indirect
- github.com/gliderlabs/ssh v0.3.3 // indirect
github.com/go-critic/go-critic v0.6.1 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.3.1 // indirect
@@ -234,6 +233,7 @@ require (
github.com/stretchr/testify v1.7.0 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/sylvia7788/contextcheck v1.0.4 // indirect
+ github.com/tailscale/golang-x-crypto v0.0.0-20220326011347-d690bbfb6b5f // indirect
github.com/tdakkota/asciicheck v0.1.1 // indirect
github.com/tetafro/godot v1.4.11 // indirect
github.com/timakin/bodyclose v0.0.0-20210704033933-f49887972144 // indirect
diff --git a/go.sum b/go.sum
index 5548c874b..07bb4242f 100644
--- a/go.sum
+++ b/go.sum
@@ -1087,14 +1087,14 @@ github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502 h1:34icjjmqJ2HP
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41 h1:/V2rCMMWcsjYaYO2MeovLw+ClP63OtXgCF2Y1eb8+Ns=
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41/go.mod h1:/roCdA6gg6lQyw/Oz6gIIGu3ggJKYhF+WC/AQReE5XQ=
+github.com/tailscale/golang-x-crypto v0.0.0-20220326011347-d690bbfb6b5f h1:SO0bJlfWstNuolA3zjWDcLq0mjLfIw6RWEImAPxCkSU=
+github.com/tailscale/golang-x-crypto v0.0.0-20220326011347-d690bbfb6b5f/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
github.com/tailscale/hujson v0.0.0-20211105212140-3a0adc019d83 h1:f7nwzdAHTUUOJjHZuDvLz9CEAlUM228amCRvwzlPvsA=
github.com/tailscale/hujson v0.0.0-20211105212140-3a0adc019d83/go.mod h1:iTDXJsA6A2wNNjurgic2rk+is6uzU4U2NLm4T+edr6M=
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk=
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
-github.com/tailscale/ssh v0.3.4-0.20220313013419-be8b7add4057 h1:aN1DLGpS7j6LLQaM33w4Lo6Otvq8Rx60D2ciI/UC1KQ=
-github.com/tailscale/ssh v0.3.4-0.20220313013419-be8b7add4057/go.mod h1:LC21Rp6xYOAc7NEeMhYN0xTXUB74MZAN60GRPegZLkg=
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8=
github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
diff --git a/ipn/ipnlocal/ssh.go b/ipn/ipnlocal/ssh.go
index 8689fe2d4..e02d25294 100644
--- a/ipn/ipnlocal/ssh.go
+++ b/ipn/ipnlocal/ssh.go
@@ -23,7 +23,7 @@
"strings"
"sync"
- "golang.org/x/crypto/ssh"
+ "github.com/tailscale/golang-x-crypto/ssh"
"tailscale.com/envknob"
)
diff --git a/ssh/tailssh/incubator.go b/ssh/tailssh/incubator.go
index dc073883f..fdd3acff0 100644
--- a/ssh/tailssh/incubator.go
+++ b/ssh/tailssh/incubator.go
@@ -29,11 +29,11 @@
"syscall"
"github.com/creack/pty"
- "github.com/tailscale/ssh"
"github.com/u-root/u-root/pkg/termios"
gossh "golang.org/x/crypto/ssh"
"golang.org/x/sys/unix"
"tailscale.com/cmd/tailscaled/childproc"
+ "tailscale.com/tempfork/gliderlabs/ssh"
"tailscale.com/types/logger"
)
diff --git a/ssh/tailssh/tailssh.go b/ssh/tailssh/tailssh.go
index def871370..f679d23f4 100644
--- a/ssh/tailssh/tailssh.go
+++ b/ssh/tailssh/tailssh.go
@@ -29,13 +29,13 @@
"sync"
"time"
- "github.com/tailscale/ssh"
"inet.af/netaddr"
"tailscale.com/envknob"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/logtail/backoff"
"tailscale.com/net/tsaddr"
"tailscale.com/tailcfg"
+ "tailscale.com/tempfork/gliderlabs/ssh"
"tailscale.com/types/logger"
)
diff --git a/ssh/tailssh/tailssh_test.go b/ssh/tailssh/tailssh_test.go
index 57767d6ab..afae4984a 100644
--- a/ssh/tailssh/tailssh_test.go
+++ b/ssh/tailssh/tailssh_test.go
@@ -19,12 +19,12 @@
"testing"
"time"
- "github.com/tailscale/ssh"
"inet.af/netaddr"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/store/mem"
"tailscale.com/net/tsdial"
"tailscale.com/tailcfg"
+ "tailscale.com/tempfork/gliderlabs/ssh"
"tailscale.com/types/logger"
"tailscale.com/util/cibuild"
"tailscale.com/util/lineread"
diff --git a/tempfork/gliderlabs/ssh/LICENSE b/tempfork/gliderlabs/ssh/LICENSE
new file mode 100644
index 000000000..4a03f02a2
--- /dev/null
+++ b/tempfork/gliderlabs/ssh/LICENSE
@@ -0,0 +1,27 @@
+Copyright (c) 2016 Glider Labs. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Glider Labs nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/tempfork/gliderlabs/ssh/README.md b/tempfork/gliderlabs/ssh/README.md
new file mode 100644
index 000000000..79b5b89fa
--- /dev/null
+++ b/tempfork/gliderlabs/ssh/README.md
@@ -0,0 +1,96 @@
+# gliderlabs/ssh
+
+[![GoDoc](https://godoc.org/tailscale.com/tempfork/gliderlabs/ssh?status.svg)](https://godoc.org/github.com/gliderlabs/ssh)
+[![CircleCI](https://img.shields.io/circleci/project/github/gliderlabs/ssh.svg)](https://circleci.com/gh/gliderlabs/ssh)
+[![Go Report Card](https://goreportcard.com/badge/tailscale.com/tempfork/gliderlabs/ssh)](https://goreportcard.com/report/github.com/gliderlabs/ssh)
+[![OpenCollective](https://opencollective.com/ssh/sponsors/badge.svg)](#sponsors)
+[![Slack](http://slack.gliderlabs.com/badge.svg)](http://slack.gliderlabs.com)
+[![Email Updates](https://img.shields.io/badge/updates-subscribe-yellow.svg)](https://app.convertkit.com/landing_pages/243312)
+
+> The Glider Labs SSH server package is dope. —[@bradfitz](https://twitter.com/bradfitz), Go team member
+
+This Go package wraps the [crypto/ssh
+package](https://godoc.org/golang.org/x/crypto/ssh) with a higher-level API for
+building SSH servers. The goal of the API was to make it as simple as using
+[net/http](https://golang.org/pkg/net/http/), so the API is very similar:
+
+```go
+ package main
+
+ import (
+ "tailscale.com/tempfork/gliderlabs/ssh"
+ "io"
+ "log"
+ )
+
+ func main() {
+ ssh.Handle(func(s ssh.Session) {
+ io.WriteString(s, "Hello world\n")
+ })
+
+ log.Fatal(ssh.ListenAndServe(":2222", nil))
+ }
+
+```
+This package was built by [@progrium](https://twitter.com/progrium) after working on nearly a dozen projects at Glider Labs using SSH and collaborating with [@shazow](https://twitter.com/shazow) (known for [ssh-chat](https://github.com/shazow/ssh-chat)).
+
+## Examples
+
+A bunch of great examples are in the `_examples` directory.
+
+## Usage
+
+[See GoDoc reference.](https://godoc.org/tailscale.com/tempfork/gliderlabs/ssh)
+
+## Contributing
+
+Pull requests are welcome! However, since this project is very much about API
+design, please submit API changes as issues to discuss before submitting PRs.
+
+Also, you can [join our Slack](http://slack.gliderlabs.com) to discuss as well.
+
+## Roadmap
+
+* Non-session channel handlers
+* Cleanup callback API
+* 1.0 release
+* High-level client?
+
+## Sponsors
+
+Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/ssh#sponsor)]
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## License
+
+[BSD](LICENSE)
diff --git a/tempfork/gliderlabs/ssh/agent.go b/tempfork/gliderlabs/ssh/agent.go
new file mode 100644
index 000000000..f0ffb5b53
--- /dev/null
+++ b/tempfork/gliderlabs/ssh/agent.go
@@ -0,0 +1,83 @@
+package ssh
+
+import (
+ "io"
+ "io/ioutil"
+ "net"
+ "path"
+ "sync"
+
+ gossh "github.com/tailscale/golang-x-crypto/ssh"
+)
+
+const (
+ agentRequestType = "auth-agent-req@openssh.com"
+ agentChannelType = "auth-agent@openssh.com"
+
+ agentTempDir = "auth-agent"
+ agentListenFile = "listener.sock"
+)
+
+// contextKeyAgentRequest is an internal context key for storing if the
+// client requested agent forwarding
+var contextKeyAgentRequest = &contextKey{"auth-agent-req"}
+
+// SetAgentRequested sets up the session context so that AgentRequested
+// returns true.
+func SetAgentRequested(ctx Context) {
+ ctx.SetValue(contextKeyAgentRequest, true)
+}
+
+// AgentRequested returns true if the client requested agent forwarding.
+func AgentRequested(sess Session) bool {
+ return sess.Context().Value(contextKeyAgentRequest) == true
+}
+
+// NewAgentListener sets up a temporary Unix socket that can be communicated
+// to the session environment and used for forwarding connections.
+func NewAgentListener() (net.Listener, error) {
+ dir, err := ioutil.TempDir("", agentTempDir)
+ if err != nil {
+ return nil, err
+ }
+ l, err := net.Listen("unix", path.Join(dir, agentListenFile))
+ if err != nil {
+ return nil, err
+ }
+ return l, nil
+}
+
+// ForwardAgentConnections takes connections from a listener to proxy into the
+// session on the OpenSSH channel for agent connections. It blocks and services
+// connections until the listener stop accepting.
+func ForwardAgentConnections(l net.Listener, s Session) {
+ sshConn := s.Context().Value(ContextKeyConn).(gossh.Conn)
+ for {
+ conn, err := l.Accept()
+ if err != nil {
+ return
+ }
+ go func(conn net.Conn) {
+ defer conn.Close()
+ channel, reqs, err := sshConn.OpenChannel(agentChannelType, nil)
+ if err != nil {
+ return
+ }
+ defer channel.Close()
+ go gossh.DiscardRequests(reqs)
+ var wg sync.WaitGroup
+ wg.Add(2)
+ go func() {
+ io.Copy(conn, channel)
+ conn.(*net.UnixConn).CloseWrite()
+ wg.Done()
+ }()
+ go func() {
+ io.Copy(channel, conn)
+ channel.CloseWrite()
+ wg.Done()
+ }()
+ wg.Wait()
+ }(conn)
+ }
+}
diff --git a/tempfork/gliderlabs/ssh/conn.go b/tempfork/gliderlabs/ssh/conn.go
new file mode 100644
index 000000000..ebef8845b
--- /dev/null
+++ b/tempfork/gliderlabs/ssh/conn.go
@@ -0,0 +1,55 @@
+package ssh
+
+import (
+ "context"
+ "net"
+ "time"
+)
+
+type serverConn struct {
+ net.Conn
+
+ idleTimeout time.Duration
+ maxDeadline time.Time
+ closeCanceler context.CancelFunc
+}
+
+func (c *serverConn) Write(p []byte) (n int, err error) {
+ c.updateDeadline()
+ n, err = c.Conn.Write(p)
+ if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil {
+ c.closeCanceler()
+ }
+ return
+}
+
+func (c *serverConn) Read(b []byte) (n int, err error) {
+ c.updateDeadline()
+ n, err = c.Conn.Read(b)
+ if _, isNetErr := err.(net.Error); isNetErr && c.closeCanceler != nil {
+ c.closeCanceler()
+ }
+ return
+}
+
+func (c *serverConn) Close() (err error) {
+ err = c.Conn.Close()
+ if c.closeCanceler != nil {
+ c.closeCanceler()
+ }
+ return
+}
+
+func (c *serverConn) updateDeadline() {
+ switch {
+ case c.idleTimeout > 0:
+ idleDeadline := time.Now().Add(c.idleTimeout)
+ if idleDeadline.Unix() < c.maxDeadline.Unix() || c.maxDeadline.IsZero() {
+ c.Conn.SetDeadline(idleDeadline)
+ return
+ }
+ fallthrough
+ default:
+ c.Conn.SetDeadline(c.maxDeadline)
+ }
+}
diff --git a/tempfork/gliderlabs/ssh/context.go b/tempfork/gliderlabs/ssh/context.go
new file mode 100644
index 000000000..6715bcd4c
--- /dev/null
+++ b/tempfork/gliderlabs/ssh/context.go
@@ -0,0 +1,155 @@
+package ssh
+
+import (
+ "context"
+ "encoding/hex"
+ "net"
+ "sync"
+
+ gossh "github.com/tailscale/golang-x-crypto/ssh"
+)
+
+// contextKey is a value for use with context.WithValue. It's used as
+// a pointer so it fits in an interface{} without allocation.
+type contextKey struct {
+ name string
+}
+
+var (
+ // ContextKeyUser is a context key for use with Contexts in this package.
+ // The associated value will be of type string.
+ ContextKeyUser = &contextKey{"user"}
+
+ // ContextKeySessionID is a context key for use with Contexts in this package.
+ // The associated value will be of type string.
+ ContextKeySessionID = &contextKey{"session-id"}
+
+ // ContextKeyPermissions is a context key for use with Contexts in this package.
+ // The associated value will be of type *Permissions.
+ ContextKeyPermissions = &contextKey{"permissions"}
+
+ // ContextKeyClientVersion is a context key for use with Contexts in this package.
+ // The associated value will be of type string.
+ ContextKeyClientVersion = &contextKey{"client-version"}
+
+ // ContextKeyServerVersion is a context key for use with Contexts in this package.
+ // The associated value will be of type string.
+ ContextKeyServerVersion = &contextKey{"server-version"}
+
+ // ContextKeyLocalAddr is a context key for use with Contexts in this package.
+ // The associated value will be of type net.Addr.
+ ContextKeyLocalAddr = &contextKey{"local-addr"}
+
+ // ContextKeyRemoteAddr is a context key for use with Contexts in this package.
+ // The associated value will be of type net.Addr.
+ ContextKeyRemoteAddr = &contextKey{"remote-addr"}
+
+ // ContextKeyServer is a context key for use with Contexts in this package.
+ // The associated value will be of type *Server.
+ ContextKeyServer = &contextKey{"ssh-server"}
+
+ // ContextKeyConn is a context key for use with Contexts in this package.
+ // The associated value will be of type gossh.ServerConn.
+ ContextKeyConn = &contextKey{"ssh-conn"}
+
+ // ContextKeyPublicKey is a context key for use with Contexts in this package.
+ // The associated value will be of type PublicKey.
+ ContextKeyPublicKey = &contextKey{"public-key"}
+)
+
+// Context is a package specific context interface. It exposes connection
+// metadata and allows new values to be easily written to it. It's used in
+// authentication handlers and callbacks, and its underlying context.Context is
+// exposed on Session in the session Handler. A connection-scoped lock is also
+// embedded in the context to make it easier to limit operations per-connection.
+type Context interface {
+ context.Context
+ sync.Locker
+
+ // User returns the username used when establishing the SSH connection.
+ User() string
+
+ // SessionID returns the session hash.
+ SessionID() string
+
+ // ClientVersion returns the version reported by the client.
+ ClientVersion() string
+
+ // ServerVersion returns the version reported by the server.
+ ServerVersion() string
+
+ // RemoteAddr returns the remote address for this connection.
+ RemoteAddr() net.Addr
+
+ // LocalAddr returns the local address for this connection.
+ LocalAddr() net.Addr
+
+ // Permissions returns the Permissions object used for this connection.
+ Permissions() *Permissions
+
+ // SetValue allows you to easily write new values into the underlying context.
+ SetValue(key, value interface{})
+}
+
+type sshContext struct {
+ context.Context
+ *sync.Mutex
+}
+
+func newContext(srv *Server) (*sshContext, context.CancelFunc) {
+ innerCtx, cancel := context.WithCancel(context.Background())
+ ctx := &sshContext{innerCtx, &sync.Mutex{}}
+ ctx.SetValue(ContextKeyServer, srv)
+ perms := &Permissions{&gossh.Permissions{}}
+ ctx.SetValue(ContextKeyPermissions, perms)
+ return ctx, cancel
+}
+
+// this is separate from newContext because we will get ConnMetadata
+// at different points so it needs to be applied separately
+func applyConnMetadata(ctx Context, conn gossh.ConnMetadata) {
+ if ctx.Value(ContextKeySessionID) != nil {
+ return
+ }
+ ctx.SetValue(ContextKeySessionID, hex.EncodeToString(conn.SessionID()))
+ ctx.SetValue(ContextKeyClientVersion, string(conn.ClientVersion()))
+ ctx.SetValue(ContextKeyServerVersion, string(conn.ServerVersion()))
+ ctx.SetValue(ContextKeyUser, conn.User())
+ ctx.SetValue(ContextKeyLocalAddr, conn.LocalAddr())
+ ctx.SetValue(ContextKeyRemoteAddr, conn.RemoteAddr())
+}
+
+func (ctx *sshContext) SetValue(key, value interface{}) {
+ ctx.Context = context.WithValue(ctx.Context, key, value)
+}
+
+func (ctx *sshContext) User() string {
+ return ctx.Value(ContextKeyUser).(string)
+}
+
+func (ctx *sshContext) SessionID() string {
+ return ctx.Value(ContextKeySessionID).(string)
+}
+
+func (ctx *sshContext) ClientVersion() string {
+ return ctx.Value(ContextKeyClientVersion).(string)
+}
+
+func (ctx *sshContext) ServerVersion() string {
+ return ctx.Value(ContextKeyServerVersion).(string)
+}
+
+func (ctx *sshContext) RemoteAddr() net.Addr {
+ if addr, ok := ctx.Value(ContextKeyRemoteAddr).(net.Addr); ok {
+ return addr
+ }
+ return nil
+}
+
+func (ctx *sshContext) LocalAddr() net.Addr {
+ return ctx.Value(ContextKeyLocalAddr).(net.Addr)
+}
+
+func (ctx *sshContext) Permissions() *Permissions {
+ return ctx.Value(ContextKeyPermissions).(*Permissions)
+}
diff --git a/tempfork/gliderlabs/ssh/context_test.go b/tempfork/gliderlabs/ssh/context_test.go
new file mode 100644
index 000000000..d7c0b2406
--- /dev/null
+++ b/tempfork/gliderlabs/ssh/context_test.go
@@ -0,0 +1,50 @@
+//go:build glidertests
+// +build glidertests
+
+package ssh
+
+import "testing"
+
+func TestSetPermissions(t *testing.T) {
+ t.Parallel()
+ permsExt := map[string]string{
+ "foo": "bar",
+ }
+ session, _, cleanup := newTestSessionWithOptions(t, &Server{
+ Handler: func(s Session) {
+ if _, ok := s.Permissions().Extensions["foo"]; !ok {
+ t.Fatalf("got %#v; want %#v", s.Permissions().Extensions, permsExt)
+ }
+ },
+ }, nil, PasswordAuth(func(ctx Context, password string) bool {
+ ctx.Permissions().Extensions = permsExt
+ return true
+ }))
+ defer cleanup()
+ if err := session.Run(""); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestSetValue(t *testing.T) {
+ t.Parallel()
+ value := map[string]string{
+ "foo": "bar",
+ }
+ key := "testValue"
+ session, _, cleanup := newTestSessionWithOptions(t, &Server{
+ Handler: func(s Session) {
+ v := s.Context().Value(key).(map[string]string)
+ if v["foo"] != value["foo"] {
+ t.Fatalf("got %#v; want %#v", v, value)
+ }
+ },
+ }, nil, PasswordAuth(func(ctx Context, password string) bool {
+ ctx.SetValue(key, value)
+ return true
+ }))
+ defer cleanup()
+ if err := session.Run(""); err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/tempfork/gliderlabs/ssh/doc.go b/tempfork/gliderlabs/ssh/doc.go
new file mode 100644
index 000000000..5a10393c2
--- /dev/null
+++ b/tempfork/gliderlabs/ssh/doc.go
@@ -0,0 +1,45 @@
+/*
+Package ssh wraps the crypto/ssh package with a higher-level API for building
+SSH servers. The goal of the API was to make it as simple as using net/http, so
+the API is very similar.
+
+You should be able to build any SSH server using only this package, which wraps
+relevant types and some functions from crypto/ssh. However, you still need to
+use crypto/ssh for building SSH clients.
+
+ListenAndServe starts an SSH server with a given address, handler, and options. The
+handler is usually nil, which means to use DefaultHandler. Handle sets DefaultHandler:
+
+ ssh.Handle(func(s ssh.Session) {
+ io.WriteString(s, "Hello world\n")
+ })
+
+ log.Fatal(ssh.ListenAndServe(":2222", nil))
+
+If you don't specify a host key, it will generate one every time. This is convenient
+except you'll have to deal with clients being confused that the host key is different.
+It's a better idea to generate or point to an existing key on your system:
+
+ log.Fatal(ssh.ListenAndServe(":2222", nil, ssh.HostKeyFile("/Users/progrium/.ssh/id_rsa")))
+
+Although all options have functional option helpers, another way to control the
+server's behavior is by creating a custom Server:
+
+ s := &ssh.Server{
+ Addr: ":2222",
+ Handler: sessionHandler,
+ PublicKeyHandler: authHandler,
+ }
+ s.AddHostKey(hostKeySigner)
+
+ log.Fatal(s.ListenAndServe())
+
+This package automatically handles basic SSH requests like setting environment
+variables, requesting PTY, and changing window size. These requests are
+processed, responded to, and any relevant state is updated. This state is then
+exposed to you via the Session interface.
+
+The one big feature missing from the Session abstraction is signals. This was
+started, but not completed. Pull Requests welcome!
+*/
+package ssh
diff --git a/tempfork/gliderlabs/ssh/example_test.go b/tempfork/gliderlabs/ssh/example_test.go
new file mode 100644
index 000000000..1425d7a34
--- /dev/null
+++ b/tempfork/gliderlabs/ssh/example_test.go
@@ -0,0 +1,40 @@
+package ssh_test
+
+import (
+ "io"
+ "io/ioutil"
+
+ "tailscale.com/tempfork/gliderlabs/ssh"
+)
+
+func ExampleListenAndServe() {
+ ssh.ListenAndServe(":2222", func(s ssh.Session) {
+ io.WriteString(s, "Hello world\n")
+ })
+}
+
+func ExamplePasswordAuth() {
+ ssh.ListenAndServe(":2222", nil,
+ ssh.PasswordAuth(func(ctx ssh.Context, pass string) bool {
+ return pass == "secret"
+ }),
+ )
+}
+
+func ExampleNoPty() {
+ ssh.ListenAndServe(":2222", nil, ssh.NoPty())
+}
+
+func ExamplePublicKeyAuth() {
+ ssh.ListenAndServe(":2222", nil,
+ ssh.PublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) bool {
+ data, _ := ioutil.ReadFile("/path/to/allowed/key.pub")
+ allowed, _, _, _, _ := ssh.ParseAuthorizedKey(data)
+ return ssh.KeysEqual(key, allowed)
+ }),
+ )
+}
+
+func ExampleHostKeyFile() {
+ ssh.ListenAndServe(":2222", nil, ssh.HostKeyFile("/path/to/host/key"))
+}
diff --git a/tempfork/gliderlabs/ssh/options.go b/tempfork/gliderlabs/ssh/options.go
new file mode 100644
index 000000000..18ec45416
--- /dev/null
+++ b/tempfork/gliderlabs/ssh/options.go
@@ -0,0 +1,84 @@
+package ssh
+
+import (
+ "io/ioutil"
+
+ gossh "github.com/tailscale/golang-x-crypto/ssh"
+)
+
+// PasswordAuth returns a functional option that sets PasswordHandler on the server.
+func PasswordAuth(fn PasswordHandler) Option {
+ return func(srv *Server) error {
+ srv.PasswordHandler = fn
+ return nil
+ }
+}
+
+// PublicKeyAuth returns a functional option that sets PublicKeyHandler on the server.
+func PublicKeyAuth(fn PublicKeyHandler) Option {
+ return func(srv *Server) error {
+ srv.PublicKeyHandler = fn
+ return nil
+ }
+}
+
+// HostKeyFile returns a functional option that adds HostSigners to the server
+// from a PEM file at filepath.
+func HostKeyFile(filepath string) Option {
+ return func(srv *Server) error {
+ pemBytes, err := ioutil.ReadFile(filepath)
+ if err != nil {
+ return err
+ }
+
+ signer, err := gossh.ParsePrivateKey(pemBytes)
+ if err != nil {
+ return err
+ }
+
+ srv.AddHostKey(signer)
+
+ return nil
+ }
+}
+
+func KeyboardInteractiveAuth(fn KeyboardInteractiveHandler) Option {
+ return func(srv *Server) error {
+ srv.KeyboardInteractiveHandler = fn
+ return nil
+ }
+}
+
+// HostKeyPEM returns a functional option that adds HostSigners to the server
+// from a PEM file as bytes.
+func HostKeyPEM(bytes []byte) Option {
+ return func(srv *Server) error {
+ signer, err := gossh.ParsePrivateKey(bytes)
+ if err != nil {
+ return err
+ }
+
+ srv.AddHostKey(signer)
+
+ return nil
+ }
+}
+
+// NoPty returns a functional option that sets PtyCallback to return false,
+// denying PTY requests.
+func NoPty() Option {
+ return func(srv *Server) error {
+ srv.PtyCallback = func(ctx Context, pty Pty) bool {
+ return false
+ }
+ return nil
+ }
+}
+
+// WrapConn returns a functional option that sets ConnCallback on the server.
+func WrapConn(fn ConnCallback) Option {
+ return func(srv *Server) error {
+ srv.ConnCallback = fn
+ return nil
+ }
+}
diff --git a/tempfork/gliderlabs/ssh/options_test.go b/tempfork/gliderlabs/ssh/options_test.go
new file mode 100644
index 000000000..3d4415c38
--- /dev/null
+++ b/tempfork/gliderlabs/ssh/options_test.go
@@ -0,0 +1,112 @@
+//go:build glidertests
+// +build glidertests
+
+package ssh
+
+import (
+ "net"
+ "strings"
+ "sync/atomic"
+ "testing"
+
+ gossh "github.com/tailscale/golang-x-crypto/ssh"
+)
+
+func newTestSessionWithOptions(t *testing.T, srv *Server, cfg *gossh.ClientConfig, options ...Option) (*gossh.Session, *gossh.Client, func()) {
+ for _, option := range options {
+ if err := srv.SetOption(option); err != nil {
+ t.Fatal(err)
+ }
+ }
+ return newTestSession(t, srv, cfg)
+}
+
+func TestPasswordAuth(t *testing.T) {
+ t.Parallel()
+ testUser := "testuser"
+ testPass := "testpass"
+ session, _, cleanup := newTestSessionWithOptions(t, &Server{
+ Handler: func(s Session) {
+ // noop
+ },
+ }, &gossh.ClientConfig{
+ User: testUser,
+ Auth: []gossh.AuthMethod{
+ gossh.Password(testPass),
+ },
+ HostKeyCallback: gossh.InsecureIgnoreHostKey(),
+ }, PasswordAuth(func(ctx Context, password string) bool {
+ if ctx.User() != testUser {
+ t.Fatalf("user = %#v; want %#v", ctx.User(), testUser)
+ }
+ if password != testPass {
+ t.Fatalf("user = %#v; want %#v", password, testPass)
+ }
+ return true
+ }))
+ defer cleanup()
+ if err := session.Run(""); err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestPasswordAuthBadPass(t *testing.T) {
+ t.Parallel()
+ l := newLocalListener()
+ srv := &Server{Handler: func(s Session) {}}
+ srv.SetOption(PasswordAuth(func(ctx Context, password string) bool {
+ return false
+ }))
+ go srv.serveOnce(l)
+ _, err := gossh.Dial("tcp", l.Addr().String(), &gossh.ClientConfig{
+ User: "testuser",
+ Auth: []gossh.AuthMethod{
+ gossh.Password("testpass"),
+ },
+ HostKeyCallback: gossh.InsecureIgnoreHostKey(),
+ })
+ if err != nil {
+ if !strings.Contains(err.Error(), "unable to authenticate") {
+ t.Fatal(err)
+ }
+ }
+}
+
+type wrappedConn struct {
+ net.Conn
+ written int32
+}
+
+func (c *wrappedConn) Write(p []byte) (n int, err error) {
+ n, err = c.Conn.Write(p)
+ atomic.AddInt32(&(c.written), int32(n))
+ return
+}
+
+func TestConnWrapping(t *testing.T) {
+ t.Parallel()
+ var wrapped *wrappedConn
+ session, _, cleanup := newTestSessionWithOptions(t, &Server{
+ Handler: func(s Session) {
+ // nothing
+ },
+ }, &gossh.ClientConfig{
+ User: "testuser",
+ Auth: []gossh.AuthMethod{
+ gossh.Password("testpass"),
+ },
+ HostKeyCallback: gossh.InsecureIgnoreHostKey(),
+ }, PasswordAuth(func(ctx Context, password string) bool {
+ return true
+ }), WrapConn(func(ctx Context, conn net.Conn) net.Conn {
+ wrapped = &wrappedConn{conn, 0}
+ return wrapped
+ }))
+ defer cleanup()
+ if err := session.Shell(); err != nil {
+ t.Fatal(err)
+ }
+ if atomic.LoadInt32(&(wrapped.written)) == 0 {
+ t.Fatal("wrapped conn not written to")
+ }
+}
diff --git a/tempfork/gliderlabs/ssh/server.go b/tempfork/gliderlabs/ssh/server.go
new file mode 100644
index 000000000..934139e2c
--- /dev/null
+++ b/tempfork/gliderlabs/ssh/server.go
@@ -0,0 +1,449 @@
+package ssh
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "net"
+ "sync"
+ "time"
+
+ gossh "github.com/tailscale/golang-x-crypto/ssh"
+)
+
+// ErrServerClosed is returned by the Server's Serve, ListenAndServe,
+// and ListenAndServeTLS methods after a call to Shutdown or Close.
+var ErrServerClosed = errors.New("ssh: Server closed")
+
+type SubsystemHandler func(s Session)
+
+var DefaultSubsystemHandlers = map[string]SubsystemHandler{}
+
+type RequestHandler func(ctx Context, srv *Server, req *gossh.Request) (ok bool, payload []byte)
+
+var DefaultRequestHandlers = map[string]RequestHandler{}
+
+type ChannelHandler func(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context)
+
+var DefaultChannelHandlers = map[string]ChannelHandler{
+ "session": DefaultSessionHandler,
+}
+
+// Server defines parameters for running an SSH server. The zero value for
+// Server is a valid configuration. When both PasswordHandler and
+// PublicKeyHandler are nil, no client authentication is performed.
+type Server struct {
+ Addr string // TCP address to listen on, ":22" if empty
+ Handler Handler // handler to invoke, ssh.DefaultHandler if nil
+ HostSigners []Signer // private keys for the host key, must have at least one
+ Version string // server version to be sent before the initial handshake
+
+ KeyboardInteractiveHandler KeyboardInteractiveHandler // keyboard-interactive authentication handler
+ PasswordHandler PasswordHandler // password authentication handler
+ PublicKeyHandler PublicKeyHandler // public key authentication handler
+ PtyCallback PtyCallback // callback for allowing PTY sessions, allows all if nil
+ ConnCallback ConnCallback // optional callback for wrapping net.Conn before handling
+ LocalPortForwardingCallback LocalPortForwardingCallback // callback for allowing local port forwarding, denies all if nil
+ ReversePortForwardingCallback ReversePortForwardingCallback // callback for allowing reverse port forwarding, denies all if nil
+ ServerConfigCallback ServerConfigCallback // callback for configuring detailed SSH options
+ SessionRequestCallback SessionRequestCallback // callback for allowing or denying SSH sessions
+
+ ConnectionFailedCallback ConnectionFailedCallback // callback to report connection failures
+
+ IdleTimeout time.Duration // connection timeout when no activity, none if empty
+ MaxTimeout time.Duration // absolute connection timeout, none if empty
+
+ // ChannelHandlers allow overriding the built-in session handlers or provide
+ // extensions to the protocol, such as tcpip forwarding. By default only the
+ // "session" handler is enabled.
+ ChannelHandlers map[string]ChannelHandler
+
+ // RequestHandlers allow overriding the server-level request handlers or
+ // provide extensions to the protocol, such as tcpip forwarding. By default
+ // no handlers are enabled.
+ RequestHandlers map[string]RequestHandler
+
+ // SubsystemHandlers are handlers which are similar to the usual SSH command
+ // handlers, but handle named subsystems.
+ SubsystemHandlers map[string]SubsystemHandler
+
+ listenerWg sync.WaitGroup
+ mu sync.RWMutex
+ listeners map[net.Listener]struct{}
+ conns map[*gossh.ServerConn]struct{}
+ connWg sync.WaitGroup
+ doneChan chan struct{}
+}
+
+func (srv *Server) ensureHostSigner() error {
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+
+ if len(srv.HostSigners) == 0 {
+ signer, err := generateSigner()
+ if err != nil {
+ return err
+ }
+ srv.HostSigners = append(srv.HostSigners, signer)
+ }
+ return nil
+}
+
+func (srv *Server) ensureHandlers() {
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+
+ if srv.RequestHandlers == nil {
+ srv.RequestHandlers = map[string]RequestHandler{}
+ for k, v := range DefaultRequestHandlers {
+ srv.RequestHandlers[k] = v
+ }
+ }
+ if srv.ChannelHandlers == nil {
+ srv.ChannelHandlers = map[string]ChannelHandler{}
+ for k, v := range DefaultChannelHandlers {
+ srv.ChannelHandlers[k] = v
+ }
+ }
+ if srv.SubsystemHandlers == nil {
+ srv.SubsystemHandlers = map[string]SubsystemHandler{}
+ for k, v := range DefaultSubsystemHandlers {
+ srv.SubsystemHandlers[k] = v
+ }
+ }
+}
+
+func (srv *Server) config(ctx Context) *gossh.ServerConfig {
+ srv.mu.RLock()
+ defer srv.mu.RUnlock()
+
+ var config *gossh.ServerConfig
+ if srv.ServerConfigCallback == nil {
+ config = &gossh.ServerConfig{}
+ } else {
+ config = srv.ServerConfigCallback(ctx)
+ }
+ for _, signer := range srv.HostSigners {
+ config.AddHostKey(signer)
+ }
+ if srv.PasswordHandler == nil && srv.PublicKeyHandler == nil && srv.KeyboardInteractiveHandler == nil {
+ config.NoClientAuth = true
+ }
+ if srv.Version != "" {
+ config.ServerVersion = "SSH-2.0-" + srv.Version
+ }
+ if srv.PasswordHandler != nil {
+ config.PasswordCallback = func(conn gossh.ConnMetadata, password []byte) (*gossh.Permissions, error) {
+ applyConnMetadata(ctx, conn)
+ if ok := srv.PasswordHandler(ctx, string(password)); !ok {
+ return ctx.Permissions().Permissions, fmt.Errorf("permission denied")
+ }
+ return ctx.Permissions().Permissions, nil
+ }
+ }
+ if srv.PublicKeyHandler != nil {
+ config.PublicKeyCallback = func(conn gossh.ConnMetadata, key gossh.PublicKey) (*gossh.Permissions, error) {
+ applyConnMetadata(ctx, conn)
+ if ok := srv.PublicKeyHandler(ctx, key); !ok {
+ return ctx.Permissions().Permissions, fmt.Errorf("permission denied")
+ }
+ ctx.SetValue(ContextKeyPublicKey, key)
+ return ctx.Permissions().Permissions, nil
+ }
+ }
+ if srv.KeyboardInteractiveHandler != nil {
+ config.KeyboardInteractiveCallback = func(conn gossh.ConnMetadata, challenger gossh.KeyboardInteractiveChallenge) (*gossh.Permissions, error) {
+ applyConnMetadata(ctx, conn)
+ if ok := srv.KeyboardInteractiveHandler(ctx, challenger); !ok {
+ return ctx.Permissions().Permissions, fmt.Errorf("permission denied")
+ }
+ return ctx.Permissions().Permissions, nil
+ }
+ }
+ return config
+}
+
+// Handle sets the Handler for the server.
+func (srv *Server) Handle(fn Handler) {
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+
+ srv.Handler = fn
+}
+
+// Close immediately closes all active listeners and all active
+// connections.
+//
+// Close returns any error returned from closing the Server's
+// underlying Listener(s).
+func (srv *Server) Close() error {
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+
+ srv.closeDoneChanLocked()
+ err := srv.closeListenersLocked()
+ for c := range srv.conns {
+ c.Close()
+ delete(srv.conns, c)
+ }
+ return err
+}
+
+// Shutdown gracefully shuts down the server without interrupting any
+// active connections. Shutdown works by first closing all open
+// listeners, and then waiting indefinitely for connections to close.
+// If the provided context expires before the shutdown is complete,
+// then the context's error is returned.
+func (srv *Server) Shutdown(ctx context.Context) error {
+ srv.mu.Lock()
+ lnerr := srv.closeListenersLocked()
+ srv.closeDoneChanLocked()
+ srv.mu.Unlock()
+
+ finished := make(chan struct{}, 1)
+ go func() {
+ srv.listenerWg.Wait()
+ srv.connWg.Wait()
+ finished <- struct{}{}
+ }()
+
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-finished:
+ return lnerr
+ }
+}
+
+// Serve accepts incoming connections on the Listener l, creating a new
+// connection goroutine for each. The connection goroutines read requests and then
+// calls srv.Handler to handle sessions.
+//
+// Serve always returns a non-nil error.
+func (srv *Server) Serve(l net.Listener) error {
+ srv.ensureHandlers()
+ defer l.Close()
+ if err := srv.ensureHostSigner(); err != nil {
+ return err
+ }
+ if srv.Handler == nil {
+ srv.Handler = DefaultHandler
+ }
+ var tempDelay time.Duration
+
+ srv.trackListener(l, true)
+ defer srv.trackListener(l, false)
+ for {
+ conn, e := l.Accept()
+ if e != nil {
+ select {
+ case <-srv.getDoneChan():
+ return ErrServerClosed
+ default:
+ }
+ if ne, ok := e.(net.Error); ok && ne.Temporary() {
+ if tempDelay == 0 {
+ tempDelay = 5 * time.Millisecond
+ } else {
+ tempDelay *= 2
+ }
+ if max := 1 * time.Second; tempDelay > max {
+ tempDelay = max
+ }
+ time.Sleep(tempDelay)
+ continue
+ }
+ return e
+ }
+ go srv.HandleConn(conn)
+ }
+}
+
+func (srv *Server) HandleConn(newConn net.Conn) {
+ ctx, cancel := newContext(srv)
+ if srv.ConnCallback != nil {
+ cbConn := srv.ConnCallback(ctx, newConn)
+ if cbConn == nil {
+ newConn.Close()
+ return
+ }
+ newConn = cbConn
+ }
+ conn := &serverConn{
+ Conn: newConn,
+ idleTimeout: srv.IdleTimeout,
+ closeCanceler: cancel,
+ }
+ if srv.MaxTimeout > 0 {
+ conn.maxDeadline = time.Now().Add(srv.MaxTimeout)
+ }
+ defer conn.Close()
+ sshConn, chans, reqs, err := gossh.NewServerConn(conn, srv.config(ctx))
+ if err != nil {
+ if srv.ConnectionFailedCallback != nil {
+ srv.ConnectionFailedCallback(conn, err)
+ }
+ return
+ }
+
+ srv.trackConn(sshConn, true)
+ defer srv.trackConn(sshConn, false)
+
+ ctx.SetValue(ContextKeyConn, sshConn)
+ applyConnMetadata(ctx, sshConn)
+ //go gossh.DiscardRequests(reqs)
+ go srv.handleRequests(ctx, reqs)
+ for ch := range chans {
+ handler := srv.ChannelHandlers[ch.ChannelType()]
+ if handler == nil {
+ handler = srv.ChannelHandlers["default"]
+ }
+ if handler == nil {
+ ch.Reject(gossh.UnknownChannelType, "unsupported channel type")
+ continue
+ }
+ go handler(srv, sshConn, ch, ctx)
+ }
+}
+
+func (srv *Server) handleRequests(ctx Context, in <-chan *gossh.Request) {
+ for req := range in {
+ handler := srv.RequestHandlers[req.Type]
+ if handler == nil {
+ handler = srv.RequestHandlers["default"]
+ }
+ if handler == nil {
+ req.Reply(false, nil)
+ continue
+ }
+ /*reqCtx, cancel := context.WithCancel(ctx)
+ defer cancel() */
+ ret, payload := handler(ctx, srv, req)
+ req.Reply(ret, payload)
+ }
+}
+
+// ListenAndServe listens on the TCP network address srv.Addr and then calls
+// Serve to handle incoming connections. If srv.Addr is blank, ":22" is used.
+// ListenAndServe always returns a non-nil error.
+func (srv *Server) ListenAndServe() error {
+ addr := srv.Addr
+ if addr == "" {
+ addr = ":22"
+ }
+ ln, err := net.Listen("tcp", addr)
+ if err != nil {
+ return err
+ }
+ return srv.Serve(ln)
+}
+
+// AddHostKey adds a private key as a host key. If an existing host key exists
+// with the same algorithm, it is overwritten. Each server config must have at
+// least one host key.
+func (srv *Server) AddHostKey(key Signer) {
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+
+ // these are later added via AddHostKey on ServerConfig, which performs the
+ // check for one of every algorithm.
+
+ // This check is based on the AddHostKey method from the x/crypto/ssh
+ // library. This allows us to only keep one active key for each type on a
+ // server at once. So, if you're dynamically updating keys at runtime, this
+ // list will not keep growing.
+ for i, k := range srv.HostSigners {
+ if k.PublicKey().Type() == key.PublicKey().Type() {
+ srv.HostSigners[i] = key
+ return
+ }
+ }
+
+ srv.HostSigners = append(srv.HostSigners, key)
+}
+
+// SetOption runs a functional option against the server.
+func (srv *Server) SetOption(option Option) error {
+ // NOTE: there is a potential race here for any option that doesn't call an
+ // internal method. We can't actually lock here because if something calls
+ // (as an example) AddHostKey, it will deadlock.
+
+ //srv.mu.Lock()
+ //defer srv.mu.Unlock()
+
+ return option(srv)
+}
+
+func (srv *Server) getDoneChan() <-chan struct{} {
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+
+ return srv.getDoneChanLocked()
+}
+
+func (srv *Server) getDoneChanLocked() chan struct{} {
+ if srv.doneChan == nil {
+ srv.doneChan = make(chan struct{})
+ }
+ return srv.doneChan
+}
+
+func (srv *Server) closeDoneChanLocked() {
+ ch := srv.getDoneChanLocked()
+ select {
+ case <-ch:
+ // Already closed. Don't close again.
+ default:
+ // Safe to close here. We're the only closer, guarded
+ // by srv.mu.
+ close(ch)
+ }
+}
+
+func (srv *Server) closeListenersLocked() error {
+ var err error
+ for ln := range srv.listeners {
+ if cerr := ln.Close(); cerr != nil && err == nil {
+ err = cerr
+ }
+ delete(srv.listeners, ln)
+ }
+ return err
+}
+
+func (srv *Server) trackListener(ln net.Listener, add bool) {
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+
+ if srv.listeners == nil {
+ srv.listeners = make(map[net.Listener]struct{})
+ }
+ if add {
+ // If the *Server is being reused after a previous
+ // Close or Shutdown, reset its doneChan:
+ if len(srv.listeners) == 0 && len(srv.conns) == 0 {
+ srv.doneChan = nil
+ }
+ srv.listeners[ln] = struct{}{}
+ srv.listenerWg.Add(1)
+ } else {
+ delete(srv.listeners, ln)
+ srv.listenerWg.Done()
+ }
+}
+
+func (srv *Server) trackConn(c *gossh.ServerConn, add bool) {
+ srv.mu.Lock()
+ defer srv.mu.Unlock()
+
+ if srv.conns == nil {
+ srv.conns = make(map[*gossh.ServerConn]struct{})
+ }
+ if add {
+ srv.conns[c] = struct{}{}
+ srv.connWg.Add(1)
+ } else {
+ delete(srv.conns, c)
+ srv.connWg.Done()
+ }
+}
diff --git a/tempfork/gliderlabs/ssh/server_test.go b/tempfork/gliderlabs/ssh/server_test.go
new file mode 100644
index 000000000..113f477c8
--- /dev/null
+++ b/tempfork/gliderlabs/ssh/server_test.go
@@ -0,0 +1,129 @@
+//go:build glidertests
+// +build glidertests
+
+package ssh
+
+import (
+ "bytes"
+ "context"
+ "io"
+ "testing"
+ "time"
+)
+
+func TestAddHostKey(t *testing.T) {
+ s := Server{}
+ signer, err := generateSigner()
+ if err != nil {
+ t.Fatal(err)
+ }
+ s.AddHostKey(signer)
+ if len(s.HostSigners) != 1 {
+ t.Fatal("Key was not properly added")
+ }
+ signer, err = generateSigner()
+ if err != nil {
+ t.Fatal(err)
+ }
+ s.AddHostKey(signer)
+ if len(s.HostSigners) != 1 {
+ t.Fatal("Key was not properly replaced")
+ }
+}
+
+func TestServerShutdown(t *testing.T) {
+ l := newLocalListener()
+ testBytes := []byte("Hello world\n")
+ s := &Server{
+ Handler: func(s Session) {
+ s.Write(testBytes)
+ time.Sleep(50 * time.Millisecond)
+ },
+ }
+ go func() {
+ err := s.Serve(l)
+ if err != nil && err != ErrServerClosed {
+ t.Fatal(err)
+ }
+ }()
+ sessDone := make(chan struct{})
+ sess, _, cleanup := newClientSession(t, l.Addr().String(), nil)
+ go func() {
+ defer cleanup()
+ defer close(sessDone)
+ var stdout bytes.Buffer
+ sess.Stdout = &stdout
+ if err := sess.Run(""); err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(stdout.Bytes(), testBytes) {
+ t.Fatalf("expected = %s; got %s", testBytes, stdout.Bytes())
+ }
+ }()
+
+ srvDone := make(chan struct{})
+ go func() {
+ defer close(srvDone)
+ err := s.Shutdown(context.Background())
+ if err != nil {
+ t.Fatal(err)
+ }
+ }()
+
+ timeout := time.After(2 * time.Second)
+ select {
+ case <-timeout:
+ t.Fatal("timeout")
+ return
+ case <-srvDone:
+ // TODO: add timeout for sessDone
+ <-sessDone
+ return
+ }
+}
+
+func TestServerClose(t *testing.T) {
+ l := newLocalListener()
+ s := &Server{
+ Handler: func(s Session) {
+ time.Sleep(5 * time.Second)
+ },
+ }
+ go func() {
+ err := s.Serve(l)
+ if err != nil && err != ErrServerClosed {
+ t.Fatal(err)
+ }
+ }()
+
+ clientDoneChan := make(chan struct{})
+ closeDoneChan := make(chan struct{})
+
+ sess, _, cleanup := newClientSession(t, l.Addr().String(), nil)
+ go func() {
+ defer cleanup()
+ defer close(clientDoneChan)
+ <-closeDoneChan
+ if err := sess.Run(""); err != nil && err != io.EOF {
+ t.Fatal(err)
+ }
+ }()
+
+ go func() {
+ err := s.Close()
+ if err != nil {
+ t.Fatal(err)
+ }
+ close(closeDoneChan)
+ }()
+
+ timeout := time.After(100 * time.Millisecond)
+ select {
+ case <-timeout:
+ t.Error("timeout")
+ return
+ case <-s.getDoneChan():
+ <-clientDoneChan
+ return
+ }
+}
diff --git a/tempfork/gliderlabs/ssh/session.go b/tempfork/gliderlabs/ssh/session.go
new file mode 100644
index 000000000..0a4a21e53
--- /dev/null
+++ b/tempfork/gliderlabs/ssh/session.go
@@ -0,0 +1,386 @@
+package ssh
+
+import (
+ "bytes"
+ "context"
+ "errors"
+ "fmt"
+ "net"
+ "sync"
+
+ "github.com/anmitsu/go-shlex"
+ gossh "github.com/tailscale/golang-x-crypto/ssh"
+)
+
+// Session provides access to information about an SSH session and methods
+// to read and write to the SSH channel with an embedded Channel interface from
+// crypto/ssh.
+//
+// When Command() returns an empty slice, the user requested a shell. Otherwise
+// the user is performing an exec with those command arguments.
+//
+// TODO: Signals
+type Session interface {
+ gossh.Channel
+
+ // User returns the username used when establishing the SSH connection.
+ User() string
+
+ // RemoteAddr returns the net.Addr of the client side of the connection.
+ RemoteAddr() net.Addr
+
+ // LocalAddr returns the net.Addr of the server side of the connection.
+ LocalAddr() net.Addr
+
+ // Environ returns a copy of strings representing the environment set by the
+ // user for this session, in the form "key=value".
+ Environ() []string
+
+ // Exit sends an exit status and then closes the session.
+ Exit(code int) error
+
+ // Command returns a shell parsed slice of arguments that were provided by the
+ // user. Shell parsing splits the command string according to POSIX shell rules,
+ // which considers quoting not just whitespace.
+ Command() []string
+
+ // RawCommand returns the exact command that was provided by the user.
+ RawCommand() string
+
+ // Subsystem returns the subsystem requested by the user.
+ Subsystem() string
+
+ // PublicKey returns the PublicKey used to authenticate. If a public key was not
+ // used it will return nil.
+ PublicKey() PublicKey
+
+ // Context returns the connection's context. The returned context is always
+ // non-nil and holds the same data as the Context passed into auth
+ // handlers and callbacks.
+ //
+ // The context is canceled when the client's connection closes or I/O
+ // operation fails.
+ Context() context.Context
+
+ // Permissions returns a copy of the Permissions object that was available for
+ // setup in the auth handlers via the Context.
+ Permissions() Permissions
+
+ // Pty returns PTY information, a channel of window size changes, and a boolean
+ // of whether or not a PTY was accepted for this session.
+ Pty() (Pty, <-chan Window, bool)
+
+ // Signals registers a channel to receive signals sent from the client. The
+ // channel must handle signal sends or it will block the SSH request loop.
+ // Registering nil will unregister the channel from signal sends. During the
+ // time no channel is registered signals are buffered up to a reasonable amount.
+ // If there are buffered signals when a channel is registered, they will be
+ // sent in order on the channel immediately after registering.
+ Signals(c chan<- Signal)
+
+ // Break regisers a channel to receive notifications of break requests sent
+ // from the client. The channel must handle break requests, or it will block
+ // the request handling loop. Registering nil will unregister the channel.
+ // During the time that no channel is registered, breaks are ignored.
+ Break(c chan<- bool)
+
+ // DisablePTYEmulation disables the session's default minimal PTY emulation.
+ // If you're setting the pty's termios settings from the Pty request, use
+ // this method to avoid corruption.
+ // Currently (2022-03-12) the only emulation implemented is NL-to-CRNL translation (`\n`=>`\r\n`).
+ // A call of DisablePTYEmulation must precede any call to Write.
+ DisablePTYEmulation()
+}
+
+// maxSigBufSize is how many signals will be buffered
+// when there is no signal channel specified
+const maxSigBufSize = 128
+
+func DefaultSessionHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context) {
+ ch, reqs, err := newChan.Accept()
+ if err != nil {
+ // TODO: trigger event callback
+ return
+ }
+ sess := &session{
+ Channel: ch,
+ conn: conn,
+ handler: srv.Handler,
+ ptyCb: srv.PtyCallback,
+ sessReqCb: srv.SessionRequestCallback,
+ subsystemHandlers: srv.SubsystemHandlers,
+ ctx: ctx,
+ }
+ sess.handleRequests(reqs)
+}
+
+type session struct {
+ sync.Mutex
+ gossh.Channel
+ conn *gossh.ServerConn
+ handler Handler
+ subsystemHandlers map[string]SubsystemHandler
+ handled bool
+ exited bool
+ pty *Pty
+ winch chan Window
+ env []string
+ ptyCb PtyCallback
+ sessReqCb SessionRequestCallback
+ rawCmd string
+ subsystem string
+ ctx Context
+ sigCh chan<- Signal
+ sigBuf []Signal
+ breakCh chan<- bool
+ disablePtyEmulation bool
+}
+
+func (sess *session) DisablePTYEmulation() {
+ sess.disablePtyEmulation = true
+}
+
+func (sess *session) Write(p []byte) (n int, err error) {
+ if sess.pty != nil && !sess.disablePtyEmulation {
+ m := len(p)
+ // normalize \n to \r\n when pty is accepted.
+ // this is a hardcoded shortcut since we don't support terminal modes.
+ p = bytes.Replace(p, []byte{'\n'}, []byte{'\r', '\n'}, -1)
+ p = bytes.Replace(p, []byte{'\r', '\r', '\n'}, []byte{'\r', '\n'}, -1)
+ n, err = sess.Channel.Write(p)
+ if n > m {
+ n = m
+ }
+ return
+ }
+ return sess.Channel.Write(p)
+}
+
+func (sess *session) PublicKey() PublicKey {
+ sessionkey := sess.ctx.Value(ContextKeyPublicKey)
+ if sessionkey == nil {
+ return nil
+ }
+ return sessionkey.(PublicKey)
+}
+
+func (sess *session) Permissions() Permissions {
+ // use context permissions because its properly
+ // wrapped and easier to dereference
+ perms := sess.ctx.Value(ContextKeyPermissions).(*Permissions)
+ return *perms
+}
+
+func (sess *session) Context() context.Context {
+ return sess.ctx
+}
+
+func (sess *session) Exit(code int) error {
+ sess.Lock()
+ defer sess.Unlock()
+ if sess.exited {
+ return errors.New("Session.Exit called multiple times")
+ }
+ sess.exited = true
+
+ status := struct{ Status uint32 }{uint32(code)}
+ _, err := sess.SendRequest("exit-status", false, gossh.Marshal(&status))
+ if err != nil {
+ return err
+ }
+ return sess.Close()
+}
+
+func (sess *session) User() string {
+ return sess.conn.User()
+}
+
+func (sess *session) RemoteAddr() net.Addr {
+ return sess.conn.RemoteAddr()
+}
+
+func (sess *session) LocalAddr() net.Addr {
+ return sess.conn.LocalAddr()
+}
+
+func (sess *session) Environ() []string {
+ return append([]string(nil), sess.env...)
+}
+
+func (sess *session) RawCommand() string {
+ return sess.rawCmd
+}
+
+func (sess *session) Command() []string {
+ cmd, _ := shlex.Split(sess.rawCmd, true)
+ return append([]string(nil), cmd...)
+}
+
+func (sess *session) Subsystem() string {
+ return sess.subsystem
+}
+
+func (sess *session) Pty() (Pty, <-chan Window, bool) {
+ if sess.pty != nil {
+ return *sess.pty, sess.winch, true
+ }
+ return Pty{}, sess.winch, false
+}
+
+func (sess *session) Signals(c chan<- Signal) {
+ sess.Lock()
+ defer sess.Unlock()
+ sess.sigCh = c
+ if len(sess.sigBuf) > 0 {
+ go func() {
+ for _, sig := range sess.sigBuf {
+ sess.sigCh <- sig
+ }
+ }()
+ }
+}
+
+func (sess *session) Break(c chan<- bool) {
+ sess.Lock()
+ defer sess.Unlock()
+ sess.breakCh = c
+}
+
+func (sess *session) handleRequests(reqs <-chan *gossh.Request) {
+ for req := range reqs {
+ switch req.Type {
+ case "shell", "exec":
+ if sess.handled {
+ req.Reply(false, nil)
+ continue
+ }
+
+ var payload = struct{ Value string }{}
+ gossh.Unmarshal(req.Payload, &payload)
+ sess.rawCmd = payload.Value
+
+ // If there's a session policy callback, we need to confirm before
+ // accepting the session.
+ if sess.sessReqCb != nil && !sess.sessReqCb(sess, req.Type) {
+ sess.rawCmd = ""
+ req.Reply(false, nil)
+ continue
+ }
+
+ sess.handled = true
+ req.Reply(true, nil)
+
+ go func() {
+ sess.handler(sess)
+ sess.Exit(0)
+ }()
+ case "subsystem":
+ if sess.handled {
+ req.Reply(false, nil)
+ continue
+ }
+
+ var payload = struct{ Value string }{}
+ gossh.Unmarshal(req.Payload, &payload)
+ sess.subsystem = payload.Value
+
+ // If there's a session policy callback, we need to confirm before
+ // accepting the session.
+ if sess.sessReqCb != nil && !sess.sessReqCb(sess, req.Type) {
+ sess.rawCmd = ""
+ req.Reply(false, nil)
+ continue
+ }
+
+ handler := sess.subsystemHandlers[payload.Value]
+ if handler == nil {
+ handler = sess.subsystemHandlers["default"]
+ }
+ if handler == nil {
+ req.Reply(false, nil)
+ continue
+ }
+
+ sess.handled = true
+ req.Reply(true, nil)
+
+ go func() {
+ handler(sess)
+ sess.Exit(0)
+ }()
+ case "env":
+ if sess.handled {
+ req.Reply(false, nil)
+ continue
+ }
+ var kv struct{ Key, Value string }
+ gossh.Unmarshal(req.Payload, &kv)
+ sess.env = append(sess.env, fmt.Sprintf("%s=%s", kv.Key, kv.Value))
+ req.Reply(true, nil)
+ case "signal":
+ var payload struct{ Signal string }
+ gossh.Unmarshal(req.Payload, &payload)
+ sess.Lock()
+ if sess.sigCh != nil {
+ sess.sigCh <- Signal(payload.Signal)
+ } else {
+ if len(sess.sigBuf) < maxSigBufSize {
+ sess.sigBuf = append(sess.sigBuf, Signal(payload.Signal))
+ }
+ }
+ sess.Unlock()
+ case "pty-req":
+ if sess.handled || sess.pty != nil {
+ req.Reply(false, nil)
+ continue
+ }
+ ptyReq, ok := parsePtyRequest(req.Payload)
+ if !ok {
+ req.Reply(false, nil)
+ continue
+ }
+ if sess.ptyCb != nil {
+ ok := sess.ptyCb(sess.ctx, ptyReq)
+ if !ok {
+ req.Reply(false, nil)
+ continue
+ }
+ }
+ sess.pty = &ptyReq
+ sess.winch = make(chan Window, 1)
+ sess.winch <- ptyReq.Window
+ defer func() {
+ // when reqs is closed
+ close(sess.winch)
+ }()
+ req.Reply(ok, nil)
+ case "window-change":
+ if sess.pty == nil {
+ req.Reply(false, nil)
+ continue
+ }
+ win, _, ok := parseWindow(req.Payload)
+ if ok {
+ sess.pty.Window = win
+ sess.winch <- win
+ }
+ req.Reply(ok, nil)
+ case agentRequestType:
+ // TODO: option/callback to allow agent forwarding
+ SetAgentRequested(sess.ctx)
+ req.Reply(true, nil)
+ case "break":
+ ok := false
+ sess.Lock()
+ if sess.breakCh != nil {
+ sess.breakCh <- true
+ ok = true
+ }
+ req.Reply(ok, nil)
+ sess.Unlock()
+ default:
+ // TODO: debug log
+ req.Reply(false, nil)
+ }
+ }
+}
diff --git a/tempfork/gliderlabs/ssh/session_test.go b/tempfork/gliderlabs/ssh/session_test.go
new file mode 100644
index 000000000..2a915c319
--- /dev/null
+++ b/tempfork/gliderlabs/ssh/session_test.go
@@ -0,0 +1,441 @@
+//go:build glidertests
+// +build glidertests
+
+package ssh
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "net"
+ "testing"
+
+ gossh "github.com/tailscale/golang-x-crypto/ssh"
+)
+
+func (srv *Server) serveOnce(l net.Listener) error {
+ srv.ensureHandlers()
+ if err := srv.ensureHostSigner(); err != nil {
+ return err
+ }
+ conn, e := l.Accept()
+ if e != nil {
+ return e
+ }
+ srv.ChannelHandlers = map[string]ChannelHandler{
+ "session": DefaultSessionHandler,
+ "direct-tcpip": DirectTCPIPHandler,
+ }
+ srv.HandleConn(conn)
+ return nil
+}
+
+func newLocalListener() net.Listener {
+ l, err := net.Listen("tcp", "127.0.0.1:0")
+ if err != nil {
+ if l, err = net.Listen("tcp6", "[::1]:0"); err != nil {
+ panic(fmt.Sprintf("failed to listen on a port: %v", err))
+ }
+ }
+ return l
+}
+
+func newClientSession(t *testing.T, addr string, config *gossh.ClientConfig) (*gossh.Session, *gossh.Client, func()) {
+ if config == nil {
+ config = &gossh.ClientConfig{
+ User: "testuser",
+ Auth: []gossh.AuthMethod{
+ gossh.Password("testpass"),
+ },
+ }
+ }
+ if config.HostKeyCallback == nil {
+ config.HostKeyCallback = gossh.InsecureIgnoreHostKey()
+ }
+ client, err := gossh.Dial("tcp", addr, config)
+ if err != nil {
+ t.Fatal(err)
+ }
+ session, err := client.NewSession()
+ if err != nil {
+ t.Fatal(err)
+ }
+ return session, client, func() {
+ session.Close()
+ client.Close()
+ }
+}
+
+func newTestSession(t *testing.T, srv *Server, cfg *gossh.ClientConfig) (*gossh.Session, *gossh.Client, func()) {
+ l := newLocalListener()
+ go srv.serveOnce(l)
+ return newClientSession(t, l.Addr().String(), cfg)
+}
+
+func TestStdout(t *testing.T) {
+ t.Parallel()
+ testBytes := []byte("Hello world\n")
+ session, _, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {
+ s.Write(testBytes)
+ },
+ }, nil)
+ defer cleanup()
+ var stdout bytes.Buffer
+ session.Stdout = &stdout
+ if err := session.Run(""); err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(stdout.Bytes(), testBytes) {
+ t.Fatalf("stdout = %#v; want %#v", stdout.Bytes(), testBytes)
+ }
+}
+
+func TestStderr(t *testing.T) {
+ t.Parallel()
+ testBytes := []byte("Hello world\n")
+ session, _, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {
+ s.Stderr().Write(testBytes)
+ },
+ }, nil)
+ defer cleanup()
+ var stderr bytes.Buffer
+ session.Stderr = &stderr
+ if err := session.Run(""); err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(stderr.Bytes(), testBytes) {
+ t.Fatalf("stderr = %#v; want %#v", stderr.Bytes(), testBytes)
+ }
+}
+
+func TestStdin(t *testing.T) {
+ t.Parallel()
+ testBytes := []byte("Hello world\n")
+ session, _, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {
+ io.Copy(s, s) // stdin back into stdout
+ },
+ }, nil)
+ defer cleanup()
+ var stdout bytes.Buffer
+ session.Stdout = &stdout
+ session.Stdin = bytes.NewBuffer(testBytes)
+ if err := session.Run(""); err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(stdout.Bytes(), testBytes) {
+ t.Fatalf("stdout = %#v; want %#v given stdin = %#v", stdout.Bytes(), testBytes, testBytes)
+ }
+}
+
+func TestUser(t *testing.T) {
+ t.Parallel()
+ testUser := []byte("progrium")
+ session, _, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {
+ io.WriteString(s, s.User())
+ },
+ }, &gossh.ClientConfig{
+ User: string(testUser),
+ })
+ defer cleanup()
+ var stdout bytes.Buffer
+ session.Stdout = &stdout
+ if err := session.Run(""); err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(stdout.Bytes(), testUser) {
+ t.Fatalf("stdout = %#v; want %#v given user = %#v", stdout.Bytes(), testUser, string(testUser))
+ }
+}
+
+func TestDefaultExitStatusZero(t *testing.T) {
+ t.Parallel()
+ session, _, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {
+ // noop
+ },
+ }, nil)
+ defer cleanup()
+ err := session.Run("")
+ if err != nil {
+ t.Fatalf("expected nil but got %v", err)
+ }
+}
+
+func TestExplicitExitStatusZero(t *testing.T) {
+ t.Parallel()
+ session, _, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {
+ s.Exit(0)
+ },
+ }, nil)
+ defer cleanup()
+ err := session.Run("")
+ if err != nil {
+ t.Fatalf("expected nil but got %v", err)
+ }
+}
+
+func TestExitStatusNonZero(t *testing.T) {
+ t.Parallel()
+ session, _, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {
+ s.Exit(1)
+ },
+ }, nil)
+ defer cleanup()
+ err := session.Run("")
+ e, ok := err.(*gossh.ExitError)
+ if !ok {
+ t.Fatalf("expected ExitError but got %T", err)
+ }
+ if e.ExitStatus() != 1 {
+ t.Fatalf("exit-status = %#v; want %#v", e.ExitStatus(), 1)
+ }
+}
+
+func TestPty(t *testing.T) {
+ t.Parallel()
+ term := "xterm"
+ winWidth := 40
+ winHeight := 80
+ done := make(chan bool)
+ session, _, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {
+ ptyReq, _, isPty := s.Pty()
+ if !isPty {
+ t.Fatalf("expected pty but none requested")
+ }
+ if ptyReq.Term != term {
+ t.Fatalf("expected term %#v but got %#v", term, ptyReq.Term)
+ }
+ if ptyReq.Window.Width != winWidth {
+ t.Fatalf("expected window width %#v but got %#v", winWidth, ptyReq.Window.Width)
+ }
+ if ptyReq.Window.Height != winHeight {
+ t.Fatalf("expected window height %#v but got %#v", winHeight, ptyReq.Window.Height)
+ }
+ close(done)
+ },
+ }, nil)
+ defer cleanup()
+ if err := session.RequestPty(term, winHeight, winWidth, gossh.TerminalModes{}); err != nil {
+ t.Fatalf("expected nil but got %v", err)
+ }
+ if err := session.Shell(); err != nil {
+ t.Fatalf("expected nil but got %v", err)
+ }
+ <-done
+}
+
+func TestPtyResize(t *testing.T) {
+ t.Parallel()
+ winch0 := Window{Width: 40, Height: 80}
+ winch1 := Window{Width: 80, Height: 160}
+ winch2 := Window{Width: 20, Height: 40}
+ winches := make(chan Window)
+ done := make(chan bool)
+ session, _, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {
+ ptyReq, winCh, isPty := s.Pty()
+ if !isPty {
+ t.Fatalf("expected pty but none requested")
+ }
+ if ptyReq.Window != winch0 {
+ t.Fatalf("expected window %#v but got %#v", winch0, ptyReq.Window)
+ }
+ for win := range winCh {
+ winches <- win
+ }
+ close(done)
+ },
+ }, nil)
+ defer cleanup()
+ // winch0
+ if err := session.RequestPty("xterm", winch0.Height, winch0.Width, gossh.TerminalModes{}); err != nil {
+ t.Fatalf("expected nil but got %v", err)
+ }
+ if err := session.Shell(); err != nil {
+ t.Fatalf("expected nil but got %v", err)
+ }
+ gotWinch := <-winches
+ if gotWinch != winch0 {
+ t.Fatalf("expected window %#v but got %#v", winch0, gotWinch)
+ }
+ // winch1
+ winchMsg := struct{ w, h uint32 }{uint32(winch1.Width), uint32(winch1.Height)}
+ ok, err := session.SendRequest("window-change", true, gossh.Marshal(&winchMsg))
+ if err == nil && !ok {
+ t.Fatalf("unexpected error or bad reply on send request")
+ }
+ gotWinch = <-winches
+ if gotWinch != winch1 {
+ t.Fatalf("expected window %#v but got %#v", winch1, gotWinch)
+ }
+ // winch2
+ winchMsg = struct{ w, h uint32 }{uint32(winch2.Width), uint32(winch2.Height)}
+ ok, err = session.SendRequest("window-change", true, gossh.Marshal(&winchMsg))
+ if err == nil && !ok {
+ t.Fatalf("unexpected error or bad reply on send request")
+ }
+ gotWinch = <-winches
+ if gotWinch != winch2 {
+ t.Fatalf("expected window %#v but got %#v", winch2, gotWinch)
+ }
+ session.Close()
+ <-done
+}
+
+func TestSignals(t *testing.T) {
+ t.Parallel()
+
+ // errChan lets us get errors back from the session
+ errChan := make(chan error, 5)
+
+ // doneChan lets us specify that we should exit.
+ doneChan := make(chan interface{})
+
+ session, _, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {
+ // We need to use a buffered channel here, otherwise it's possible for the
+ // second call to Signal to get discarded.
+ signals := make(chan Signal, 2)
+ s.Signals(signals)
+
+ select {
+ case sig := <-signals:
+ if sig != SIGINT {
+ errChan <- fmt.Errorf("expected signal %v but got %v", SIGINT, sig)
+ return
+ }
+ case <-doneChan:
+ errChan <- fmt.Errorf("Unexpected done")
+ return
+ }
+
+ select {
+ case sig := <-signals:
+ if sig != SIGKILL {
+ errChan <- fmt.Errorf("expected signal %v but got %v", SIGKILL, sig)
+ return
+ }
+ case <-doneChan:
+ errChan <- fmt.Errorf("Unexpected done")
+ return
+ }
+ },
+ }, nil)
+ defer cleanup()
+
+ go func() {
+ session.Signal(gossh.SIGINT)
+ session.Signal(gossh.SIGKILL)
+ }()
+
+ go func() {
+ errChan <- session.Run("")
+ }()
+
+ err := <-errChan
+ close(doneChan)
+
+ if err != nil {
+ t.Fatalf("expected nil but got %v", err)
+ }
+}
+
+func TestBreakWithChanRegistered(t *testing.T) {
+ t.Parallel()
+
+ // errChan lets us get errors back from the session
+ errChan := make(chan error, 5)
+
+ // doneChan lets us specify that we should exit.
+ doneChan := make(chan interface{})
+
+ breakChan := make(chan bool)
+
+ readyToReceiveBreak := make(chan bool)
+
+ session, _, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {
+ s.Break(breakChan) // register a break channel with the session
+ readyToReceiveBreak <- true
+
+ select {
+ case <-breakChan:
+ io.WriteString(s, "break")
+ case <-doneChan:
+ errChan <- fmt.Errorf("Unexpected done")
+ return
+ }
+ },
+ }, nil)
+ defer cleanup()
+ var stdout bytes.Buffer
+ session.Stdout = &stdout
+ go func() {
+ errChan <- session.Run("")
+ }()
+
+ <-readyToReceiveBreak
+ ok, err := session.SendRequest("break", true, nil)
+ if err != nil {
+ t.Fatalf("expected nil but got %v", err)
+ }
+ if ok != true {
+ t.Fatalf("expected true but got %v", ok)
+ }
+
+ err = <-errChan
+ close(doneChan)
+
+ if err != nil {
+ t.Fatalf("expected nil but got %v", err)
+ }
+ if !bytes.Equal(stdout.Bytes(), []byte("break")) {
+ t.Fatalf("stdout = %#v, expected 'break'", stdout.Bytes())
+ }
+}
+
+func TestBreakWithoutChanRegistered(t *testing.T) {
+ t.Parallel()
+
+ // errChan lets us get errors back from the session
+ errChan := make(chan error, 5)
+
+ // doneChan lets us specify that we should exit.
+ doneChan := make(chan interface{})
+
+ waitUntilAfterBreakSent := make(chan bool)
+
+ session, _, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {
+ <-waitUntilAfterBreakSent
+ },
+ }, nil)
+ defer cleanup()
+ var stdout bytes.Buffer
+ session.Stdout = &stdout
+ go func() {
+ errChan <- session.Run("")
+ }()
+
+ ok, err := session.SendRequest("break", true, nil)
+ if err != nil {
+ t.Fatalf("expected nil but got %v", err)
+ }
+ if ok != false {
+ t.Fatalf("expected false but got %v", ok)
+ }
+ waitUntilAfterBreakSent <- true
+
+ err = <-errChan
+ close(doneChan)
+ if err != nil {
+ t.Fatalf("expected nil but got %v", err)
+ }
+}
diff --git a/tempfork/gliderlabs/ssh/ssh.go b/tempfork/gliderlabs/ssh/ssh.go
new file mode 100644
index 000000000..3262d84b2
--- /dev/null
+++ b/tempfork/gliderlabs/ssh/ssh.go
@@ -0,0 +1,152 @@
+package ssh
+
+import (
+ "crypto/subtle"
+ "net"
+
+ gossh "github.com/tailscale/golang-x-crypto/ssh"
+)
+
+type Signal string
+
+// POSIX signals as listed in RFC 4254 Section 6.10.
+const (
+ SIGABRT Signal = "ABRT"
+ SIGALRM Signal = "ALRM"
+ SIGFPE Signal = "FPE"
+ SIGHUP Signal = "HUP"
+ SIGILL Signal = "ILL"
+ SIGINT Signal = "INT"
+ SIGKILL Signal = "KILL"
+ SIGPIPE Signal = "PIPE"
+ SIGQUIT Signal = "QUIT"
+ SIGSEGV Signal = "SEGV"
+ SIGTERM Signal = "TERM"
+ SIGUSR1 Signal = "USR1"
+ SIGUSR2 Signal = "USR2"
+)
+
+// DefaultHandler is the default Handler used by Serve.
+var DefaultHandler Handler
+
+// Option is a functional option handler for Server.
+type Option func(*Server) error
+
+// Handler is a callback for handling established SSH sessions.
+type Handler func(Session)
+
+// PublicKeyHandler is a callback for performing public key authentication.
+type PublicKeyHandler func(ctx Context, key PublicKey) bool
+
+// PasswordHandler is a callback for performing password authentication.
+type PasswordHandler func(ctx Context, password string) bool
+
+// KeyboardInteractiveHandler is a callback for performing keyboard-interactive authentication.
+type KeyboardInteractiveHandler func(ctx Context, challenger gossh.KeyboardInteractiveChallenge) bool
+
+// PtyCallback is a hook for allowing PTY sessions.
+type PtyCallback func(ctx Context, pty Pty) bool
+
+// SessionRequestCallback is a callback for allowing or denying SSH sessions.
+type SessionRequestCallback func(sess Session, requestType string) bool
+
+// ConnCallback is a hook for new connections before handling.
+// It allows wrapping for timeouts and limiting by returning
+// the net.Conn that will be used as the underlying connection.
+type ConnCallback func(ctx Context, conn net.Conn) net.Conn
+
+// LocalPortForwardingCallback is a hook for allowing port forwarding
+type LocalPortForwardingCallback func(ctx Context, destinationHost string, destinationPort uint32) bool
+
+// ReversePortForwardingCallback is a hook for allowing reverse port forwarding
+type ReversePortForwardingCallback func(ctx Context, bindHost string, bindPort uint32) bool
+
+// ServerConfigCallback is a hook for creating custom default server configs
+type ServerConfigCallback func(ctx Context) *gossh.ServerConfig
+
+// ConnectionFailedCallback is a hook for reporting failed connections
+// Please note: the net.Conn is likely to be closed at this point
+type ConnectionFailedCallback func(conn net.Conn, err error)
+
+// Window represents the size of a PTY window.
+//
+// See https://datatracker.ietf.org/doc/html/rfc4254#section-6.2
+//
+// Zero dimension parameters MUST be ignored. The character/row dimensions
+// override the pixel dimensions (when nonzero). Pixel dimensions refer
+// to the drawable area of the window.
+type Window struct {
+ // Width is the number of columns.
+ // It overrides WidthPixels.
+ Width int
+ // Height is the number of rows.
+ // It overrides HeightPixels.
+ Height int
+
+ // WidthPixels is the drawable width of the window, in pixels.
+ WidthPixels int
+ // HeightPixels is the drawable height of the window, in pixels.
+ HeightPixels int
+}
+
+// Pty represents a PTY request and configuration.
+type Pty struct {
+ // Term is the TERM environment variable value.
+ Term string
+
+ // Window is the Window sent as part of the pty-req.
+ Window Window
+
+ // Modes represent a mapping of Terminal Mode opcode to value as it was
+ // requested by the client as part of the pty-req. These are outlined as
+ // part of https://datatracker.ietf.org/doc/html/rfc4254#section-8.
+ //
+ // The opcodes are defined as constants in github.com/tailscale/golang-x-crypto/ssh (VINTR,VQUIT,etc.).
+ // Boolean opcodes have values 0 or 1.
+ Modes gossh.TerminalModes
+}
+
+// Serve accepts incoming SSH connections on the listener l, creating a new
+// connection goroutine for each. The connection goroutines read requests and
+// then calls handler to handle sessions. Handler is typically nil, in which
+// case the DefaultHandler is used.
+func Serve(l net.Listener, handler Handler, options ...Option) error {
+ srv := &Server{Handler: handler}
+ for _, option := range options {
+ if err := srv.SetOption(option); err != nil {
+ return err
+ }
+ }
+ return srv.Serve(l)
+}
+
+// ListenAndServe listens on the TCP network address addr and then calls Serve
+// with handler to handle sessions on incoming connections. Handler is typically
+// nil, in which case the DefaultHandler is used.
+func ListenAndServe(addr string, handler Handler, options ...Option) error {
+ srv := &Server{Addr: addr, Handler: handler}
+ for _, option := range options {
+ if err := srv.SetOption(option); err != nil {
+ return err
+ }
+ }
+ return srv.ListenAndServe()
+}
+
+// Handle registers the handler as the DefaultHandler.
+func Handle(handler Handler) {
+ DefaultHandler = handler
+}
+
+// KeysEqual is constant time compare of the keys to avoid timing attacks.
+func KeysEqual(ak, bk PublicKey) bool {
+
+ //avoid panic if one of the keys is nil, return false instead
+ if ak == nil || bk == nil {
+ return false
+ }
+
+ a := ak.Marshal()
+ b := bk.Marshal()
+ return (len(a) == len(b) && subtle.ConstantTimeCompare(a, b) == 1)
+}
diff --git a/tempfork/gliderlabs/ssh/ssh_test.go b/tempfork/gliderlabs/ssh/ssh_test.go
new file mode 100644
index 000000000..aa301b048
--- /dev/null
+++ b/tempfork/gliderlabs/ssh/ssh_test.go
@@ -0,0 +1,17 @@
+package ssh
+
+import (
+ "testing"
+)
+
+func TestKeysEqual(t *testing.T) {
+ defer func() {
+ if r := recover(); r != nil {
+ t.Errorf("The code did panic")
+ }
+ }()
+
+ if KeysEqual(nil, nil) {
+ t.Error("two nil keys should not return true")
+ }
+}
diff --git a/tempfork/gliderlabs/ssh/tcpip.go b/tempfork/gliderlabs/ssh/tcpip.go
new file mode 100644
index 000000000..056a0c734
--- /dev/null
+++ b/tempfork/gliderlabs/ssh/tcpip.go
@@ -0,0 +1,193 @@
+package ssh
+
+import (
+ "io"
+ "log"
+ "net"
+ "strconv"
+ "sync"
+
+ gossh "github.com/tailscale/golang-x-crypto/ssh"
+)
+
+const (
+ forwardedTCPChannelType = "forwarded-tcpip"
+)
+
+// direct-tcpip data struct as specified in RFC4254, Section 7.2
+type localForwardChannelData struct {
+ DestAddr string
+ DestPort uint32
+
+ OriginAddr string
+ OriginPort uint32
+}
+
+// DirectTCPIPHandler can be enabled by adding it to the server's
+// ChannelHandlers under direct-tcpip.
+func DirectTCPIPHandler(srv *Server, conn *gossh.ServerConn, newChan gossh.NewChannel, ctx Context) {
+ d := localForwardChannelData{}
+ if err := gossh.Unmarshal(newChan.ExtraData(), &d); err != nil {
+ newChan.Reject(gossh.ConnectionFailed, "error parsing forward data: "+err.Error())
+ return
+ }
+
+ if srv.LocalPortForwardingCallback == nil || !srv.LocalPortForwardingCallback(ctx, d.DestAddr, d.DestPort) {
+ newChan.Reject(gossh.Prohibited, "port forwarding is disabled")
+ return
+ }
+
+ dest := net.JoinHostPort(d.DestAddr, strconv.FormatInt(int64(d.DestPort), 10))
+
+ var dialer net.Dialer
+ dconn, err := dialer.DialContext(ctx, "tcp", dest)
+ if err != nil {
+ newChan.Reject(gossh.ConnectionFailed, err.Error())
+ return
+ }
+
+ ch, reqs, err := newChan.Accept()
+ if err != nil {
+ dconn.Close()
+ return
+ }
+ go gossh.DiscardRequests(reqs)
+
+ go func() {
+ defer ch.Close()
+ defer dconn.Close()
+ io.Copy(ch, dconn)
+ }()
+ go func() {
+ defer ch.Close()
+ defer dconn.Close()
+ io.Copy(dconn, ch)
+ }()
+}
+
+type remoteForwardRequest struct {
+ BindAddr string
+ BindPort uint32
+}
+
+type remoteForwardSuccess struct {
+ BindPort uint32
+}
+
+type remoteForwardCancelRequest struct {
+ BindAddr string
+ BindPort uint32
+}
+
+type remoteForwardChannelData struct {
+ DestAddr string
+ DestPort uint32
+ OriginAddr string
+ OriginPort uint32
+}
+
+// ForwardedTCPHandler can be enabled by creating a ForwardedTCPHandler and
+// adding the HandleSSHRequest callback to the server's RequestHandlers under
+// tcpip-forward and cancel-tcpip-forward.
+type ForwardedTCPHandler struct {
+ forwards map[string]net.Listener
+ sync.Mutex
+}
+
+func (h *ForwardedTCPHandler) HandleSSHRequest(ctx Context, srv *Server, req *gossh.Request) (bool, []byte) {
+ h.Lock()
+ if h.forwards == nil {
+ h.forwards = make(map[string]net.Listener)
+ }
+ h.Unlock()
+ conn := ctx.Value(ContextKeyConn).(*gossh.ServerConn)
+ switch req.Type {
+ case "tcpip-forward":
+ var reqPayload remoteForwardRequest
+ if err := gossh.Unmarshal(req.Payload, &reqPayload); err != nil {
+ // TODO: log parse failure
+ return false, []byte{}
+ }
+ if srv.ReversePortForwardingCallback == nil || !srv.ReversePortForwardingCallback(ctx, reqPayload.BindAddr, reqPayload.BindPort) {
+ return false, []byte("port forwarding is disabled")
+ }
+ addr := net.JoinHostPort(reqPayload.BindAddr, strconv.Itoa(int(reqPayload.BindPort)))
+ ln, err := net.Listen("tcp", addr)
+ if err != nil {
+ // TODO: log listen failure
+ return false, []byte{}
+ }
+ _, destPortStr, _ := net.SplitHostPort(ln.Addr().String())
+ destPort, _ := strconv.Atoi(destPortStr)
+ h.Lock()
+ h.forwards[addr] = ln
+ h.Unlock()
+ go func() {
+ <-ctx.Done()
+ h.Lock()
+ ln, ok := h.forwards[addr]
+ h.Unlock()
+ if ok {
+ ln.Close()
+ }
+ }()
+ go func() {
+ for {
+ c, err := ln.Accept()
+ if err != nil {
+ // TODO: log accept failure
+ break
+ }
+ originAddr, orignPortStr, _ := net.SplitHostPort(c.RemoteAddr().String())
+ originPort, _ := strconv.Atoi(orignPortStr)
+ payload := gossh.Marshal(&remoteForwardChannelData{
+ DestAddr: reqPayload.BindAddr,
+ DestPort: uint32(destPort),
+ OriginAddr: originAddr,
+ OriginPort: uint32(originPort),
+ })
+ go func() {
+ ch, reqs, err := conn.OpenChannel(forwardedTCPChannelType, payload)
+ if err != nil {
+ // TODO: log failure to open channel
+ log.Println(err)
+ c.Close()
+ return
+ }
+ go gossh.DiscardRequests(reqs)
+ go func() {
+ defer ch.Close()
+ defer c.Close()
+ io.Copy(ch, c)
+ }()
+ go func() {
+ defer ch.Close()
+ defer c.Close()
+ io.Copy(c, ch)
+ }()
+ }()
+ }
+ h.Lock()
+ delete(h.forwards, addr)
+ h.Unlock()
+ }()
+ return true, gossh.Marshal(&remoteForwardSuccess{uint32(destPort)})
+
+ case "cancel-tcpip-forward":
+ var reqPayload remoteForwardCancelRequest
+ if err := gossh.Unmarshal(req.Payload, &reqPayload); err != nil {
+ // TODO: log parse failure
+ return false, []byte{}
+ }
+ addr := net.JoinHostPort(reqPayload.BindAddr, strconv.Itoa(int(reqPayload.BindPort)))
+ h.Lock()
+ ln, ok := h.forwards[addr]
+ h.Unlock()
+ if ok {
+ ln.Close()
+ }
+ return true, nil
+ default:
+ return false, nil
+ }
+}
diff --git a/tempfork/gliderlabs/ssh/tcpip_test.go b/tempfork/gliderlabs/ssh/tcpip_test.go
new file mode 100644
index 000000000..7d43bb0a9
--- /dev/null
+++ b/tempfork/gliderlabs/ssh/tcpip_test.go
@@ -0,0 +1,86 @@
+//go:build glidertests
+// +build glidertests
+
+package ssh
+
+import (
+ "bytes"
+ "io/ioutil"
+ "net"
+ "strconv"
+ "strings"
+ "testing"
+
+ gossh "github.com/tailscale/golang-x-crypto/ssh"
+)
+
+var sampleServerResponse = []byte("Hello world")
+
+func sampleSocketServer() net.Listener {
+ l := newLocalListener()
+
+ go func() {
+ conn, err := l.Accept()
+ if err != nil {
+ return
+ }
+ conn.Write(sampleServerResponse)
+ conn.Close()
+ }()
+
+ return l
+}
+
+func newTestSessionWithForwarding(t *testing.T, forwardingEnabled bool) (net.Listener, *gossh.Client, func()) {
+ l := sampleSocketServer()
+
+ _, client, cleanup := newTestSession(t, &Server{
+ Handler: func(s Session) {},
+ LocalPortForwardingCallback: func(ctx Context, destinationHost string, destinationPort uint32) bool {
+ addr := net.JoinHostPort(destinationHost, strconv.FormatInt(int64(destinationPort), 10))
+ if addr != l.Addr().String() {
+ panic("unexpected destinationHost: " + addr)
+ }
+ return forwardingEnabled
+ },
+ }, nil)
+
+ return l, client, func() {
+ cleanup()
+ l.Close()
+ }
+}
+
+func TestLocalPortForwardingWorks(t *testing.T) {
+ t.Parallel()
+
+ l, client, cleanup := newTestSessionWithForwarding(t, true)
+ defer cleanup()
+
+ conn, err := client.Dial("tcp", l.Addr().String())
+ if err != nil {
+ t.Fatalf("Error connecting to %v: %v", l.Addr().String(), err)
+ }
+ result, err := ioutil.ReadAll(conn)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(result, sampleServerResponse) {
+ t.Fatalf("result = %#v; want %#v", result, sampleServerResponse)
+ }
+}
+
+func TestLocalPortForwardingRespectsCallback(t *testing.T) {
+ t.Parallel()
+
+ l, client, cleanup := newTestSessionWithForwarding(t, false)
+ defer cleanup()
+
+ _, err := client.Dial("tcp", l.Addr().String())
+ if err == nil {
+ t.Fatalf("Expected error connecting to %v but it succeeded", l.Addr().String())
+ }
+ if !strings.Contains(err.Error(), "port forwarding is disabled") {
+ t.Fatalf("Expected permission error but got %#v", err)
+ }
+}
diff --git a/tempfork/gliderlabs/ssh/util.go b/tempfork/gliderlabs/ssh/util.go
new file mode 100644
index 000000000..e3b5716a3
--- /dev/null
+++ b/tempfork/gliderlabs/ssh/util.go
@@ -0,0 +1,157 @@
+package ssh
+
+import (
+ "crypto/rand"
+ "crypto/rsa"
+ "encoding/binary"
+
+ "github.com/tailscale/golang-x-crypto/ssh"
+)
+
+func generateSigner() (ssh.Signer, error) {
+ key, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ return nil, err
+ }
+ return ssh.NewSignerFromKey(key)
+}
+
+func parsePtyRequest(payload []byte) (pty Pty, ok bool) {
+ // See https://datatracker.ietf.org/doc/html/rfc4254#section-6.2
+ // 6.2. Requesting a Pseudo-Terminal
+ // A pseudo-terminal can be allocated for the session by sending the
+ // following message.
+ // byte SSH_MSG_CHANNEL_REQUEST
+ // uint32 recipient channel
+ // string "pty-req"
+ // boolean want_reply
+ // string TERM environment variable value (e.g., vt100)
+ // uint32 terminal width, characters (e.g., 80)
+ // uint32 terminal height, rows (e.g., 24)
+ // uint32 terminal width, pixels (e.g., 640)
+ // uint32 terminal height, pixels (e.g., 480)
+ // string encoded terminal modes
+
+ // The payload starts from the TERM variable.
+ term, rem, ok := parseString(payload)
+ if !ok {
+ return
+ }
+ win, rem, ok := parseWindow(rem)
+ if !ok {
+ return
+ }
+ modes, ok := parseTerminalModes(rem)
+ if !ok {
+ return
+ }
+ pty = Pty{
+ Term: term,
+ Window: win,
+ Modes: modes,
+ }
+ return
+}
+
+func parseTerminalModes(in []byte) (modes ssh.TerminalModes, ok bool) {
+ // See https://datatracker.ietf.org/doc/html/rfc4254#section-8
+ // 8. Encoding of Terminal Modes
+ //
+ // All 'encoded terminal modes' (as passed in a pty request) are encoded
+ // into a byte stream. It is intended that the coding be portable
+ // across different environments. The stream consists of opcode-
+ // argument pairs wherein the opcode is a byte value. Opcodes 1 to 159
+ // have a single uint32 argument. Opcodes 160 to 255 are not yet
+ // defined, and cause parsing to stop (they should only be used after
+ // any other data). The stream is terminated by opcode TTY_OP_END
+ // (0x00).
+ //
+ // The client SHOULD put any modes it knows about in the stream, and the
+ // server MAY ignore any modes it does not know about. This allows some
+ // degree of machine-independence, at least between systems that use a
+ // POSIX-like tty interface. The protocol can support other systems as
+ // well, but the client may need to fill reasonable values for a number
+ // of parameters so the server pty gets set to a reasonable mode (the
+ // server leaves all unspecified mode bits in their default values, and
+ // only some combinations make sense).
+ _, rem, ok := parseUint32(in)
+ if !ok {
+ return
+ }
+ const ttyOpEnd = 0
+ for len(rem) > 0 {
+ if modes == nil {
+ modes = make(ssh.TerminalModes)
+ }
+ code := uint8(rem[0])
+ rem = rem[1:]
+ if code == ttyOpEnd || code > 160 {
+ break
+ }
+ var val uint32
+ val, rem, ok = parseUint32(rem)
+ if !ok {
+ return
+ }
+ modes[code] = val
+ }
+ ok = true
+ return
+}
+
+func parseWindow(s []byte) (win Window, rem []byte, ok bool) {
+ // See https://datatracker.ietf.org/doc/html/rfc4254#section-6.7
+ // 6.7. Window Dimension Change Message
+ // When the window (terminal) size changes on the client side, it MAY
+ // send a message to the other side to inform it of the new dimensions.
+
+ // byte SSH_MSG_CHANNEL_REQUEST
+ // uint32 recipient channel
+ // string "window-change"
+ // boolean FALSE
+ // uint32 terminal width, columns
+ // uint32 terminal height, rows
+ // uint32 terminal width, pixels
+ // uint32 terminal height, pixels
+ wCols, rem, ok := parseUint32(s)
+ if !ok {
+ return
+ }
+ hRows, rem, ok := parseUint32(rem)
+ if !ok {
+ return
+ }
+ wPixels, rem, ok := parseUint32(rem)
+ if !ok {
+ return
+ }
+ hPixels, rem, ok := parseUint32(rem)
+ if !ok {
+ return
+ }
+ win = Window{
+ Width: int(wCols),
+ Height: int(hRows),
+ WidthPixels: int(wPixels),
+ HeightPixels: int(hPixels),
+ }
+ return
+}
+
+func parseString(in []byte) (out string, rem []byte, ok bool) {
+ length, rem, ok := parseUint32(in)
+ if uint32(len(rem)) < length || !ok {
+ ok = false
+ return
+ }
+ out, rem = string(rem[:length]), rem[length:]
+ ok = true
+ return
+}
+
+func parseUint32(in []byte) (uint32, []byte, bool) {
+ if len(in) < 4 {
+ return 0, nil, false
+ }
+ return binary.BigEndian.Uint32(in), in[4:], true
+}
diff --git a/tempfork/gliderlabs/ssh/wrap.go b/tempfork/gliderlabs/ssh/wrap.go
new file mode 100644
index 000000000..17867d751
--- /dev/null
+++ b/tempfork/gliderlabs/ssh/wrap.go
@@ -0,0 +1,33 @@
+package ssh
+
+import gossh "github.com/tailscale/golang-x-crypto/ssh"
+
+// PublicKey is an abstraction of different types of public keys.
+type PublicKey interface {
+ gossh.PublicKey
+}
+
+// The Permissions type holds fine-grained permissions that are specific to a
+// user or a specific authentication method for a user. Permissions, except for
+// "source-address", must be enforced in the server application layer, after
+// successful authentication.
+type Permissions struct {
+ *gossh.Permissions
+}
+
+// A Signer can create signatures that verify against a public key.
+type Signer interface {
+ gossh.Signer
+}
+
+// ParseAuthorizedKey parses a public key from an authorized_keys file used in
+// OpenSSH according to the sshd(8) manual page.
+func ParseAuthorizedKey(in []byte) (out PublicKey, comment string, options []string, rest []byte, err error) {
+ return gossh.ParseAuthorizedKey(in)
+}
+
+// ParsePublicKey parses an SSH public key formatted for use in
+// the SSH wire protocol according to RFC 4253, section 6.6.
+func ParsePublicKey(in []byte) (out PublicKey, err error) {
+ return gossh.ParsePublicKey(in)
+}