Skip to content

Commit 836adc0

Browse files
committed
feat: add Unix forwarding server implementations
Adds optional (disabled by default) implementations of local->remote and remote->local Unix forwarding through OpenSSH's protocol extensions: - [email protected] - [email protected] - [email protected] - [email protected] Adds tests for Unix forwarding, reverse Unix forwarding and reverse TCP forwarding.
1 parent db09465 commit 836adc0

9 files changed

+579
-30
lines changed

options_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func TestPasswordAuth(t *testing.T) {
4949

5050
func TestPasswordAuthBadPass(t *testing.T) {
5151
t.Parallel()
52-
l := newLocalListener()
52+
l := newLocalTCPListener()
5353
srv := &Server{Handler: func(s Session) {}}
5454
srv.SetOption(PasswordAuth(func(ctx Context, password string) bool {
5555
return false

server.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ type Server struct {
4444
PtyCallback PtyCallback // callback for allowing PTY sessions, allows all if nil
4545
ConnCallback ConnCallback // optional callback for wrapping net.Conn before handling
4646
LocalPortForwardingCallback LocalPortForwardingCallback // callback for allowing local port forwarding, denies all if nil
47+
LocalUnixForwardingCallback LocalUnixForwardingCallback // callback for allowing local unix forwarding ([email protected]), denies all if nil
4748
ReversePortForwardingCallback ReversePortForwardingCallback // callback for allowing reverse port forwarding, denies all if nil
49+
ReverseUnixForwardingCallback ReverseUnixForwardingCallback // callback for allowing reverse unix forwarding ([email protected]), denies all if nil
4850
ServerConfigCallback ServerConfigCallback // callback for configuring detailed SSH options
4951
SessionRequestCallback SessionRequestCallback // callback for allowing or denying SSH sessions
5052

server_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func TestAddHostKey(t *testing.T) {
2929
}
3030

3131
func TestServerShutdown(t *testing.T) {
32-
l := newLocalListener()
32+
l := newLocalTCPListener()
3333
testBytes := []byte("Hello world\n")
3434
s := &Server{
3535
Handler: func(s Session) {
@@ -80,7 +80,7 @@ func TestServerShutdown(t *testing.T) {
8080
}
8181

8282
func TestServerClose(t *testing.T) {
83-
l := newLocalListener()
83+
l := newLocalTCPListener()
8484
s := &Server{
8585
Handler: func(s Session) {
8686
time.Sleep(5 * time.Second)

session_test.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,25 @@ func (srv *Server) serveOnce(l net.Listener) error {
2020
return e
2121
}
2222
srv.ChannelHandlers = map[string]ChannelHandler{
23-
"session": DefaultSessionHandler,
24-
"direct-tcpip": DirectTCPIPHandler,
23+
"session": DefaultSessionHandler,
24+
"direct-tcpip": DirectTCPIPHandler,
25+
"[email protected]": DirectStreamLocalHandler,
2526
}
27+
28+
forwardedTCPHandler := &ForwardedTCPHandler{}
29+
forwardedUnixHandler := &ForwardedUnixHandler{}
30+
srv.RequestHandlers = map[string]RequestHandler{
31+
"tcpip-forward": forwardedTCPHandler.HandleSSHRequest,
32+
"cancel-tcpip-forward": forwardedTCPHandler.HandleSSHRequest,
33+
"[email protected]": forwardedUnixHandler.HandleSSHRequest,
34+
"[email protected]": forwardedUnixHandler.HandleSSHRequest,
35+
}
36+
2637
srv.HandleConn(conn)
2738
return nil
2839
}
2940

30-
func newLocalListener() net.Listener {
41+
func newLocalTCPListener() net.Listener {
3142
l, err := net.Listen("tcp", "127.0.0.1:0")
3243
if err != nil {
3344
if l, err = net.Listen("tcp6", "[::1]:0"); err != nil {
@@ -64,7 +75,7 @@ func newClientSession(t *testing.T, addr string, config *gossh.ClientConfig) (*g
6475
}
6576

6677
func newTestSession(t *testing.T, srv *Server, cfg *gossh.ClientConfig) (*gossh.Session, *gossh.Client, func()) {
67-
l := newLocalListener()
78+
l := newLocalTCPListener()
6879
go srv.serveOnce(l)
6980
return newClientSession(t, l.Addr().String(), cfg)
7081
}

ssh.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,17 @@ type ConnCallback func(ctx Context, conn net.Conn) net.Conn
5858
// LocalPortForwardingCallback is a hook for allowing port forwarding
5959
type LocalPortForwardingCallback func(ctx Context, destinationHost string, destinationPort uint32) bool
6060

61+
// LocalUnixForwardingCallback is a hook for allowing unix forwarding
62+
63+
type LocalUnixForwardingCallback func(ctx Context, socketPath string) bool
64+
6165
// ReversePortForwardingCallback is a hook for allowing reverse port forwarding
6266
type ReversePortForwardingCallback func(ctx Context, bindHost string, bindPort uint32) bool
6367

68+
// ReverseUnixForwardingCallback is a hook for allowing reverse unix forwarding
69+
70+
type ReverseUnixForwardingCallback func(ctx Context, socketPath string) bool
71+
6472
// ServerConfigCallback is a hook for creating custom default server configs
6573
type ServerConfigCallback func(ctx Context) *gossh.ServerConfig
6674

streamlocal.go

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
package ssh
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"net"
7+
"os"
8+
"path/filepath"
9+
"sync"
10+
11+
gossh "golang.org/x/crypto/ssh"
12+
)
13+
14+
const (
15+
forwardedUnixChannelType = "[email protected]"
16+
)
17+
18+
// directStreamLocalChannelData data struct as specified in OpenSSH's protocol
19+
// extensions document, Section 2.4.
20+
// https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL?annotate=HEAD
21+
type directStreamLocalChannelData struct {
22+
SocketPath string
23+
24+
Reserved1 string
25+
Reserved2 uint32
26+
}
27+
28+
// DirectStreamLocalHandler provides Unix forwarding from client -> server. It
29+
// can be enabled by adding it to the server's ChannelHandlers under
30+
31+
//
32+
// Unix socket support on Windows is not widely available, so this handler may
33+
// not work on all Windows installations and is not tested on Windows.
34+
func DirectStreamLocalHandler(srv *Server, _ *gossh.ServerConn, newChan gossh.NewChannel, ctx Context) {
35+
var d directStreamLocalChannelData
36+
err := gossh.Unmarshal(newChan.ExtraData(), &d)
37+
if err != nil {
38+
_ = newChan.Reject(gossh.ConnectionFailed, "error parsing direct-streamlocal data: "+err.Error())
39+
return
40+
}
41+
42+
if srv.LocalUnixForwardingCallback == nil || !srv.LocalUnixForwardingCallback(ctx, d.SocketPath) {
43+
newChan.Reject(gossh.Prohibited, "unix forwarding is disabled")
44+
return
45+
}
46+
47+
var dialer net.Dialer
48+
dconn, err := dialer.DialContext(ctx, "unix", d.SocketPath)
49+
if err != nil {
50+
_ = newChan.Reject(gossh.ConnectionFailed, fmt.Sprintf("dial unix socket %q: %+v", d.SocketPath, err.Error()))
51+
return
52+
}
53+
54+
ch, reqs, err := newChan.Accept()
55+
if err != nil {
56+
_ = dconn.Close()
57+
return
58+
}
59+
go gossh.DiscardRequests(reqs)
60+
61+
bicopy(ctx, ch, dconn)
62+
}
63+
64+
// remoteUnixForwardRequest describes the extra data sent in a
65+
// [email protected] containing the socket path to bind to.
66+
type remoteUnixForwardRequest struct {
67+
SocketPath string
68+
}
69+
70+
// remoteUnixForwardChannelData describes the data sent as the payload in the new
71+
// channel request when a Unix connection is accepted by the listener.
72+
type remoteUnixForwardChannelData struct {
73+
SocketPath string
74+
Reserved uint32
75+
}
76+
77+
// ForwardedUnixHandler can be enabled by creating a ForwardedUnixHandler and
78+
// adding the HandleSSHRequest callback to the server's RequestHandlers under
79+
80+
81+
//
82+
// Unix socket support on Windows is not widely available, so this handler may
83+
// not work on all Windows installations and is not tested on Windows.
84+
type ForwardedUnixHandler struct {
85+
sync.Mutex
86+
forwards map[string]net.Listener
87+
}
88+
89+
func (h *ForwardedUnixHandler) HandleSSHRequest(ctx Context, srv *Server, req *gossh.Request) (bool, []byte) {
90+
h.Lock()
91+
if h.forwards == nil {
92+
h.forwards = make(map[string]net.Listener)
93+
}
94+
h.Unlock()
95+
conn, ok := ctx.Value(ContextKeyConn).(*gossh.ServerConn)
96+
if !ok {
97+
// TODO: log cast failure
98+
return false, nil
99+
}
100+
101+
switch req.Type {
102+
103+
var reqPayload remoteUnixForwardRequest
104+
err := gossh.Unmarshal(req.Payload, &reqPayload)
105+
if err != nil {
106+
// TODO: log parse failure
107+
return false, nil
108+
}
109+
110+
if srv.ReverseUnixForwardingCallback == nil || !srv.ReverseUnixForwardingCallback(ctx, reqPayload.SocketPath) {
111+
return false, []byte("unix forwarding is disabled")
112+
}
113+
114+
addr := reqPayload.SocketPath
115+
h.Lock()
116+
_, ok := h.forwards[addr]
117+
h.Unlock()
118+
if ok {
119+
// TODO: log failure
120+
return false, nil
121+
}
122+
123+
// Create socket parent dir if not exists.
124+
parentDir := filepath.Dir(addr)
125+
err = os.MkdirAll(parentDir, 0700)
126+
if err != nil {
127+
// TODO: log mkdir failure
128+
return false, nil
129+
}
130+
131+
ln, err := net.Listen("unix", addr)
132+
if err != nil {
133+
// TODO: log unix listen failure
134+
return false, nil
135+
}
136+
137+
// The listener needs to successfully start before it can be added to
138+
// the map, so we don't have to worry about checking for an existing
139+
// listener as you can't listen on the same socket twice.
140+
//
141+
// This is also what the TCP version of this code does.
142+
h.Lock()
143+
h.forwards[addr] = ln
144+
h.Unlock()
145+
146+
ctx, cancel := context.WithCancel(ctx)
147+
go func() {
148+
<-ctx.Done()
149+
_ = ln.Close()
150+
}()
151+
go func() {
152+
defer cancel()
153+
154+
for {
155+
c, err := ln.Accept()
156+
if err != nil {
157+
// closed below
158+
break
159+
}
160+
payload := gossh.Marshal(&remoteUnixForwardChannelData{
161+
SocketPath: addr,
162+
})
163+
164+
go func() {
165+
ch, reqs, err := conn.OpenChannel(forwardedUnixChannelType, payload)
166+
if err != nil {
167+
_ = c.Close()
168+
return
169+
}
170+
go gossh.DiscardRequests(reqs)
171+
bicopy(ctx, ch, c)
172+
}()
173+
}
174+
175+
h.Lock()
176+
ln2, ok := h.forwards[addr]
177+
if ok && ln2 == ln {
178+
delete(h.forwards, addr)
179+
}
180+
h.Unlock()
181+
_ = ln.Close()
182+
}()
183+
184+
return true, nil
185+
186+
187+
var reqPayload remoteUnixForwardRequest
188+
err := gossh.Unmarshal(req.Payload, &reqPayload)
189+
if err != nil {
190+
// TODO: log parse failure
191+
return false, nil
192+
}
193+
h.Lock()
194+
ln, ok := h.forwards[reqPayload.SocketPath]
195+
h.Unlock()
196+
if ok {
197+
_ = ln.Close()
198+
}
199+
return true, nil
200+
201+
default:
202+
return false, nil
203+
}
204+
}

0 commit comments

Comments
 (0)