From 6bb503e2500d811ce0156c396d8f597f476d7062 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 29 Mar 2022 17:31:44 -0700 Subject: [PATCH 01/10] go.mod, ssh/tailssh, tempfork/gliderlabs: bump x/crypto/ssh fork for NoClientAuthCallback Originally from https://github.com/tailscale/tailscale/commit/3d180c03764d4aebdc9804fe08e858a5233b7e26 --- server.go | 7 +++++++ ssh.go | 3 +++ 2 files changed, 10 insertions(+) diff --git a/server.go b/server.go index be4355e..e5bb24a 100644 --- a/server.go +++ b/server.go @@ -38,6 +38,9 @@ type Server struct { HostSigners []Signer // private keys for the host key, must have at least one Version string // server version to be sent before the initial handshake + NoClientAuthCallback func(gossh.ConnMetadata) (*gossh.Permissions, error) + + BannerHandler BannerHandler // server banner handler, overrides Banner KeyboardInteractiveHandler KeyboardInteractiveHandler // keyboard-interactive authentication handler PasswordHandler PasswordHandler // password authentication handler PublicKeyHandler PublicKeyHandler // public key authentication handler @@ -129,6 +132,10 @@ func (srv *Server) config(ctx Context) *gossh.ServerConfig { if srv.PasswordHandler == nil && srv.PublicKeyHandler == nil && srv.KeyboardInteractiveHandler == nil { config.NoClientAuth = true } + if srv.NoClientAuthCallback != nil { + config.NoClientAuth = true + config.NoClientAuthCallback = srv.NoClientAuthCallback + } if srv.Version != "" { config.ServerVersion = "SSH-2.0-" + srv.Version } diff --git a/ssh.go b/ssh.go index 8bb02a3..b8e7ff3 100644 --- a/ssh.go +++ b/ssh.go @@ -35,6 +35,9 @@ type Option func(*Server) error // Handler is a callback for handling established SSH sessions. type Handler func(Session) +// BannerHandler is a callback for displaying the server banner. +type BannerHandler func(ctx Context) string + // PublicKeyHandler is a callback for performing public key authentication. type PublicKeyHandler func(ctx Context, key PublicKey) bool From 6991780787c2c01edc1b53d5db651ecb2803761c Mon Sep 17 00:00:00 2001 From: Maisem Ali Date: Wed, 20 Apr 2022 13:39:15 -0700 Subject: [PATCH 02/10] ssh/tailssh: terminate ssh auth early if no policy can match Also bump github.com/tailscale/golang-x-crypto/ssh Updates #3802 Signed-off-by: Maisem Ali Originally from https://github.com/tailscale/tailscale/commit/14d077fc3ac82d581fe989008b888cadad00b2f4 --- server.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/server.go b/server.go index e5bb24a..99fa04b 100644 --- a/server.go +++ b/server.go @@ -38,8 +38,6 @@ type Server struct { HostSigners []Signer // private keys for the host key, must have at least one Version string // server version to be sent before the initial handshake - NoClientAuthCallback func(gossh.ConnMetadata) (*gossh.Permissions, error) - BannerHandler BannerHandler // server banner handler, overrides Banner KeyboardInteractiveHandler KeyboardInteractiveHandler // keyboard-interactive authentication handler PasswordHandler PasswordHandler // password authentication handler @@ -132,10 +130,6 @@ func (srv *Server) config(ctx Context) *gossh.ServerConfig { if srv.PasswordHandler == nil && srv.PublicKeyHandler == nil && srv.KeyboardInteractiveHandler == nil { config.NoClientAuth = true } - if srv.NoClientAuthCallback != nil { - config.NoClientAuth = true - config.NoClientAuthCallback = srv.NoClientAuthCallback - } if srv.Version != "" { config.ServerVersion = "SSH-2.0-" + srv.Version } From 603440d34e3e30232bf9fe1aba4fa0b88dacbdf4 Mon Sep 17 00:00:00 2001 From: Maisem Ali Date: Wed, 20 Apr 2022 17:36:19 -0700 Subject: [PATCH 03/10] ssh/tailssh: send banner messages during auth, move more to conn (VSCode Live Share between Brad & Maisem!) Updates #3802 Change-Id: Id8edca4481b0811debfdf56d4ccb1a46f71dd6d3 Co-Authored-By: Brad Fitzpatrick Signed-off-by: Maisem Ali Originally from https://github.com/tailscale/tailscale/commit/2b8b887d5518d08eee9a121bd70c33059c3b71c1 Co-Authored-By: Brad Fitzpatrick --- example_test.go | 20 +++++++++++++++----- server.go | 4 ++-- ssh.go | 2 +- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/example_test.go b/example_test.go index 972d3ef..e81d12e 100644 --- a/example_test.go +++ b/example_test.go @@ -1,8 +1,9 @@ package ssh_test import ( + "errors" "io" - "io/ioutil" + "os" "github.com/gliderlabs/ssh" ) @@ -27,10 +28,19 @@ func ExampleNoPty() { 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) + ssh.PublicKeyAuth(func(ctx ssh.Context, key ssh.PublicKey) error { + data, err := os.ReadFile("/path/to/allowed/key.pub") + if err != nil { + return err + } + allowed, _, _, _, err := ssh.ParseAuthorizedKey(data) + if err != nil { + return err + } + if !ssh.KeysEqual(key, allowed) { + return errors.New("some error") + } + return nil }), ) } diff --git a/server.go b/server.go index 99fa04b..865b6bf 100644 --- a/server.go +++ b/server.go @@ -145,8 +145,8 @@ func (srv *Server) config(ctx Context) *gossh.ServerConfig { 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") + if err := srv.PublicKeyHandler(ctx, key); err != nil { + return ctx.Permissions().Permissions, err } ctx.SetValue(ContextKeyPublicKey, key) return ctx.Permissions().Permissions, nil diff --git a/ssh.go b/ssh.go index b8e7ff3..bcd5578 100644 --- a/ssh.go +++ b/ssh.go @@ -39,7 +39,7 @@ type Handler func(Session) type BannerHandler func(ctx Context) string // PublicKeyHandler is a callback for performing public key authentication. -type PublicKeyHandler func(ctx Context, key PublicKey) bool +type PublicKeyHandler func(ctx Context, key PublicKey) error // PasswordHandler is a callback for performing password authentication. type PasswordHandler func(ctx Context, password string) bool From 6f4746a5d9987f7095ccaffc4bb02aa8a3446a65 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 2 Aug 2022 09:33:46 -0700 Subject: [PATCH 04/10] all: gofmt for Go 1.19 Updates #5210 Change-Id: Ib02cd5e43d0a8db60c1f09755a8ac7b140b670be Signed-off-by: Brad Fitzpatrick Originally from https://github.com/tailscale/tailscale/commit/116f55ff6647d88f7f4a16237e28eda8d0e5b82f --- doc.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/doc.go b/doc.go index 5a10393..d139191 100644 --- a/doc.go +++ b/doc.go @@ -10,29 +10,29 @@ 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") - }) + ssh.Handle(func(s ssh.Session) { + io.WriteString(s, "Hello world\n") + }) - log.Fatal(ssh.ListenAndServe(":2222", nil)) + 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"))) + 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) + s := &ssh.Server{ + Addr: ":2222", + Handler: sessionHandler, + PublicKeyHandler: authHandler, + } + s.AddHostKey(hostKeySigner) - log.Fatal(s.ListenAndServe()) + log.Fatal(s.ListenAndServe()) This package automatically handles basic SSH requests like setting environment variables, requesting PTY, and changing window size. These requests are From 83161df1534b674843f616ff23967c5155dcf4af Mon Sep 17 00:00:00 2001 From: Maisem Ali Date: Thu, 6 Oct 2022 10:34:58 -0700 Subject: [PATCH 05/10] ssh/tailssh: do the full auth flow during ssh auth Fixes #5091 Signed-off-by: Maisem Ali Originally from https://github.com/tailscale/tailscale/commit/f16b77de5d75341244290960797dface7a8fca09 --- server.go | 19 ++++++++++++++++++- ssh.go | 4 ++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/server.go b/server.go index 865b6bf..5e24f1e 100644 --- a/server.go +++ b/server.go @@ -39,9 +39,11 @@ type Server struct { Version string // server version to be sent before the initial handshake BannerHandler BannerHandler // server banner handler, overrides Banner - KeyboardInteractiveHandler KeyboardInteractiveHandler // keyboard-interactive authentication handler + KeyboardInteractiveHandler KeyboardInteractiveHandler // keyboard-interactive authentication handler + BannerHandler BannerHandler PasswordHandler PasswordHandler // password authentication handler PublicKeyHandler PublicKeyHandler // public key authentication handler + NoClientAuthHandler NoClientAuthHandler // no client 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 @@ -161,6 +163,21 @@ func (srv *Server) config(ctx Context) *gossh.ServerConfig { return ctx.Permissions().Permissions, nil } } + if srv.NoClientAuthHandler != nil { + config.NoClientAuthCallback = func(conn gossh.ConnMetadata) (*gossh.Permissions, error) { + applyConnMetadata(ctx, conn) + if err := srv.NoClientAuthHandler(ctx); err != nil { + return ctx.Permissions().Permissions, err + } + return ctx.Permissions().Permissions, nil + } + } + if srv.BannerHandler != nil { + config.BannerCallback = func(conn gossh.ConnMetadata) string { + applyConnMetadata(ctx, conn) + return srv.BannerHandler(ctx) + } + } return config } diff --git a/ssh.go b/ssh.go index bcd5578..22391b4 100644 --- a/ssh.go +++ b/ssh.go @@ -41,6 +41,10 @@ type BannerHandler func(ctx Context) string // PublicKeyHandler is a callback for performing public key authentication. type PublicKeyHandler func(ctx Context, key PublicKey) error +type NoClientAuthHandler func(ctx Context) error + +type BannerHandler func(ctx Context) string + // PasswordHandler is a callback for performing password authentication. type PasswordHandler func(ctx Context, password string) bool From 8c55f7fde49493fc870171fed6f9830bee589890 Mon Sep 17 00:00:00 2001 From: Maisem Ali Date: Sun, 9 Oct 2022 10:31:19 -0700 Subject: [PATCH 06/10] ssh/tailssh: add support for sending multiple banners Signed-off-by: Maisem Ali Originally from https://github.com/tailscale/tailscale/commit/4de1601ef44a4959543bdbb88f0cd90997bab2cf --- context.go | 9 +++++++++ server.go | 9 +-------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/context.go b/context.go index 505a43d..54ac75a 100644 --- a/context.go +++ b/context.go @@ -55,6 +55,8 @@ var ( // ContextKeyPublicKey is a context key for use with Contexts in this package. // The associated value will be of type PublicKey. ContextKeyPublicKey = &contextKey{"public-key"} + + ContextKeySendAuthBanner = &contextKey{"send-auth-banner"} ) // Context is a package specific context interface. It exposes connection @@ -89,6 +91,8 @@ type Context interface { // SetValue allows you to easily write new values into the underlying context. SetValue(key, value interface{}) + + SendAuthBanner(banner string) error } type sshContext struct { @@ -117,6 +121,7 @@ func applyConnMetadata(ctx Context, conn gossh.ConnMetadata) { ctx.SetValue(ContextKeyUser, conn.User()) ctx.SetValue(ContextKeyLocalAddr, conn.LocalAddr()) ctx.SetValue(ContextKeyRemoteAddr, conn.RemoteAddr()) + ctx.SetValue(ContextKeySendAuthBanner, conn.SendAuthBanner) } func (ctx *sshContext) SetValue(key, value interface{}) { @@ -153,3 +158,7 @@ func (ctx *sshContext) LocalAddr() net.Addr { func (ctx *sshContext) Permissions() *Permissions { return ctx.Value(ContextKeyPermissions).(*Permissions) } + +func (ctx *sshContext) SendAuthBanner(msg string) error { + return ctx.Value(ContextKeySendAuthBanner).(func(string) error)(msg) +} diff --git a/server.go b/server.go index 5e24f1e..5c17d2f 100644 --- a/server.go +++ b/server.go @@ -39,8 +39,7 @@ type Server struct { Version string // server version to be sent before the initial handshake BannerHandler BannerHandler // server banner handler, overrides Banner - KeyboardInteractiveHandler KeyboardInteractiveHandler // keyboard-interactive authentication handler - BannerHandler BannerHandler + KeyboardInteractiveHandler KeyboardInteractiveHandler // keyboard-interactive authentication handler PasswordHandler PasswordHandler // password authentication handler PublicKeyHandler PublicKeyHandler // public key authentication handler NoClientAuthHandler NoClientAuthHandler // no client authentication handler @@ -172,12 +171,6 @@ func (srv *Server) config(ctx Context) *gossh.ServerConfig { return ctx.Permissions().Permissions, nil } } - if srv.BannerHandler != nil { - config.BannerCallback = func(conn gossh.ConnMetadata) string { - applyConnMetadata(ctx, conn) - return srv.BannerHandler(ctx) - } - } return config } From 1d99585f37c06e3ae4fe541d4040db0934dd8430 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Wed, 16 Aug 2023 22:09:53 -0700 Subject: [PATCH 07/10] all: use Go 1.21 slices, maps instead of x/exp/{slices,maps} Originally from https://github.com/tailscale/tailscale/commit/e8551d6b405c95d7dc7743aeb624d76e362ca39a --- tcpip_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/tcpip_test.go b/tcpip_test.go index 3c27eb1..b2d1d8e 100644 --- a/tcpip_test.go +++ b/tcpip_test.go @@ -4,6 +4,7 @@ import ( "bytes" "io/ioutil" "net" + "io" "strconv" "strings" "testing" From b526ee3727c63a598315171434c15cd207107810 Mon Sep 17 00:00:00 2001 From: Percy Wegmann Date: Fri, 31 Jan 2025 12:19:22 -0600 Subject: [PATCH 08/10] ssh,tempfork/gliderlabs/ssh: replace github.com/tailscale/golang-x-crypto/ssh with golang.org/x/crypto/ssh Collapsed from three commits in the tailscale monorepo: - 46fd4e58a (original change) - b60f6b849 (revert) - 2e95313b8 (re-apply) Import path changes are no-ops for this repo; the functional change is removing SendAuthBanner from context.go. Originally from https://github.com/tailscale/tailscale/commit/46fd4e58a27495263336b86ee961ee28d8c332b7 Originally from https://github.com/tailscale/tailscale/commit/b60f6b849af1fae1cf343be98f7fb1714c9ea165 Originally from https://github.com/tailscale/tailscale/commit/2e95313b8bb08e4dca1c0a27854fb3d65d40194f --- context.go | 9 --------- tcpip_test.go | 1 - 2 files changed, 10 deletions(-) diff --git a/context.go b/context.go index 54ac75a..505a43d 100644 --- a/context.go +++ b/context.go @@ -55,8 +55,6 @@ var ( // ContextKeyPublicKey is a context key for use with Contexts in this package. // The associated value will be of type PublicKey. ContextKeyPublicKey = &contextKey{"public-key"} - - ContextKeySendAuthBanner = &contextKey{"send-auth-banner"} ) // Context is a package specific context interface. It exposes connection @@ -91,8 +89,6 @@ type Context interface { // SetValue allows you to easily write new values into the underlying context. SetValue(key, value interface{}) - - SendAuthBanner(banner string) error } type sshContext struct { @@ -121,7 +117,6 @@ func applyConnMetadata(ctx Context, conn gossh.ConnMetadata) { ctx.SetValue(ContextKeyUser, conn.User()) ctx.SetValue(ContextKeyLocalAddr, conn.LocalAddr()) ctx.SetValue(ContextKeyRemoteAddr, conn.RemoteAddr()) - ctx.SetValue(ContextKeySendAuthBanner, conn.SendAuthBanner) } func (ctx *sshContext) SetValue(key, value interface{}) { @@ -158,7 +153,3 @@ func (ctx *sshContext) LocalAddr() net.Addr { func (ctx *sshContext) Permissions() *Permissions { return ctx.Value(ContextKeyPermissions).(*Permissions) } - -func (ctx *sshContext) SendAuthBanner(msg string) error { - return ctx.Value(ContextKeySendAuthBanner).(func(string) error)(msg) -} diff --git a/tcpip_test.go b/tcpip_test.go index b2d1d8e..3c27eb1 100644 --- a/tcpip_test.go +++ b/tcpip_test.go @@ -4,7 +4,6 @@ import ( "bytes" "io/ioutil" "net" - "io" "strconv" "strings" "testing" From 3ad9bb1b3f94391d4f6aa4614ffc6630659544b6 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 11 Mar 2026 14:04:10 +0000 Subject: [PATCH 09/10] all: rename module to github.com/tailscale/gliderssh Rename the module from github.com/tailscale/ssh to github.com/tailscale/gliderssh. Update all import paths in examples and tests. Rewrite README for the new repo name. Remove CircleCI config. Fix duplicate BannerHandler declaration and use named fields for Window struct literals. --- README.md | 85 +++++----------------- _examples/ssh-docker/README.md | 8 +- _examples/ssh-docker/docker.go | 2 +- _examples/ssh-forwardagent/forwardagent.go | 2 +- _examples/ssh-pty/pty.go | 2 +- _examples/ssh-publickey/public_key.go | 2 +- _examples/ssh-remoteforward/portforward.go | 2 +- _examples/ssh-simple/simple.go | 2 +- _examples/ssh-timeouts/timeouts.go | 2 +- circle.yml | 26 ------- example_test.go | 2 +- go.mod | 9 ++- go.sum | 17 ++--- server_test.go | 15 ++-- session_test.go | 38 +++++++--- ssh.go | 2 - 16 files changed, 77 insertions(+), 139 deletions(-) delete mode 100644 circle.yml diff --git a/README.md b/README.md index fbb2fbb..ab74665 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,33 @@ -# gliderlabs/ssh +# tailscale/gliderssh -[![GoDoc](https://godoc.org/github.com/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/github.com/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) +[![Go Reference](https://pkg.go.dev/badge/github.com/tailscale/gliderssh.svg)](https://pkg.go.dev/github.com/tailscale/gliderssh) +[![Go Report Card](https://goreportcard.com/badge/github.com/tailscale/gliderssh)](https://goreportcard.com/report/github.com/tailscale/gliderssh) -> The Glider Labs SSH server package is dope. —[@bradfitz](https://twitter.com/bradfitz), Go team member +This is a [Tailscale](https://tailscale.com) fork of [gliderlabs/ssh](https://github.com/gliderlabs/ssh). This Go package wraps the [crypto/ssh -package](https://godoc.org/golang.org/x/crypto/ssh) with a higher-level API for +package](https://pkg.go.dev/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 +package main - import ( - "github.com/gliderlabs/ssh" - "io" - "log" - ) +import ( + "io" + "log" - func main() { - ssh.Handle(func(s ssh.Session) { - io.WriteString(s, "Hello world\n") - }) + ssh "github.com/tailscale/gliderssh" +) - log.Fatal(ssh.ListenAndServe(":2222", nil)) - } +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 @@ -40,57 +35,13 @@ A bunch of great examples are in the `_examples` directory. ## Usage -[See GoDoc reference.](https://godoc.org/github.com/gliderlabs/ssh) +[See Go reference.](https://pkg.go.dev/github.com/tailscale/gliderssh) ## 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/_examples/ssh-docker/README.md b/_examples/ssh-docker/README.md index decee02..6454080 100644 --- a/_examples/ssh-docker/README.md +++ b/_examples/ssh-docker/README.md @@ -2,10 +2,10 @@ Run docker containers over SSH. You can even pipe things into them too! # Installation / Prep -We're going to build JQ as an SSH service using the Glider Labs SSH package. If you haven't installed GoLang and docker yet, see the doc's for help getting your environment setup. +We're going to build JQ as an SSH service using the gliderssh package. If you haven't installed Go and docker yet, see the docs for help getting your environment setup. -Install the Glider Labs SSH package -`go get github.com/gliderlabs/ssh` +Install the gliderssh package +`go get github.com/tailscale/gliderssh` Build the example docker container with `docker build --rm -t jq .` @@ -88,7 +88,7 @@ JQ's help text! It's working! Now let's pipe some json into our SSH service an ``` # Conclusion -We built JQ as a service over SSH in Go using the Glider Labs SSH package. We showed how you can run docker containers through the service as well as how to pipe stuff into your SSH service. +We built JQ as a service over SSH in Go using the gliderssh package. We showed how you can run docker containers through the service as well as how to pipe stuff into your SSH service. # Troubleshooting diff --git a/_examples/ssh-docker/docker.go b/_examples/ssh-docker/docker.go index 6c2b347..e3d2107 100644 --- a/_examples/ssh-docker/docker.go +++ b/_examples/ssh-docker/docker.go @@ -10,7 +10,7 @@ import ( "github.com/docker/docker/api/types/container" "github.com/docker/docker/client" "github.com/docker/docker/pkg/stdcopy" - "github.com/gliderlabs/ssh" + "github.com/tailscale/gliderssh" ) func main() { diff --git a/_examples/ssh-forwardagent/forwardagent.go b/_examples/ssh-forwardagent/forwardagent.go index 227693f..0b474dd 100644 --- a/_examples/ssh-forwardagent/forwardagent.go +++ b/_examples/ssh-forwardagent/forwardagent.go @@ -5,7 +5,7 @@ import ( "log" "os/exec" - "github.com/gliderlabs/ssh" + "github.com/tailscale/gliderssh" ) func main() { diff --git a/_examples/ssh-pty/pty.go b/_examples/ssh-pty/pty.go index 80bfac5..0a205cc 100644 --- a/_examples/ssh-pty/pty.go +++ b/_examples/ssh-pty/pty.go @@ -9,7 +9,7 @@ import ( "syscall" "unsafe" - "github.com/gliderlabs/ssh" + "github.com/tailscale/gliderssh" "github.com/creack/pty" ) diff --git a/_examples/ssh-publickey/public_key.go b/_examples/ssh-publickey/public_key.go index 453ce04..9ca5727 100644 --- a/_examples/ssh-publickey/public_key.go +++ b/_examples/ssh-publickey/public_key.go @@ -5,7 +5,7 @@ import ( "io" "log" - "github.com/gliderlabs/ssh" + "github.com/tailscale/gliderssh" gossh "golang.org/x/crypto/ssh" ) diff --git a/_examples/ssh-remoteforward/portforward.go b/_examples/ssh-remoteforward/portforward.go index 2ce866f..a132cf9 100644 --- a/_examples/ssh-remoteforward/portforward.go +++ b/_examples/ssh-remoteforward/portforward.go @@ -4,7 +4,7 @@ import ( "io" "log" - "github.com/gliderlabs/ssh" + "github.com/tailscale/gliderssh" ) func main() { diff --git a/_examples/ssh-simple/simple.go b/_examples/ssh-simple/simple.go index d2bcff1..a102ab4 100644 --- a/_examples/ssh-simple/simple.go +++ b/_examples/ssh-simple/simple.go @@ -5,7 +5,7 @@ import ( "io" "log" - "github.com/gliderlabs/ssh" + "github.com/tailscale/gliderssh" ) func main() { diff --git a/_examples/ssh-timeouts/timeouts.go b/_examples/ssh-timeouts/timeouts.go index 1dba09f..1648918 100644 --- a/_examples/ssh-timeouts/timeouts.go +++ b/_examples/ssh-timeouts/timeouts.go @@ -4,7 +4,7 @@ import ( "log" "time" - "github.com/gliderlabs/ssh" + "github.com/tailscale/gliderssh" ) var ( diff --git a/circle.yml b/circle.yml deleted file mode 100644 index c97103d..0000000 --- a/circle.yml +++ /dev/null @@ -1,26 +0,0 @@ -version: 2 -jobs: - build-go-latest: - docker: - - image: golang:latest - working_directory: /go/src/github.com/gliderlabs/ssh - steps: - - checkout - - run: go get - - run: go test -v -race - - build-go-1.12: - docker: - - image: golang:1.12 - working_directory: /go/src/github.com/gliderlabs/ssh - steps: - - checkout - - run: go get - - run: go test -v -race - -workflows: - version: 2 - build: - jobs: - - build-go-latest - - build-go-1.12 diff --git a/example_test.go b/example_test.go index e81d12e..fa9bafa 100644 --- a/example_test.go +++ b/example_test.go @@ -5,7 +5,7 @@ import ( "io" "os" - "github.com/gliderlabs/ssh" + "github.com/tailscale/gliderssh" ) func ExampleListenAndServe() { diff --git a/go.mod b/go.mod index eee9a4f..d43753f 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,10 @@ -module github.com/tailscale/ssh +module github.com/tailscale/gliderssh -go 1.12 +go 1.26 require ( github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be - golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e - golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 // indirect + golang.org/x/crypto v0.31.0 ) + +require golang.org/x/sys v0.28.0 // indirect diff --git a/go.sum b/go.sum index e283b5f..1163128 100644 --- a/go.sum +++ b/go.sum @@ -1,13 +1,8 @@ github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22 h1:RqytpXGR1iVNX7psjB3ff8y7sNFinVFvkx1c8SjBkio= -golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= diff --git a/server_test.go b/server_test.go index 8028a3a..498621e 100644 --- a/server_test.go +++ b/server_test.go @@ -40,7 +40,7 @@ func TestServerShutdown(t *testing.T) { go func() { err := s.Serve(l) if err != nil && err != ErrServerClosed { - t.Fatal(err) + t.Error(err) } }() sessDone := make(chan struct{}) @@ -51,10 +51,11 @@ func TestServerShutdown(t *testing.T) { var stdout bytes.Buffer sess.Stdout = &stdout if err := sess.Run(""); err != nil { - t.Fatal(err) + t.Error(err) + return } if !bytes.Equal(stdout.Bytes(), testBytes) { - t.Fatalf("expected = %s; got %s", testBytes, stdout.Bytes()) + t.Errorf("expected = %s; got %s", testBytes, stdout.Bytes()) } }() @@ -63,7 +64,7 @@ func TestServerShutdown(t *testing.T) { defer close(srvDone) err := s.Shutdown(context.Background()) if err != nil { - t.Fatal(err) + t.Error(err) } }() @@ -89,7 +90,7 @@ func TestServerClose(t *testing.T) { go func() { err := s.Serve(l) if err != nil && err != ErrServerClosed { - t.Fatal(err) + t.Error(err) } }() @@ -102,14 +103,14 @@ func TestServerClose(t *testing.T) { defer close(clientDoneChan) <-closeDoneChan if err := sess.Run(""); err != nil && err != io.EOF { - t.Fatal(err) + t.Error(err) } }() go func() { err := s.Close() if err != nil { - t.Fatal(err) + t.Error(err) } close(closeDoneChan) }() diff --git a/session_test.go b/session_test.go index c6ce617..a83ab19 100644 --- a/session_test.go +++ b/session_test.go @@ -230,9 +230,9 @@ func TestPty(t *testing.T) { func TestPtyResize(t *testing.T) { t.Parallel() - winch0 := Window{40, 80} - winch1 := Window{80, 160} - winch2 := Window{20, 40} + winch0 := Window{Width: 40, Height: 80} + winch1 := Window{Width: 80, Height: 160, WidthPixels: 640, HeightPixels: 1280} + winch2 := Window{Width: 20, Height: 40, WidthPixels: 160, HeightPixels: 320} winches := make(chan Window) done := make(chan bool) session, _, cleanup := newTestSession(t, &Server{ @@ -241,8 +241,15 @@ func TestPtyResize(t *testing.T) { if !isPty { t.Fatalf("expected pty but none requested") } - if ptyReq.Window != winch0 { - t.Fatalf("expected window %#v but got %#v", winch0, ptyReq.Window) + // RequestPty computes pixel dimensions from char dimensions, + // so we only check Width/Height for the initial pty request. + if ptyReq.Window.Width != winch0.Width || ptyReq.Window.Height != winch0.Height { + t.Fatalf("expected window size %dx%d but got %dx%d", + winch0.Width, winch0.Height, + ptyReq.Window.Width, ptyReq.Window.Height) + } + if ptyReq.Window.WidthPixels == 0 || ptyReq.Window.HeightPixels == 0 { + t.Fatalf("expected non-zero pixel dimensions in pty request, got %#v", ptyReq.Window) } for win := range winCh { winches <- win @@ -259,11 +266,19 @@ func TestPtyResize(t *testing.T) { t.Fatalf("expected nil but got %v", err) } gotWinch := <-winches - if gotWinch != winch0 { - t.Fatalf("expected window %#v but got %#v", winch0, gotWinch) + if gotWinch.Width != winch0.Width || gotWinch.Height != winch0.Height { + t.Fatalf("expected window size %dx%d but got %dx%d", + winch0.Width, winch0.Height, + gotWinch.Width, gotWinch.Height) + } + if gotWinch.WidthPixels == 0 || gotWinch.HeightPixels == 0 { + t.Fatalf("expected non-zero pixel dimensions, got %#v", gotWinch) + } + // winch1 — window-change sends all 4 fields per RFC 4254 section 6.7 + winchMsg := struct{ W, H, Wpx, Hpx uint32 }{ + uint32(winch1.Width), uint32(winch1.Height), + uint32(winch1.WidthPixels), uint32(winch1.HeightPixels), } - // 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") @@ -273,7 +288,10 @@ func TestPtyResize(t *testing.T) { t.Fatalf("expected window %#v but got %#v", winch1, gotWinch) } // winch2 - winchMsg = struct{ w, h uint32 }{uint32(winch2.Width), uint32(winch2.Height)} + winchMsg = struct{ W, H, Wpx, Hpx uint32 }{ + uint32(winch2.Width), uint32(winch2.Height), + uint32(winch2.WidthPixels), uint32(winch2.HeightPixels), + } ok, err = session.SendRequest("window-change", true, gossh.Marshal(&winchMsg)) if err == nil && !ok { t.Fatalf("unexpected error or bad reply on send request") diff --git a/ssh.go b/ssh.go index 22391b4..4a77013 100644 --- a/ssh.go +++ b/ssh.go @@ -43,8 +43,6 @@ type PublicKeyHandler func(ctx Context, key PublicKey) error type NoClientAuthHandler func(ctx Context) error -type BannerHandler func(ctx Context) string - // PasswordHandler is a callback for performing password authentication. type PasswordHandler func(ctx Context, password string) bool From 40cdc2de303e78dbcaa8163575cd75c1eda28449 Mon Sep 17 00:00:00 2001 From: Kristoffer Dalby Date: Wed, 11 Mar 2026 14:36:03 +0000 Subject: [PATCH 10/10] ci: add GitHub Actions workflow for Go tests Add a CI workflow modeled after tailscale/tailscale that runs on PRs, pushes to main, and merge groups. The test matrix covers amd64, amd64 with race detector, and 386. Also runs go vet and checks that build/test do not modify tracked files or create untracked files. A check_mergeability job gates on the full matrix for use as a required status check. --- .github/workflows/test.yml | 84 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..6a61a1b --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,84 @@ +# Main CI workflow. Runs build, vet, and tests on PRs and merged commits. +name: CI + +on: + push: + branches: + - "main" + pull_request: + # all PRs on all branches + merge_group: + branches: + - "main" + +concurrency: + # For PRs, later CI runs preempt previous ones. e.g. a force push on a PR + # cancels running CI jobs and starts all new ones. + # + # For non-PR pushes, concurrency.group needs to be unique for every distinct + # CI run we want to have happen. Use run_id, which in practice means all + # non-PR CI runs will be allowed to run without preempting each other. + group: ${{ github.workflow }}-$${{ github.pull_request.number || github.run_id }} + cancel-in-progress: true + +jobs: + test: + strategy: + fail-fast: false # don't abort the entire matrix if one element fails + matrix: + include: + - goarch: amd64 + - goarch: amd64 + buildflags: "-race" + - goarch: "386" # thanks yaml + runs-on: ubuntu-24.04 + steps: + - name: checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: setup Go + uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 + with: + go-version-file: go.mod + + - name: build all + if: matrix.buildflags == '' # skip on race builder + run: go build ./... + env: + GOARCH: ${{ matrix.goarch }} + + - name: vet + if: matrix.buildflags == '' && matrix.goarch == 'amd64' + run: go vet ./... + + - name: test all + run: go test ${{ matrix.buildflags }} ./... + env: + GOARCH: ${{ matrix.goarch }} + + - name: check that no tracked files changed + run: git diff --no-ext-diff --name-only --exit-code || (echo "Build/test modified the files above."; exit 1) + + - name: check that no new files were added + run: | + # Note: The "error: pathspec..." you see below is normal! + # In the success case in which there are no new untracked files, + # git ls-files complains about the pathspec not matching anything. + # That's OK. It's not worth the effort to suppress. Please ignore it. + if git ls-files --others --exclude-standard --directory --no-empty-directory --error-unmatch -- ':/*' + then + echo "Build/test created untracked files in the repo (file names above)." + exit 1 + fi + + check_mergeability: + if: always() + runs-on: ubuntu-24.04 + needs: + - test + steps: + - name: Decide if change is okay to merge + if: github.event_name != 'push' + uses: re-actors/alls-green@05ac9388f0aebcb5727afa17fcccfecd6f8ec5fe # v1.2.2 + with: + jobs: ${{ toJSON(needs) }}