mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-20 23:59:55 +00:00
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.
129 lines
3.5 KiB
Go
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)
|
|
}
|