Trevor Dixon
Trevor Dixon

Reputation: 24362

How can I serve SSH and HTTP(S) traffic from the same listener in Go?

I want to be able to serve both SSH and HTTPS connections on the same port. To do so, I need to examine the first few bytes sent by the client, and if it starts with "SSH", serve the connection one way, but let the Go HTTP server handle it if it isn't SSH.

But the http package will only work with a net.Listener. Once I accept a connection from the listener and examine the first bytes, it's too late to send the net.Conn to http.

How can I accomplish this?

Upvotes: 2

Views: 1021

Answers (1)

Trevor Dixon
Trevor Dixon

Reputation: 24362

Make two custom listeners, one for SSH connections, and one for all other connections. Then accept connections from the raw listener, peek at the first bytes, and send the connection to the appropriate listener.

l := net.Listen("tcp", ":443")
sshListener, httpListener := MuxListener(l)
go sshServer.Serve(sshListener)
go httpServer.Serve(httpListener)

MuxListener:

// MuxListener takes a net.Listener and returns two listeners, one that
// accepts connections that start with "SSH", and another that accepts
// all others. This allows SSH and HTTPS to be served from the same port.
func MuxListener(l net.Listener) (ssh net.Listener, other net.Listener) {
    sshListener, otherListener := newListener(l), newListener(l)
    go func() {
        for {
            conn, err := l.Accept()
            if err != nil {
                log.Println("Error accepting conn:", err)
                continue
            }
            conn.SetReadDeadline(time.Now().Add(time.Second * 10))
            bconn := bufferedConn{conn, bufio.NewReaderSize(conn, 3)}
            p, err := bconn.Peek(3)
            conn.SetReadDeadline(time.Time{})
            if err != nil {
                log.Println("Error peeking into conn:", err)
                continue
            }
            prefix := string(p)
            selectedListener := otherListener
            if prefix == "SSH" {
                selectedListener = sshListener
            }
            if selectedListener.accept != nil {
                selectedListener.accept <- bconn
            }
        }
    }()
    return sshListener, otherListener
}

listener:

type listener struct {
    accept chan net.Conn
    net.Listener
}

func newListener(l net.Listener) *listener {
    return &listener{
        make(chan net.Conn),
        l,
    }
}

func (l *listener) Accept() (net.Conn, error) {
    if l.accept == nil {
        return nil, errors.New("Listener closed")
    }
    return <-l.accept, nil
}

func (l *listener) Close() error {
    close(l.accept)
    l.accept = nil
    return nil
}

bufferedConn:

type bufferedConn struct {
    net.Conn
    r *bufio.Reader
}

func (b bufferedConn) Peek(n int) ([]byte, error) {
    return b.r.Peek(n)
}

func (b bufferedConn) Read(p []byte) (int, error) {
    return b.r.Read(p)
}

Upvotes: 9

Related Questions