Files
TZGyn 437a2e447e Issue real certificates via ACME DNS-01
Tunnels now get a per-tunnel certificate from the configured ACME CA
(Let's Encrypt by default) with DNS-01 through Porkbun, keeping the
client's CSR and key. Issuance is asynchronous (issuing/ready/failed);
the account persists in the store, renewals run in a 30-day window with
retries, and a failed renewal keeps the old certificate serving.
Switching from the local CA reissues existing tunnels automatically,
and the admin dashboard shows the issuer and certificate state.

Verified against Pebble and Let's Encrypt staging.
2026-10-25 23:33:52 +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 || !certReady(tunnel.Certificate) {
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 }