HTTP API per the recorded contract, bridge WebSocket, SNI relay with pass-through tunnel TLS, local CA issuance, JSON store. Verified end-to-end with the official opentunnel CLI.
178 lines
4.6 KiB
Go
178 lines
4.6 KiB
Go
package server
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"log"
|
|
"net"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Relay is the public TCP listener. It reads only the TLS ClientHello: SNI
|
|
// equal to the domain is terminated with the server's certificate (the HTTP
|
|
// API and bridge), tunnel hostnames are passed — still encrypted — to the
|
|
// bridge that claimed the route.
|
|
type Relay struct {
|
|
Domain string
|
|
Store *Store
|
|
Hub *Hub
|
|
Server *tls.Certificate // API domain certificate
|
|
APIConns chan net.Conn // terminated API connections, served by http.Server
|
|
}
|
|
|
|
// Serve accepts on ln until it is closed.
|
|
func (r *Relay) Serve(ln net.Listener) {
|
|
for {
|
|
conn, err := ln.Accept()
|
|
if err != nil {
|
|
return
|
|
}
|
|
go r.handle(conn)
|
|
}
|
|
}
|
|
|
|
func (r *Relay) handle(conn net.Conn) {
|
|
raw, sni, alpn, err := readClientHello(conn)
|
|
if err != nil {
|
|
log.Printf("relay: %s: %v", conn.RemoteAddr(), err)
|
|
conn.Close()
|
|
return
|
|
}
|
|
sni = strings.ToLower(sni)
|
|
|
|
if sni == r.Domain {
|
|
r.serveAPI(conn, raw)
|
|
return
|
|
}
|
|
|
|
tunnelID, route, ok := routeForSNI(r.Domain, sni)
|
|
if !ok {
|
|
log.Printf("relay: %s: sni %q matches no tunnel hostname", conn.RemoteAddr(), sni)
|
|
conn.Close()
|
|
return
|
|
}
|
|
tunnel, ok := r.Store.get(tunnelID)
|
|
if !ok || tunnel.Certificate == nil {
|
|
log.Printf("relay: %s: tunnel %s unknown or certificate not ready", conn.RemoteAddr(), tunnelID)
|
|
conn.Close() // unknown tunnel or certificate not ready
|
|
return
|
|
}
|
|
bridge := r.Hub.bridgeFor(tunnelID, route)
|
|
if bridge == nil {
|
|
log.Printf("relay: %s: no bridge for route %q of tunnel %s", conn.RemoteAddr(), route, tunnelID)
|
|
conn.Close()
|
|
return
|
|
}
|
|
log.Printf("relay: %s: %s -> tunnel %s route %q", conn.RemoteAddr(), sni, tunnelID, route)
|
|
bridge.openVisitor(conn, raw, conn.RemoteAddr().String(), sni, strings.Join(alpn, ","))
|
|
}
|
|
|
|
// serveAPI replays the ClientHello into a TLS termination with the server's
|
|
// certificate and hands the connection to the shared API http.Server.
|
|
func (r *Relay) serveAPI(conn net.Conn, hello []byte) {
|
|
tlsConn := tls.Server(&replayConn{Conn: conn, prefix: hello}, &tls.Config{
|
|
Certificates: []tls.Certificate{*r.Server},
|
|
NextProtos: []string{"http/1.1"},
|
|
})
|
|
conn.SetReadDeadline(time.Now().Add(clientHelloTimeout))
|
|
err := tlsConn.Handshake()
|
|
conn.SetReadDeadline(time.Time{})
|
|
if err != nil {
|
|
conn.Close()
|
|
return
|
|
}
|
|
select {
|
|
case r.APIConns <- tlsConn:
|
|
default:
|
|
log.Printf("API connection queue full; dropping %s", conn.RemoteAddr())
|
|
tlsConn.Close()
|
|
}
|
|
}
|
|
|
|
// replayConn returns the already-read ClientHello bytes before reading from
|
|
// the underlying connection again.
|
|
type replayConn struct {
|
|
net.Conn
|
|
prefix []byte
|
|
}
|
|
|
|
func (c *replayConn) Read(p []byte) (int, error) {
|
|
if len(c.prefix) > 0 {
|
|
n := copy(p, c.prefix)
|
|
c.prefix = c.prefix[n:]
|
|
return n, nil
|
|
}
|
|
return c.Conn.Read(p)
|
|
}
|
|
|
|
// routeForSNI resolves a tunnel hostname to tunnel ID and route name:
|
|
// <id>.<domain> is route "@", <route>.<id>.<domain> is that route; anything
|
|
// deeper, malformed, or outside the domain does not match.
|
|
func routeForSNI(domain, sni string) (id, route string, ok bool) {
|
|
sni = strings.ToLower(sni)
|
|
suffix := "." + domain
|
|
if sni == "" || sni == domain || !strings.HasSuffix(sni, suffix) {
|
|
return "", "", false
|
|
}
|
|
labels := strings.Split(strings.TrimSuffix(sni, suffix), ".")
|
|
switch len(labels) {
|
|
case 1:
|
|
if !routeLabel.MatchString(labels[0]) {
|
|
return "", "", false
|
|
}
|
|
return labels[0], "@", true
|
|
case 2:
|
|
if !validRoute(labels[0]) || labels[0] == "@" || !routeLabel.MatchString(labels[1]) {
|
|
return "", "", false
|
|
}
|
|
return labels[1], labels[0], true
|
|
default:
|
|
return "", "", false
|
|
}
|
|
}
|
|
|
|
// chanListener turns a channel of connections into a net.Listener, so one
|
|
// http.Server can serve the connections the relay terminates for the domain.
|
|
type chanListener struct {
|
|
conns chan net.Conn
|
|
done chan struct{}
|
|
once sync.Once
|
|
addr net.Addr
|
|
}
|
|
|
|
func NewChanListener(capacity int, addr net.Addr) *chanListener {
|
|
return &chanListener{
|
|
conns: make(chan net.Conn, capacity),
|
|
done: make(chan struct{}),
|
|
addr: addr,
|
|
}
|
|
}
|
|
|
|
func (l *chanListener) Accept() (net.Conn, error) {
|
|
select {
|
|
case conn := <-l.conns:
|
|
return conn, nil
|
|
case <-l.done:
|
|
return nil, errListenerClosed
|
|
}
|
|
}
|
|
|
|
func (l *chanListener) Close() error {
|
|
l.once.Do(func() { close(l.done) })
|
|
return nil
|
|
}
|
|
|
|
func (l *chanListener) Addr() net.Addr { return l.addr }
|
|
|
|
// Conns returns the channel the listener serves connections from.
|
|
func (l *chanListener) Conns() chan net.Conn { return l.conns }
|
|
|
|
var errListenerClosed = &netError{}
|
|
|
|
type netError struct{}
|
|
|
|
func (e *netError) Error() string { return "listener closed" }
|
|
func (e *netError) Timeout() bool { return false }
|
|
func (e *netError) Temporary() bool { return false }
|