Files
tunnel/internal/server/relay.go
T
TZGyn 682a341010 M1: single-node OpenTunnel-compatible server
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.
2026-10-25 11:52:45 +08:00

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 }