Compare commits

...

7 Commits

Author SHA1 Message Date
Merlijn B. W. Wajer deca5d5d13 Remove address limitation for direct-tcpip for now 7 years ago
Merlijn B. W. Wajer cba5592d42 Fix direct-tcpip dial for IPV6 7 years ago
Merlijn B. W. Wajer a86d824dda Mention the client in most log statements 7 years ago
Merlijn B. W. Wajer 665ec7c7ee Add (and mention) init script 7 years ago
Merlijn B. W. Wajer 642d57f1f7 Add notes on CAP_NET_BIND_SERVICE 7 years ago
Merlijn Wajer 62cf5388d0 ListenMutex is now per client. 7 years ago
Merlijn Wajer 5c5d9bc213 Fix race condition in listen code 7 years ago
  1. 15
      README.rst
  2. 4
      TODO
  3. 1
      alpine/go-sshd
  4. 39
      gentoo/go-sshd
  5. 101
      sshd.go

@ -17,3 +17,18 @@ Same as OpenSSH authorized_keys format.
The options field contains the ports that are allowed to be forwarded, colon separated:: The options field contains the ports that are allowed to be forwarded, colon separated::
ports=3333:4444 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHPWEWu85yECrbmtL38wlFua3tBSqxTekCX/aU+dku+w COMMENTHERE ports=3333:4444 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHPWEWu85yECrbmtL38wlFua3tBSqxTekCX/aU+dku+w COMMENTHERE
Running as non-root user
========================
You should not run this program as root. Due to the way Go is implemented,
setuid is non-trivial, so instead you need to set the CAP_NET_BIND_SERVICE
capability on the resulting binary:
setcap 'cap_net_bind_service=+ep' go-sshd
Init script
===========
There is an init script for gentoo/alpine (OpenRC) users. SSHD_LISTEN needs to
be set in /etc/conf.d/go-sshd and the init-script goes in /etc/init.d/go-sshd

@ -1,4 +1,8 @@
* Make sure to not run this as root (setuid doesn't work well), so use NET capabilities * Make sure to not run this as root (setuid doesn't work well), so use NET capabilities
* Allow limiting the hosts that one can connect to use direct-tcpip (right now
all hosts are allowed)
* Allow lifting restrictions on what clients can bind on with forwarded-tcpip
* Check assertions and TODOs. * Check assertions and TODOs.
* Look if/where we want to set deadlines on open sockets * Look if/where we want to set deadlines on open sockets
* Go through all log.Println calls, and make sure they are unique(?) and * Go through all log.Println calls, and make sure they are unique(?) and

@ -0,0 +1 @@
gentoo/go-sshd

@ -0,0 +1,39 @@
#!/sbin/openrc-run
# Copyright 1999-2017 Gentoo Foundation
# Distributed under the terms of the GNU General Public License v2
description="Go Secure Shell server"
description_reload="Reload configuration"
extra_started_commands="reload"
: ${SSHD_PIDFILE:=/run/${SVCNAME}.pid}
: ${SSHD_BINARY:=/usr/local/bin/go-sshd}
: ${SSHD_LISTEN:="-listenaddr :1 -listenport 8822"}
: ${SSHD_LOG:="/var/log/mcs/${SVCNAME}"}
start() {
ebegin "Starting ${SVCNAME}"
start-stop-daemon --start --exec "${SSHD_BINARY}" \
--make-pidfile --pidfile "${SSHD_PIDFILE}" \
--background \
--user ${SSHD_USER} --group ${SSHD_GROUP} \
--stderr "${SSHD_LOG}" \
-- ${SSHD_OPTS} ${SSHD_LISTEN} -hostkey /etc/go-sshd/tunnel \
-authorisedkeys /etc/go-sshd/authorized_keys
eend $?
}
stop() {
ebegin "Stopping ${SVCNAME}"
start-stop-daemon --stop --exec "${SSHD_BINARY}" \
--pidfile "${SSHD_PIDFILE}" --quiet
eend $?
}
reload() {
ebegin "Reloading ${SVCNAME}"
start-stop-daemon --signal USR1 \
--exec "${SSHD_BINARY}" --pidfile "${SSHD_PIDFILE}"
eend $?
}

