tempfork: temporarily fork gliderlabs/ssh and x/crypto/ssh

While we rearrange/upstream things.

gliderlabs/ssh is forked into tempfork from our prior fork
at be8b7add40

x/crypto/ssh OTOH is forked at
https://github.com/tailscale/golang-x-crypto because it was gnarlier
to vendor with various internal packages, etc.
Its git history shows where it starts (2c7772ba30643b7a2026cbea938420dce7c6384d).

Updates #3802

Change-Id: I546e5cdf831cfc030a6c42557c0ad2c58766c65f
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2022-03-25 15:35:36 -07:00 committed by Brad Fitzpatrick
parent 6fecc16c3b
commit 5a44f9f5b5
28 changed files with 2811 additions and 17 deletions

View File

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

View File

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

6
go.mod
View File

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

4
go.sum
View File

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

View File

@ -23,7 +23,7 @@
"strings"
"sync"
"golang.org/x/crypto/ssh"
"github.com/tailscale/golang-x-crypto/ssh"
"tailscale.com/envknob"
)

View File

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

View File

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

View File

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

View File

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

View File

@ -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. &mdash;[@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)]
<a href="https://opencollective.com/ssh/sponsor/0/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/1/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/2/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/3/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/4/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/5/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/6/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/7/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/8/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/9/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/9/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/10/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/10/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/11/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/11/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/12/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/12/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/13/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/13/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/14/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/14/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/15/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/15/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/16/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/16/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/17/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/17/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/18/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/18/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/19/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/19/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/20/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/20/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/21/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/21/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/22/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/22/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/23/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/23/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/24/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/24/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/25/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/25/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/26/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/26/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/27/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/27/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/28/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/28/avatar.svg"></a>
<a href="https://opencollective.com/ssh/sponsor/29/website" target="_blank"><img src="https://opencollective.com/ssh/sponsor/29/avatar.svg"></a>
## License
[BSD](LICENSE)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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