Files
netbird/shared/relay/tls/server_dev.go
Claude 078c323ef3 relay: add WebTransport listener + WASM client, share UDP/443 via ALPN mux
The relay now accepts WebTransport sessions on the same UDP socket that
serves raw QUIC. The ALPN-multiplexing QUIC listener owns the socket and
dispatches incoming connections: "nb-quic" continues to the existing
relay handler, "h3" is handed to webtransport-go via http3.Server.
Browsers reach the relay over 443/udp without a second port.

Client side:
- Native builds keep using raw QUIC (no WT dialer registered).
- WASM/browser builds gain a WebTransport dialer that bridges syscall/js
  to the browser's WebTransport API and uses datagrams (matching the
  native QUIC dialer's semantics — no head-of-line blocking).
- The race dialer learned a transport hint so clients skip dialers a
  given relay has not advertised.

Management protocol carries the hint as a new RelayEndpoint{url,
transports[]} list on RelayConfig, mirroring how peers and proxies
announce capabilities. Older management servers that only send urls keep
working unchanged.

devcert build: relay generates an ECDSA P-256 cert with 13-day validity
(within the WebTransport serverCertificateHashes 14-day cap) and exposes
its SHA-256 so the WASM dialer can pin it.

Bumps quic-go v0.55.0 -> v0.59.0 (no API breaks for relay's importers)
and adds github.com/quic-go/webtransport-go v0.10.0.
2026-05-17 11:08:30 +00:00

125 lines
3.5 KiB
Go

//go:build devcert
package tls
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"net"
"sync"
"time"
log "github.com/sirupsen/logrus"
)
// devCertHash holds the SHA-256 hash of the latest generated dev certificate.
// The WASM WebTransport client reads it via DevCertHash() to pin the self-
// signed cert through serverCertificateHashes — browsers require an ECDSA cert
// with validity <= 14 days when this pinning mode is used.
var (
devCertHashMu sync.RWMutex
devCertHash []byte
)
// DevCertHash returns the SHA-256 hash of the dev TLS certificate, or nil if
// no dev cert has been generated yet. WASM clients can pass this through
// serverCertificateHashes on WebTransport handshake.
func DevCertHash() []byte {
devCertHashMu.RLock()
defer devCertHashMu.RUnlock()
if devCertHash == nil {
return nil
}
out := make([]byte, len(devCertHash))
copy(out, devCertHash)
return out
}
func setDevCertHash(certDER []byte) {
sum := sha256.Sum256(certDER)
devCertHashMu.Lock()
devCertHash = sum[:]
devCertHashMu.Unlock()
}
func ServerQUICTLSConfig(originTLSCfg *tls.Config) (*tls.Config, error) {
if originTLSCfg == nil {
log.Warnf("QUIC server will use self signed certificate for testing!")
return generateTestTLSConfig([]string{NBalpn})
}
cfg := originTLSCfg.Clone()
cfg.NextProtos = []string{NBalpn}
return cfg, nil
}
// ServerMuxTLSConfig returns a TLS config offering both ALPNs so a single UDP
// socket can serve raw QUIC and WebTransport clients.
func ServerMuxTLSConfig(originTLSCfg *tls.Config) (*tls.Config, error) {
if originTLSCfg == nil {
log.Warnf("QUIC/WT server will use self signed certificate for testing!")
return generateTestTLSConfig([]string{NBalpn, H3alpn})
}
cfg := originTLSCfg.Clone()
cfg.NextProtos = []string{NBalpn, H3alpn}
return cfg, nil
}
// generateTestTLSConfig creates a self-signed ECDSA P-256 certificate suitable
// for both raw QUIC and browser WebTransport. Validity is capped at 13 days so
// the cert remains usable with WebTransport serverCertificateHashes pinning
// (browser limit is 14 days).
func generateTestTLSConfig(alpns []string) (*tls.Config, error) {
log.Infof("generating test TLS config (ECDSA P-256, 13 day validity) for ALPNs %v", alpns)
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"Test Organization"},
},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour * 24 * 13),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
DNSNames: []string{"localhost"},
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
}
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
if err != nil {
return nil, err
}
setDevCertHash(certDER)
keyDER, err := x509.MarshalECPrivateKey(privateKey)
if err != nil {
return nil, err
}
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
if err != nil {
return nil, err
}
return &tls.Config{
Certificates: []tls.Certificate{tlsCert},
NextProtos: alpns,
}, nil
}