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