- Add a WebTransport transport to any-sync, enabling node communication over HTTP/3 WebTransport
- Uses real domain SSL certificates for TLS; peer identity verified via application-level handshake (like WebRTC)
- Closely mirrors the existing QUIC transport pattern (
net/transport/quic/) - Includes WASM/browser dial-only support (like
webrtc_js.go) - Full PeerService integration with
webtransport://address scheme - CORS support on the HTTP/3 endpoint
- HTTP path:
/webtransport
- Files/components involved:
net/transport/transport.go— transport interfaces, addWebTransportconstantnet/transport/quic/— primary reference implementation (closest pattern)net/transport/webrtc/— reference for browser/WASM support and app-level handshake patternnet/peerservice/peerservice.go— transport registration and dial routinggo.mod/go.sum— addwebtransport-godependency
- Library:
github.com/quic-go/webtransport-go(uses samequic-go v0.59.0) - Key API:
webtransport.Server.Upgrade()returns*Session;webtransport.Dialer.Dial()returns*Session;Session.OpenStreamSync()/AcceptStream()for multiplexed streams
- Testing approach: Regular (code first, then tests)
- Complete each task fully before moving to the next
- Make small, focused changes
- CRITICAL: every task MUST include new/updated tests for code changes in that task
- CRITICAL: all tests must pass before starting next task — no exceptions
- CRITICAL: update this plan file when scope changes during implementation
- Run tests after each change
- Maintain backward compatibility
- Unit tests: required for every task
- Test infrastructure follows
quic_test.gopattern (fixture with app, transport, test accepter) - Tests use self-signed certs generated at test time (production uses real certs)
- Mark completed items with
[x]immediately when done - Add newly discovered tasks with ➕ prefix
- Document issues/blockers with
⚠️ prefix - Update plan if implementation deviates from original scope
- Add
github.com/quic-go/webtransport-gotogo.modand rungo mod tidy - Add
WebTransport = "webtransport"constant tonet/transport/transport.go - Verify
go build ./...passes - Run existing tests — must pass before next task
- Create
net/transport/webtransport/webtransport.go:CName = "net.transport.webtransport"WebTransportinterface (embedstransport.Transport+app.ComponentRunnable)New()constructor
- Create
net/transport/webtransport/config.go:configGetterinterface withGetWebTransport() ConfigConfigstruct:ListenAddrs []string,Path string,CertFile string,KeyFile string,WriteTimeoutSec int,CloseTimeoutSec int,DialTimeoutSec int,MaxStreams int64
- Verify
go build ./net/transport/webtransport/...passes - Run project tests — must pass before next task
- Create
net/transport/webtransport/conn.go:wtAddrimplementingnet.Addr(Network="webtransport", String=addr)wtNetConnstruct wrapping*webtransport.Streamasnet.Conn:Read,Write(with optional write deadline),Close(CancelRead + Close, like quicNetConn)LocalAddr,RemoteAddr,SetDeadline,SetReadDeadline,SetWriteDeadline
newConn()function creatingwtMultiConnwtMultiConnstruct implementingtransport.MultiConn:- Fields:
cctx context.Context,session *webtransport.Session,writeTimeout,closeTimeout,remoteAddr Context()→ return enriched cctx (withpeer.CtxWithPeerAddr)Accept()→session.AcceptStream(ctx)→ wrap aswtNetConnOpen(ctx)→session.OpenStreamSync(ctx)→ wrap aswtNetConnAddr()→"webtransport://" + remoteAddrIsClosed()→ check session context doneCloseChan()→session.Context().Done()Close()→session.CloseWithError(0, "")
- Fields:
- Write tests for
wtNetConn(read/write/close) - Write tests for
wtMultiConn(open/accept/close/addr) - Run project tests — must pass before next task
- Create
net/transport/webtransport/webtransport_native.go(build tag//go:build !js):wtTransportstruct:- Fields:
secure secureservice.SecureService,localPeerId string,accepter transport.Accepter,conf Config,server *webtransport.Server,udpConns []net.PacketConn,listCtx/listCtxCancel,mu sync.Mutex
- Fields:
Init(app):- Get
secureservice.SecureServicevia MustComponent - Get
accountservice.Servicefor localPeerId - Get config from optional configGetter
- Set defaults: Path="/webtransport", CloseTimeoutSec=5, DialTimeoutSec=30, MaxStreams=128
- Get
Name()→ CNameSetAccepter(accepter)Run(ctx):- Create
tls.ConfigwithGetCertificatecallback that re-readsCertFile/KeyFilefrom disk on each new connection (hot-reload for certbot rotation, no restart needed) - Create
http3.Serverwith TLS config and QUIC config (EnableDatagrams, MaxIncomingStreams) - Call
webtransport.ConfigureHTTP3Server - Create
webtransport.Server{H3: &h3Server, CheckOrigin: allow all} - Register HTTP handler at
conf.Paththat callshandleUpgrade - For each listen addr:
net.ListenUDP→server.Serve(udpConn)in goroutine
- Create
handleUpgrade(w, r):- Add CORS headers (Access-Control-Allow-Origin: *, etc.)
- Handle OPTIONS preflight
- Call
server.Upgrade(w, r)→ get session - Extract remotePeerId from query param
?peerId= - Launch goroutine:
accept(session, remoteAddr, remotePeerId)
accept(session, remoteAddr, remotePeerId):- Timeout context from DialTimeoutSec
session.AcceptStream(ctx)— handshake stream- Wrap as net.Conn
secure.HandshakeInbound(ctx, stream, remotePeerId)— peerId from URL query- Close handshake stream
- Create
wtMultiConn, callaccepter.Accept(mc)
Dial(ctx, addr):- Get
expectedPeerIdfrom context - Create
webtransport.Dialer{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, QUICConfig: quicConf} dialer.Dial(ctx, "https://"+addr+conf.Path+"?peerId="+localPeerId, nil)session.OpenStreamSync(ctx)— handshake stream- Wrap as net.Conn,
secure.HandshakeOutbound(ctx, stream, expectedPeerId) - Close handshake stream
- Return
newConn(cctx, session, addr, ...)
- Get
Close(ctx):- Cancel listCtx
server.Close()- Close UDP connections
- Write tests for Dial + Accept flow (fixture pattern from quic_test.go, self-signed test certs)
- Write test for cert hot-reload (replace cert files, verify new connections use new cert)
- Write tests for CORS headers
- Write tests for close/shutdown behavior
- Run project tests — must pass before next task
- Create
net/transport/webtransport/webtransport_js.go(build tag//go:build js):- Dial-only transport (no Run/Close/listener)
Init(app)— get secureservice, localPeerId, configDial(ctx, addr):- Use browser's WebTransport API via
syscall/js - Create WebTransport connection to
https://addr/webtransport - Wait for
readypromise - Open bidirectional stream for handshake
secure.HandshakeOutbound(ctx, stream, expectedPeerId)- Return MultiConn wrapping the WebTransport session
- Use browser's WebTransport API via
SetAccepter— no-op (dial-only)Run/Close— no-op
- Verify WASM build:
GOOS=js GOARCH=wasm go build ./net/transport/webtransport/... - Run project tests (native) — must pass before next task
- Modify
net/peerservice/peerservice.go:- Add import for
"github.com/anyproto/any-sync/net/transport/webtransport" - Add field
webtransport transport.TransporttopeerServicestruct - In
Init(): register webtransport (optional, like webrtc):if comp := a.Component(webtransport.CName); comp != nil { p.webtransport = comp.(transport.Transport) p.webtransport.SetAccepter(p) }
- In
preferredSchemes(): addtransport.WebTransportafter webrtc - In
dialScheme(): add casetransport.WebTransport: tr = p.webtransport
- Add import for
- Write test verifying webtransport is discovered and routed in dialScheme
- Run project tests — must pass before next task
- Verify Go-to-Go dial/accept works end-to-end (test with self-signed certs)
- Verify stream multiplexing (open multiple streams, exchange data)
- Verify handshake identity verification (correct peerId, mismatched peerId rejected)
- Verify WASM builds (
GOOS=js GOARCH=wasm go build ./net/transport/webtransport/...) - Verify CORS headers present on server responses
- Run full test suite:
GOEXPERIMENT=synctest go test ./... --cover - Run linter — all issues must be fixed
- Verify no regressions in existing QUIC/WebRTC/Yamux transports
- Update README if needed
- Add config example for WebTransport in documentation or comments
- Peer addresses:
webtransport://host:port - Internal dial URL:
https://host:port/webtransport - Path is configurable (default:
/webtransport)
Client Server
| |
|-- QUIC+TLS (real SSL cert) ------------>|
|-- HTTP/3 CONNECT /webtransport -------->|
|<-------- 200 OK (Upgrade) -------------|
| |
|== WebTransport Session Established =====|
| |
|-- OpenStream (handshake) -------------->|
|-- HandshakeOutbound(expectedPeerId) --->|
|<--- HandshakeInbound() ----------------|
|-- Close handshake stream -------------->|
| |
|== MultiConn Ready (Open/Accept) ========|
webtransport:
listenAddrs: ["0.0.0.0:443"]
path: "/webtransport"
certFile: "/path/to/cert.pem"
keyFile: "/path/to/key.pem"
writeTimeoutSec: 10
closeTimeoutSec: 5
dialTimeoutSec: 30
maxStreams: 128webtransport.Stream → wtNetConn (net.Conn):
Close()callsCancelRead(0)+Close()(same pattern as QUIC's quicNetConn)- Write deadline support via
SetWriteDeadline
- Build tag:
//go:build jsfor WASM,//go:build !jsfor native - WASM is dial-only (no server/listener)
- Uses browser's
WebTransportAPI viasyscall/js - Identity verified through application handshake (expectedPeerId from context)
Manual verification:
- Test with real domain SSL certificate against a running node
- Test browser client connectivity (from web app)
- Performance comparison with QUIC transport
External system updates:
any-sync-node— add WebTransport config section and register transport componentany-sync-filenode— same if file transfer over WebTransport is needed- Deployment configs — open UDP port for HTTP/3, provision SSL certificates