@ -39,6 +39,8 @@ type sshClient struct {
Listeners map[string]net.Listener Listeners map[string]net.Listener
AllowedLocalPorts []uint32 AllowedLocalPorts []uint32
AllowedRemotePorts []uint32 AllowedRemotePorts []uint32
Stopping bool
ListenMutex sync.Mutex
} }
type bindInfo struct { type bindInfo struct {
@ -87,9 +89,9 @@ func main() {
config := &ssh.ServerConfig{ config := &ssh.ServerConfig{
PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) { PublicKeyCallback: func(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, error) {
authmutex.Lock()
defer authmutex.Unlock()
if deviceinfo, found := authorisedKeys[string(key.Marshal())]; found { if deviceinfo, found := authorisedKeys[string(key.Marshal())]; found {
authmutex.Lock()
defer authmutex.Unlock()
return &ssh.Permissions{ return &ssh.Permissions{
CriticalOptions: map[string]string{"name": deviceinfo.Comment, CriticalOptions: map[string]string{"name": deviceinfo.Comment,
"localports": deviceinfo.LocalPorts, "localports": deviceinfo.LocalPorts,
@ -123,16 +125,16 @@ func main() {
go func() { go func() {
sshConn, chans, reqs, err := ssh.NewServerConn(tcpConn, config) sshConn, chans, reqs, err := ssh.NewServerConn(tcpConn, config)
if err != nil { if err != nil {
log.Printf("Failed to handshake (%s)", err) log.Printf("Failed to handshake: %s (rip: %v)", err, tcpConn.RemoteAddr())
return return
} }
client := sshClient{sshConn.Permissions.CriticalOptions["name"], sshConn, make(map[string]net.Listener), nil, nil} client := sshClient{sshConn.Permissions.CriticalOptions["name"], sshConn, make(map[string]net.Listener), nil, nil, false, sync.Mutex{}}
allowedLocalPorts := sshConn.Permissions.CriticalOptions["localports"] allowedLocalPorts := sshConn.Permissions.CriticalOptions["localports"]
allowedRemotePorts := sshConn.Permissions.CriticalOptions["remoteports"] allowedRemotePorts := sshConn.Permissions.CriticalOptions["remoteports"]
if *verbose { if *verbose {
log.Printf("Connection from \"%s\", %s (%s). Allowed local ports: %s remote ports: %s", client.Name, sshConn.RemoteAddr(), sshConn.ClientVersion(), allowedLocalPorts, allowedRemotePorts) log.Printf("[%s] Connection from %s (%s). Allowed local ports: %s remote ports: %s", client.Name, sshConn.RemoteAddr(), sshConn.ClientVersion(), allowedLocalPorts, allowedRemotePorts)
} }
// Parsing a second time should not error, so we can ignore the error // Parsing a second time should not error, so we can ignore the error
@ -142,17 +144,19 @@ func main() {
go func() { go func() {
err := client.Conn.Wait() err := client.Conn.Wait()
client.ListenMutex.Lock()
client.Stopping = true
if *verbose { if *verbose {
log.Printf("SSH connection closed for client %s: %s", client.Name, err) log.Printf("[%s] SSH connection closed: %s", client.Name, err)
} }
// TODO: Make this safe? Is it impossible for cancel code to be
// running at this point?
for bind, listener := range client.Listeners { for bind, listener := range client.Listeners {
if *verbose { if *verbose {
log.Printf("Closing listener bound to %s", bind) log.Printf("[%s] Closing listener bound to %s", client.Name, bind)
} }
listener.Close() listener.Close()
} }
client.ListenMutex.Unlock()
}() }()
go handleRequest(&client, reqs) go handleRequest(&client, reqs)
@ -170,14 +174,14 @@ func handleChannels(client *sshClient, chans <-chan ssh.NewChannel) {
func handleChannel(client *sshClient, newChannel ssh.NewChannel) { func handleChannel(client *sshClient, newChannel ssh.NewChannel) {
if *verbose { if *verbose {
log.Println("Channel type:", newChannel.ChannelType()) log.Printf("[%s] Channel type: %v", client.Name, newChannel.ChannelType())
} }
if t := newChannel.ChannelType(); t == "direct-tcpip" { if t := newChannel.ChannelType(); t == "direct-tcpip" {
handleDirect(client, newChannel) handleDirect(client, newChannel)
return return
} }
newChannel.Reject(ssh.Prohibited, fmt.Sprintf("Only \"direct-tcpip\" is accepted")) newChannel.Reject(ssh.Prohibited, fmt.Sprintf("Only \"direct-tcpip\", \"forwarded-tcpip\" and \"cancel-tcpip-forward\" are accepted"))
/* /*
// TODO: USE THIS ONLY FOR USING SSH ESCAPE SEQUENCES // TODO: USE THIS ONLY FOR USING SSH ESCAPE SEQUENCES
c, _, err := newChannel.Accept() c, _, err := newChannel.Accept()
@ -196,39 +200,42 @@ func handleChannel(client *sshClient, newChannel ssh.NewChannel) {
func handleDirect(client *sshClient, newChannel ssh.NewChannel) { func handleDirect(client *sshClient, newChannel ssh.NewChannel) {
var payload directTCPPayload var payload directTCPPayload
if err := ssh.Unmarshal(newChannel.ExtraData(), &payload); err != nil { if err := ssh.Unmarshal(newChannel.ExtraData(), &payload); err != nil {
log.Printf("Could not unmarshal extra data: %s\n", err) log.Printf("[%s] Could not unmarshal extra data: %s", client.Name, err)
newChannel.Reject(ssh.Prohibited, fmt.Sprintf("Bad payload")) newChannel.Reject(ssh.Prohibited, fmt.Sprintf("Bad payload"))
return return
} }
if payload.Addr != "localhost" { /*
log.Printf("Tried to connect to prohibited host: %s", payload.Addr) // XXX: Is this sensible?
newChannel.Reject(ssh.Prohibited, fmt.Sprintf("Bad addr")) if payload.Addr != "localhost" && payload.Addr != "::1" && payload.Addr != "127.0.0.1" {
return log.Printf("[%s] Tried to connect to prohibited host: %s", client.Name, payload.Addr)
} newChannel.Reject(ssh.Prohibited, fmt.Sprintf("Bad addr"))
return
}
*/
if !portPermitted(payload.Port, client.AllowedLocalPorts) { if !portPermitted(payload.Port, client.AllowedLocalPorts) {
newChannel.Reject(ssh.Prohibited, fmt.Sprintf("Bad port")) newChannel.Reject(ssh.Prohibited, fmt.Sprintf("Bad port"))
log.Printf("Tried to connect to prohibited port: %d", payload.Port) log.Printf("[%s] Tried to connect to prohibited port: %d", client.Name, payload.Port)
return return
} }
connection, requests, err := newChannel.Accept() connection, requests, err := newChannel.Accept()
if err != nil { if err != nil {
log.Printf("Could not accept channel (%s)", err) log.Printf("[%s] Could not accept channel (%s)", client.Name, err)
return return
} }
go ssh.DiscardRequests(requests) go ssh.DiscardRequests(requests)
addr := fmt.Sprintf("%s:%d", payload.Addr, payload.Port) addr := fmt.Sprintf("[%s]:%d", payload.Addr, payload.Port)
if *verbose { if *verbose {
log.Println("Dialing:", addr) log.Printf("[%s] Dialing: %s", client.Name, addr)
} }
rconn, err := net.Dial("tcp", addr) rconn, err := net.Dial("tcp", addr)
if err != nil { if err != nil {
log.Printf("Could not dial remote (%s)", err) log.Printf("[%s] Could not dial remote (%s)", client.Name, err)
connection.Close() connection.Close()
return return
} }
@ -239,24 +246,24 @@ func handleDirect(client *sshClient, newChannel ssh.NewChannel) {
func handleTcpIpForward(client *sshClient, req *ssh.Request) (net.Listener, *bindInfo, error) { func handleTcpIpForward(client *sshClient, req *ssh.Request) (net.Listener, *bindInfo, error) {
var payload tcpIpForwardPayload var payload tcpIpForwardPayload
if err := ssh.Unmarshal(req.Payload, &payload); err != nil { if err := ssh.Unmarshal(req.Payload, &payload); err != nil {
log.Println("Unable to unmarshal payload") log.Printf("[%s] Unable to unmarshal payload", client.Name)
req.Reply(false, []byte{}) req.Reply(false, []byte{})
return nil, nil, fmt.Errorf("Unable to parse payload") return nil, nil, fmt.Errorf("Unable to parse payload")
} }
if *verbose { if *verbose {
log.Println("Request:", req.Type, req.WantReply, payload) log.Printf("[%s] Request: %s %v %v", client.Name, req.Type, req.WantReply, payload)
log.Printf("Request to listen on %s:%d", payload.Addr, payload.Port) log.Printf("[%s] Request to listen on %s:%d", client.Name, payload.Addr, payload.Port)
} }
if payload.Addr != "localhost" && payload.Addr != "" { if payload.Addr != "localhost" && payload.Addr != "" {
log.Printf("Payload address is not \"localhost\" or empty") log.Printf("[%s] Payload address is not \"localhost\" or empty: %s", client.Name, payload.Addr)
req.Reply(false, []byte{}) req.Reply(false, []byte{})
return nil, nil, fmt.Errorf("Address is not permitted") return nil, nil, fmt.Errorf("Address is not permitted")
} }
if !portPermitted(payload.Port, client.AllowedRemotePorts) { if !portPermitted(payload.Port, client.AllowedRemotePorts) {
log.Printf("Port is not permitted.") log.Printf("[%s] Port is not permitted: %d", client.Name, payload.Port)
req.Reply(false, []byte{}) req.Reply(false, []byte{})
return nil, nil, fmt.Errorf("Port is not permitted") return nil, nil, fmt.Errorf("Port is not permitted")
} }
@ -267,7 +274,7 @@ func handleTcpIpForward(client *sshClient, req *ssh.Request) (net.Listener, *bin
bind := fmt.Sprintf("%s:%d", laddr, lport) bind := fmt.Sprintf("%s:%d", laddr, lport)
ln, err := net.Listen("tcp", bind) ln, err := net.Listen("tcp", bind)
if err != nil { if err != nil {
log.Printf("Listen failed for %s", bind) log.Printf("[%s] Listen failed for %s", client.Name, bind)
req.Reply(false, []byte{}) req.Reply(false, []byte{})
return nil, nil, err return nil, nil, err
} }
@ -287,11 +294,11 @@ func handleListener(client *sshClient, bindinfo *bindInfo, listener net.Listener
if err != nil { if err != nil {
neterr := err.(net.Error) neterr := err.(net.Error)
if neterr.Timeout() { if neterr.Timeout() {
log.Println("Accept failed with timeout:", err) log.Printf("[%s] Accept failed with timeout: %s", client.Name, err)
continue continue
} }
if neterr.Temporary() { if neterr.Temporary() {
log.Println("Accept failed with temporary:", err) log.Printf("[%s] Accept failed with temporary: %s", client.Name, err)
continue continue
} }
@ -313,13 +320,12 @@ func handleForwardTcpIp(client *sshClient, bindinfo *bindInfo, lconn net.Conn) {
// Open channel with client // Open channel with client
c, requests, err := client.Conn.OpenChannel("forwarded-tcpip", mpayload) c, requests, err := client.Conn.OpenChannel("forwarded-tcpip", mpayload)
if err != nil { if err != nil {
log.Printf("Error: %s", err) log.Printf("[%s] Unable to get channel: %s. Hanging up requesting party!", client.Name, err)
log.Println("Unable to get channel. Hanging up requesting party!")
lconn.Close() lconn.Close()
return return
} }
if *verbose { if *verbose {
log.Printf("Channel opened for client %s", client.Name) log.Printf("[%s] Channel opened for client", client.Name)
} }
go ssh.DiscardRequests(requests) go ssh.DiscardRequests(requests)
@ -328,11 +334,11 @@ func handleForwardTcpIp(client *sshClient, bindinfo *bindInfo, lconn net.Conn) {
func handleTcpIPForwardCancel(client *sshClient, req *ssh.Request) { func handleTcpIPForwardCancel(client *sshClient, req *ssh.Request) {
if *verbose { if *verbose {
log.Println("Cancel called by client", client) log.Printf("[%s] \"cancel-tcpip-forward\" called by client", client.Name)
} }
var payload tcpIpForwardCancelPayload var payload tcpIpForwardCancelPayload
if err := ssh.Unmarshal(req.Payload, &payload); err != nil { if err := ssh.Unmarshal(req.Payload, &payload); err != nil {
log.Println("Unable to unmarshal cancel payload") log.Printf("[%s] Unable to unmarshal cancel payload", client.Name)
req.Reply(false, []byte{}) req.Reply(false, []byte{})
} }
@ -354,7 +360,7 @@ func serve(cssh ssh.Channel, conn net.Conn, client *sshClient) {
cssh.Close() cssh.Close()
conn.Close() conn.Close()
if *verbose { if *verbose {
log.Printf("Channel closed for client: %s", client.Name) log.Printf("[%s] Channel closed.", client.Name)
} }
} }
@ -435,8 +441,12 @@ func registerReloadSignal() {
go func() { go func() {
for sig := range c { for sig := range c {
log.Printf("Received signal: \"%s\". Reloading authorised keys.", sig.String()) if sig == syscall.SIGUSR1 {
loadAuthorisedKeys(*authorisedkeys) log.Printf("Received signal: SIGUSR1. Reloading authorised keys.")
loadAuthorisedKeys(*authorisedkeys)
} else {
log.Printf("Received unexpected signal: \"%s\".", sig.String())
}
} }
}() }()
@ -445,21 +455,34 @@ func registerReloadSignal() {
func handleRequest(client *sshClient, reqs <-chan *ssh.Request) { func handleRequest(client *sshClient, reqs <-chan *ssh.Request) {
for req := range reqs { for req := range reqs {
if *verbose { if *verbose {
log.Println("Out of band request:", req.Type, req.WantReply) log.Printf("[%s] Out of band request: %v %v", client.Name, req.Type, req.WantReply)
} }
// RFC4254: 7.1 for forwarding // RFC4254: 7.1 for forwarding
if req.Type == "tcpip-forward" { if req.Type == "tcpip-forward" {
client.ListenMutex.Lock()
/* If we are closing, do not set up a new listener */
if client.Stopping {
client.ListenMutex.Unlock()
req.Reply(false, []byte{})
continue
}
listener, bindinfo, err := handleTcpIpForward(client, req) listener, bindinfo, err := handleTcpIpForward(client, req)
if err != nil { if err != nil {
client.ListenMutex.Unlock()
continue continue
} }
client.Listeners[bindinfo.Bound] = listener client.Listeners[bindinfo.Bound] = listener
client.ListenMutex.Unlock()
go handleListener(client, bindinfo, listener) go handleListener(client, bindinfo, listener)
continue continue
} else if req.Type == "cancel-tcpip-forward" { } else if req.Type == "cancel-tcpip-forward" {
client.ListenMutex.Lock()
handleTcpIPForwardCancel(client, req) handleTcpIPForwardCancel(client, req)
client.ListenMutex.Unlock()
continue continue
} else { } else {
// Discard everything else // Discard everything else

Loading…
Cancel
Save