Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -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) }}
85 changes: 18 additions & 67 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,96 +1,47 @@
# 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

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

<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)
8 changes: 4 additions & 4 deletions _examples/ssh-docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 .`
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion _examples/ssh-docker/docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
2 changes: 1 addition & 1 deletion _examples/ssh-forwardagent/forwardagent.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"log"
"os/exec"

"github.com/gliderlabs/ssh"
"github.com/tailscale/gliderssh"
)

func main() {
Expand Down
2 changes: 1 addition & 1 deletion _examples/ssh-pty/pty.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
"syscall"
"unsafe"

"github.com/gliderlabs/ssh"
"github.com/tailscale/gliderssh"
"github.com/creack/pty"
)

Expand Down
2 changes: 1 addition & 1 deletion _examples/ssh-publickey/public_key.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"io"
"log"

"github.com/gliderlabs/ssh"
"github.com/tailscale/gliderssh"
gossh "golang.org/x/crypto/ssh"
)

Expand Down
2 changes: 1 addition & 1 deletion _examples/ssh-remoteforward/portforward.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"io"
"log"

"github.com/gliderlabs/ssh"
"github.com/tailscale/gliderssh"
)

func main() {
Expand Down
2 changes: 1 addition & 1 deletion _examples/ssh-simple/simple.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import (
"io"
"log"

"github.com/gliderlabs/ssh"
"github.com/tailscale/gliderssh"
)

func main() {
Expand Down
2 changes: 1 addition & 1 deletion _examples/ssh-timeouts/timeouts.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"log"
"time"

"github.com/gliderlabs/ssh"
"github.com/tailscale/gliderssh"
)

var (
Expand Down
26 changes: 0 additions & 26 deletions circle.yml

This file was deleted.

24 changes: 12 additions & 12 deletions doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 16 additions & 6 deletions example_test.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package ssh_test

import (
"errors"
"io"
"io/ioutil"
"os"

"github.com/gliderlabs/ssh"
"github.com/tailscale/gliderssh"
)

func ExampleListenAndServe() {
Expand All @@ -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
}),
)
}
Expand Down
Loading