Files
netbird/shared/relay/client/picker.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

129 lines
3.5 KiB
Go

package client
import (
"context"
"errors"
"fmt"
"sync/atomic"
"time"
log "github.com/sirupsen/logrus"
auth "github.com/netbirdio/netbird/shared/relay/auth/hmac"
)
const (
maxConcurrentServers = 7
defaultConnectionTimeout = 30 * time.Second
)
type connResult struct {
RelayClient *Client
Url string
Err error
}
type ServerPicker struct {
TokenStore *auth.TokenStore
// ServerURLs holds the legacy flat list of relay URLs, kept for callers
// that don't yet thread through transport hints. Endpoints, when set, is
// the source of truth — ServerURLs is derived from it.
ServerURLs atomic.Value
Endpoints atomic.Value // []ServerEndpoint
PeerID string
MTU uint16
ConnectionTimeout time.Duration
}
// loadEndpoints returns the per-endpoint list, falling back to a URL-only
// projection of ServerURLs if no endpoints have been set yet (older callers
// that still call UpdateServerURLs without the hint-aware path).
func (sp *ServerPicker) loadEndpoints() []ServerEndpoint {
if v := sp.Endpoints.Load(); v != nil {
if eps, ok := v.([]ServerEndpoint); ok && len(eps) > 0 {
return eps
}
}
if v := sp.ServerURLs.Load(); v != nil {
if urls, ok := v.([]string); ok {
return EndpointsFromURLs(urls)
}
}
return nil
}
func (sp *ServerPicker) PickServer(parentCtx context.Context) (*Client, error) {
ctx, cancel := context.WithTimeout(parentCtx, sp.ConnectionTimeout)
defer cancel()
endpoints := sp.loadEndpoints()
totalServers := len(endpoints)
connResultChan := make(chan connResult, totalServers)
successChan := make(chan connResult, 1)
concurrentLimiter := make(chan struct{}, maxConcurrentServers)
log.Debugf("pick server from list: %d endpoint(s)", totalServers)
for _, ep := range endpoints {
// todo check if we have a successful connection so we do not need to connect to other servers
concurrentLimiter <- struct{}{}
go func(ep ServerEndpoint) {
defer func() {
<-concurrentLimiter
}()
sp.startConnection(parentCtx, connResultChan, ep)
}(ep)
}
go sp.processConnResults(connResultChan, successChan)
select {
case cr, ok := <-successChan:
if !ok {
return nil, errors.New("failed to connect to any relay server: all attempts failed")
}
log.Infof("chosen home Relay server: %s", cr.Url)
return cr.RelayClient, nil
case <-ctx.Done():
return nil, fmt.Errorf("failed to connect to any relay server: %w", ctx.Err())
}
}
func (sp *ServerPicker) startConnection(ctx context.Context, resultChan chan connResult, ep ServerEndpoint) {
log.Infof("try to connecting to relay server: %s (transports=%v)", ep.URL, ep.Transports)
relayClient := NewClient(ep.URL, sp.TokenStore, sp.PeerID, sp.MTU)
if len(ep.Transports) > 0 {
relayClient.SetTransportHint(ep.Transports)
}
err := relayClient.Connect(ctx)
resultChan <- connResult{
RelayClient: relayClient,
Url: ep.URL,
Err: err,
}
}
func (sp *ServerPicker) processConnResults(resultChan chan connResult, successChan chan connResult) {
var hasSuccess bool
for numOfResults := 0; numOfResults < cap(resultChan); numOfResults++ {
cr := <-resultChan
if cr.Err != nil {
log.Tracef("failed to connect to Relay server: %s: %v", cr.Url, cr.Err)
continue
}
log.Infof("connected to Relay server: %s", cr.Url)
if hasSuccess {
log.Infof("closing unnecessary Relay connection to: %s", cr.Url)
if err := cr.RelayClient.Close(); err != nil {
log.Errorf("failed to close connection to %s: %v", cr.Url, err)
}
continue
}
hasSuccess = true
successChan <- cr
}
close(successChan)
}