Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions pkg/address-resolver/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,7 @@ func New(log *logging.Logger, s store.Store, nonceStore httpauth.NonceStore,
r.Post("/bind/stcpr", api.bind)
r.Delete("/bind/stcpr", api.delBind)
r.Post("/bind/quic", api.bindQUIC)
r.Post("/bind/wt", api.bindWT)
r.Get("/resolve/{type}/{pk}", api.resolve)
})

Expand Down Expand Up @@ -346,6 +347,14 @@ func (a *API) bind(w http.ResponseWriter, r *http.Request) {
// bindQUIC handles POST /bind/quic — registers the visor's QUIC UDP address
// (#2607 QUIC follow-on). Same validation as STCPR; stored under the QUIC type
// for peers to Resolve.
// bindWT handles POST /bind/wt — registers the visor's WebTransport UDP address
// AND its self-signed cert hash (carried in the bind payload's CertHash, stored
// via VisorData's embedded LocalAddresses so /resolve/wt returns it). Same
// validation as STCPR/QUIC; stored under the WT type.
func (a *API) bindWT(w http.ResponseWriter, r *http.Request) {
a.bindForType(w, r, types.WT)
}

func (a *API) bindQUIC(w http.ResponseWriter, r *http.Request) {
a.bindForType(w, r, types.QUIC)
}
Expand Down
18 changes: 18 additions & 0 deletions pkg/address-resolver/store/address_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,22 @@ func (s *AddressSuite) TestRegister() {
require.NoError(t, err)
require.Equal(t, visorData, got)
})

// WT carries a self-signed cert hash (no CA) that the dialing peer pins. It
// rides VisorData via the embedded LocalAddresses, so a bind→resolve must
// preserve it. Guards both store backends + the WT type in the switch.
wtData := addrresolver.VisorData{
RemoteAddr: "[::1]:5678",
LocalAddresses: addrresolver.LocalAddresses{CertHash: "deadbeefcafe"},
}
t.Run(".BindWT", func(t *testing.T) {
require.NoError(t, s.Bind(ctx, types.WT, pk, wtData))
})

