mirror of https://github.com/tailscale/tailscale/
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>
pull/4296/head
parent
6fecc16c3b
commit
5a44f9f5b5
@ -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.
|
@ -0,0 +1,96 @@
|
|||||||
|
# gliderlabs/ssh
|
||||||
|
|
||||||
|
[![GoDoc](https://godoc.org/tailscale.com/tempfork/gliderlabs/ssh?status.svg)](https://godoc.org/github.com/gliderlabs/ssh)
|
||||||
|
[![CircleCI](https://img.shields.io/circleci/project/github/gliderlabs/ssh.svg)](https://circleci.com/gh/gliderlabs/ssh)
|
||||||
|
[![Go Report Card](https://goreportcard.com/badge/tailscale.com/tempfork/gliderlabs/ssh)](https://goreportcard.com/report/github.com/gliderlabs/ssh)
|
||||||
|
[![OpenCollective](https://opencollective.com/ssh/sponsors/badge.svg)](#sponsors)
|
||||||
|
[![Slack](http://slack.gliderlabs.com/badge.svg)](http://slack.gliderlabs.com)
|
||||||
|
[![Email Updates](https://img.shields.io/badge/updates-subscribe-yellow.svg)](https://app.convertkit.com/landing_pages/243312)
|
||||||
|
|
||||||
|
> The Glider Labs SSH server package is dope. —[@bradfitz](https://twitter.com/bradfitz), Go team member
|
||||||
|
|
||||||
|
This Go package wraps the [crypto/ssh
|
||||||
|
package](https://godoc.org/golang.org/x/crypto/ssh) with a higher-level API for
|
||||||
|
building SSH servers. The goal of the API was to make it as simple as using
|
||||||
|
[net/http](https://golang.org/pkg/net/http/), so the API is very similar:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"tailscale.com/tempfork/gliderlabs/ssh"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ssh.Handle(func(s ssh.Session) {
|
||||||
|
io.WriteString(s, "Hello world\n")
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Fatal(ssh.ListenAndServe(":2222", nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
This package was built by [@progrium](https://twitter.com/progrium) after working on nearly a dozen projects at Glider Labs using SSH and collaborating with [@shazow](https://twitter.com/shazow) (known for [ssh-chat](https://github.com/shazow/ssh-chat)).
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
A bunch of great examples are in the `_examples` directory.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
[See GoDoc reference.](https://godoc.org/tailscale.com/tempfork/gliderlabs/ssh)
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Pull requests are welcome! However, since this project is very much about API
|
||||||
|
design, please submit API changes as issues to discuss before submitting PRs.
|
||||||
|
|
||||||
|
Also, you can [join our Slack](http://slack.gliderlabs.com) to discuss as well.
|
||||||
|
|
||||||
|
## Roadmap
|
||||||
|
|
||||||
|
* Non-session channel handlers
|
||||||
|
* Cleanup callback API
|
||||||
|
* 1.0 release
|
||||||
|
* High-level client?
|
||||||
|
|
||||||
|
## Sponsors
|
||||||
|
|
||||||
|
Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/ssh#sponsor)]
|
||||||
|
|
||||||
|
<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)
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
@ -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"))
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
Loading…
Reference in New Issue