t.Run(".ResolveWT", func(t *testing.T) {
got, err := s.Resolve(ctx, types.WT, pk)
require.NoError(t, err)
require.Equal(t, wtData, got)
require.Equal(t, "deadbeefcafe", got.CertHash)
})
}
8 changes: 4 additions & 4 deletions pkg/address-resolver/store/redis_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ func newRedisStore(ctx context.Context, addr, password string, poolSize int, ttl

func (s *redisStore) Bind(ctx context.Context, netType types.Type, pk cipher.PubKey, visorData addrresolver.VisorData) error {
switch netType {
case types.STCPR, types.SUDPH, types.QUIC:
case types.STCPR, types.SUDPH, types.QUIC, types.WT:
return s.bindWithIndex(ctx, netType, pk, visorData)
default:
return ErrUnknownTransportType
Expand All @@ -75,7 +75,7 @@ func (s *redisStore) Bind(ctx context.Context, netType types.Type, pk cipher.Pub

func (s *redisStore) DelBind(ctx context.Context, netType types.Type, pk cipher.PubKey) error {
switch netType {
case types.STCPR, types.SUDPH, types.QUIC:
case types.STCPR, types.SUDPH, types.QUIC, types.WT:
return s.delBindWithIndex(ctx, netType, pk)
default:
return ErrUnknownTransportType
Expand All @@ -84,7 +84,7 @@ func (s *redisStore) DelBind(ctx context.Context, netType types.Type, pk cipher.

func (s *redisStore) Resolve(ctx context.Context, netType types.Type, pk cipher.PubKey) (addrresolver.VisorData, error) {
switch netType {
case types.STCPR, types.SUDPH, types.QUIC:
case types.STCPR, types.SUDPH, types.QUIC, types.WT:
key := getKey(string(netType), pk)
return s.resolve(ctx, key)
default:
Expand All @@ -94,7 +94,7 @@ func (s *redisStore) Resolve(ctx context.Context, netType types.Type, pk cipher.

func (s *redisStore) GetAll(ctx context.Context, netType types.Type) ([]string, error) {
switch netType {
case types.STCPR, types.SUDPH, types.QUIC:
case types.STCPR, types.SUDPH, types.QUIC, types.WT:
if pks, ok := s.getAllCache.Get(netType); ok {
return pks, nil
}
Expand Down
73 changes: 73 additions & 0 deletions pkg/transport/network/addrresolver/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const (
sudphPriority = 1
stcprBindPath = "/bind/stcpr"
quicBindPath = "/bind/quic"
wtBindPath = "/bind/wt"
addrChSize = 1024
udpKeepHeartbeatInterval = 10 * time.Second
sudphReRegisterInterval = 90 * time.Second
Expand Down Expand Up @@ -83,6 +84,10 @@ type Error struct {
type APIClient interface {
BindSTCPR(ctx context.Context, port string) error
BindQUIC(ctx context.Context, port string) error
// BindWT registers this visor's WebTransport UDP port and the SHA-256 hex of
// its self-signed cert (the hash dialing peers pin). Mirrors BindQUIC + the
// cert hash.
BindWT(ctx context.Context, port, certHash string) error
BindSUDPH(filter *pfilter.PacketFilter, handshake Handshake) (<-chan RemoteVisor, error)
Resolve(ctx context.Context, netType string, pk cipher.PubKey) (VisorData, error)
Transports(ctx context.Context) (map[cipher.PubKey][]string, error)
Expand Down Expand Up @@ -403,6 +408,11 @@ type LocalAddresses struct {
// source isn't v6. Empty when the visor is v4-only — preserves the
// pre-Phase-2c single-stack contract.
PublicIPv6 string `json:"public_ip_v6,omitempty"`
// CertHash is the lowercase SHA-256 hex of the visor's self-signed
// WebTransport certificate. WT has no CA; a dialing peer pins this hash. Only
// the WT bind sets it (it rides VisorData via the embedded LocalAddresses, so
// /resolve/wt returns it); empty for every other transport type.
CertHash string `json:"cert_hash,omitempty"`
}

func (c *httpClient) Addresses(_ context.Context) string {
Expand Down Expand Up @@ -532,6 +542,69 @@ func (c *httpClient) BindQUIC(ctx context.Context, port string) error {
return nil
}

// BindWT registers this visor's WebTransport UDP port + self-signed cert hash
// with the address resolver. Mirrors BindQUIC, plus the cert hash (CertHash):
// WebTransport has no CA, so the dialing peer pins this SHA-256 to trust the
// server cert. The hash rides VisorData via the embedded LocalAddresses, so
// /resolve/wt returns it alongside the endpoint.
func (c *httpClient) BindWT(ctx context.Context, port, certHash string) error {
log := c.log.WithField("func", "httpClient.BindWT")
if !c.isReady() {
log.Debug("Address resolver is not ready yet, waiting...")
<-c.ready
log.Debug("Address resolver became ready, binding")
}

c.awaitPublicIP(stcprBindPublicIPWait)

addresses, err := netutil.LocalAddresses()
if err != nil {
return err
}
clientPublicIP := c.localPublicIPRaw()
if clientPublicIP != "" {
publicIP := clientPublicIP
if host, _, err := net.SplitHostPort(publicIP); err == nil {
publicIP = host
}
found := false
for _, addr := range addresses {
if addr == publicIP {
found = true
break
}
}
if !found {
addresses = append(addresses, publicIP)
}
}

localAddresses := LocalAddresses{
Addresses: addresses,
Port: port,
PublicIP: c.LocalPublicIP(),
PublicIPv6: c.localPublicIPv6Raw(),
CertHash: certHash,
}
log.Debugf("Address resolver binding WT with: %v port %s cert %s", addresses, port, certHash)
resp, err := c.Post(ctx, wtBindPath, localAddresses)
if err != nil {
return err
}
defer func() {
if err := resp.Body.Close(); err != nil {
log.WithError(err).Warn("Failed to close response body")
}
}()
if resp.StatusCode == http.StatusTooManyRequests {
return fmt.Errorf("rate limited by address resolver (status 429)")
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("status: %d, error: %w", resp.StatusCode, httpauthclient.ExtractError(resp.Body))
}
return nil
}

func (c *httpClient) BindSTCPR(ctx context.Context, port string) error {
log := c.log.WithField("func", "httpClient.BindSTCPR")
if !c.isReady() {
Expand Down
18 changes: 18 additions & 0 deletions pkg/transport/network/addrresolver/mock_api_client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading