[management] Scope the change to the private-capability check

The PR grew past its purpose. What it needs to do is refuse to bootstrap an
agent network endpoint onto a cluster that cannot serve it, which is the
private capability check on the picked cluster. Everything that accreted
around it — canonicalising proxy addresses at connect, refusing another
account's cluster or a host another account pinned, withdrawing a claim
lost to a concurrent one, folding casing on migrated settings rows — is
security work in its own right and moves to follow-up PRs, where each can
be reviewed against its own threat rather than as a rider on this one.

This restores main's version of every file outside that purpose and reduces
the validation to: a cluster the account can see must have a live embedded
proxy, and a cluster management holds no row for stays pinnable
(address-first). The e2e test and the fixture seeds are unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sa3DsBDP3VciAi4PPG17L6
This commit is contained in:
mlsmaycon
2026-09-12 16:08:07 +00:00
co-authored by Claude Fable 5.1
parent 4ed71f8987
commit 5502ea08ac
17 changed files with 41 additions and 1323 deletions
+9 -45
View File
@@ -13,7 +13,6 @@ import (
"math"
"net"
"net/http"
"net/netip"
"net/url"
"os"
"strconv"
@@ -496,8 +495,7 @@ func (s *ProxyServiceServer) validateProxyConnect(proxyID, address string, ctx c
if proxyID == "" {
return proxyConnectParams{}, status.Errorf(codes.InvalidArgument, "proxy_id is required")
}
address, ok := canonicalProxyAddress(address)
if !ok {
if !isProxyAddressValid(address) {
return proxyConnectParams{}, status.Errorf(codes.InvalidArgument, "proxy address is invalid")
}
@@ -571,12 +569,6 @@ func (s *ProxyServiceServer) registerProxyConnection(ctx context.Context, params
proxyRecord, err := s.proxyManager.Connect(ctx, params.proxyID, sessionID, params.address, peerInfo, accountID, caps)
if err != nil {
cancel()
if errors.Is(err, proxy.ErrClusterAddressUnavailable) {
// The claim was lost to a concurrent one after validateProxyConnect
// saw the address free; the row has been withdrawn. Same answer
// as the pre-write check gives, so the proxy treats both alike.
return nil, nil, status.Errorf(codes.AlreadyExists, "cluster address %s is already in use", params.address)
}
if accountID != nil {
return nil, nil, status.Errorf(codes.Internal, "failed to register BYOP proxy: %v", err)
}
@@ -880,44 +872,16 @@ func (s *ProxyServiceServer) snapshotServiceMappings(ctx context.Context, conn *
return mappings, nil
}
// canonicalProxyAddress validates a proxy address (domain name or IP address)
// and returns the form the store keeps.
//
// cluster_address is the key every capability, ownership and routing lookup
// matches on, and this is the only path that writes it, so the address is
// canonicalised once here rather than pushing case-insensitivity into each of
// those queries: hostnames are case-insensitive and may be unicode, so they
// fold to lowercase punycode (what domain.ValidateDomains already computes and
// this used to throw away), and an IP literal goes through netip so a mixed
// case IPv6 address does not become a second key for the same host.
func canonicalProxyAddress(addr string) (string, bool) {
if addr == "" {
return "", false
}
// A zoned literal like "fe80::1%eth0" is scoped to one host's interface,
// so it cannot identify a cluster others reach; net.ParseIP rejected it
// before and netip must not start accepting it.
if ip, err := netip.ParseAddr(addr); err == nil {
if ip.Zone() != "" {
return "", false
}
return ip.String(), true
}
// Folded before punycode conversion, not just after: idna maps the ASCII
// output to lowercase but does not case-fold the unicode input, so
// "PRÖXY.example.com" and "pröxy.example.com" would otherwise encode to
// two different labels for one host.
canonical, err := domain.ValidateDomains([]string{strings.ToLower(addr)})
if err != nil || len(canonical) != 1 {
return "", false
}
return string(canonical[0]), true
}
// isProxyAddressValid validates a proxy address (domain name or IP address)
func isProxyAddressValid(addr string) bool {
_, ok := canonicalProxyAddress(addr)
return ok
if addr == "" {
return false
}
if net.ParseIP(addr) != nil {
return true
}
_, err := domain.ValidateDomains([]string{addr})
return err == nil
}
// isStreamClosed returns true for errors that indicate normal stream
@@ -27,41 +27,3 @@ func TestIsProxyAddressValid(t *testing.T) {
})
}
}
// TestCanonicalProxyAddress pins the canonical form the store keeps. Every
// capability, ownership and routing lookup matches cluster_address exactly, so
// one host must have exactly one spelling in that column — which is what lets
// those queries stay exact (and keep using the index) instead of folding case
// per query.
func TestCanonicalProxyAddress(t *testing.T) {
tests := []struct {
name string
addr string
canonical string
ok bool
}{
{name: "lowercase domain unchanged", addr: "eu.proxy.netbird.io", canonical: "eu.proxy.netbird.io", ok: true},
{name: "mixed case domain folded", addr: "EU.Proxy.NetBird.io", canonical: "eu.proxy.netbird.io", ok: true},
{name: "uppercase domain folded", addr: "BYOP.PROXY.EXAMPLE.COM", canonical: "byop.proxy.example.com", ok: true},
{name: "unicode domain punycoded", addr: "pröxy.example.com", canonical: "xn--prxy-6qa.example.com", ok: true},
// Same host, and idna alone would encode the two cases to different
// labels, so this is the one that proves the fold happens first.
{name: "mixed case unicode folds to the same label", addr: "PRÖXY.example.com", canonical: "xn--prxy-6qa.example.com", ok: true},
{name: "ipv4 unchanged", addr: "203.0.113.10", canonical: "203.0.113.10", ok: true},
{name: "mixed case ipv6 canonicalised", addr: "2001:DB8::1", canonical: "2001:db8::1", ok: true},
{name: "empty string rejected", addr: "", ok: false},
{name: "space rejected", addr: "eu proxy.example.com", ok: false},
// Scoped to one host's interface, so it cannot name a cluster others
// reach; net.ParseIP rejected these and netip must not accept them.
{name: "zoned ipv6 rejected", addr: "fe80::1%eth0", ok: false},
{name: "unzoned link-local ipv6 accepted", addr: "fe80::1", canonical: "fe80::1", ok: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
canonical, ok := canonicalProxyAddress(tt.addr)
assert.Equal(t, tt.ok, ok)
assert.Equal(t, tt.canonical, canonical)
})
}
}
@@ -3,12 +3,11 @@ package grpc
import (
"context"
"errors"
"fmt"
"testing"
"go.uber.org/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
"google.golang.org/grpc/codes"
grpcstatus "google.golang.org/grpc/status"
@@ -167,27 +166,3 @@ func TestValidateProxyConnect_AuthorizerRunsLast(t *testing.T) {
assert.Equal(t, codes.AlreadyExists, st.Code(), "an address conflict must keep its own status")
assert.Zero(t, auth.called, "a conflicting address must be rejected before policy runs")
}
// TestRegisterProxyConnection_LostClaimIsAlreadyExists pins the status a proxy
// sees when its claim is lost after validateProxyConnect passed: the manager
// withdraws the row and reports ErrClusterAddressUnavailable, and the connect
// path must answer AlreadyExists — the same code the pre-write check gives —
// rather than the Internal it uses for a store failure, so the proxy handles
// the two paths to "address taken" identically.
func TestRegisterProxyConnection_LostClaimIsAlreadyExists(t *testing.T) {
ctrl := gomock.NewController(t)
mgr := proxy.NewMockManager(ctrl)
mgr.EXPECT().
Connect(gomock.Any(), "proxy-1", gomock.Any(), "cluster.example.com", gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil, fmt.Errorf("cluster address cluster.example.com: %w", proxy.ErrClusterAddressUnavailable))
s := &ProxyServiceServer{proxyManager: mgr}
_, _, err := s.registerProxyConnection(scopedCtx("acc-1"), proxyConnectParams{proxyID: "proxy-1", address: "cluster.example.com"}, &proxyConnection{})
require.Error(t, err)
st, ok := grpcstatus.FromError(err)
require.True(t, ok)
assert.Equal(t, codes.AlreadyExists, st.Code(), "a claim lost after the check must read as the address being taken")
assert.Contains(t, st.Message(), "already in use")
_, tracked := s.connectedProxies.Load("proxy-1")
assert.False(t, tracked, "a refused registration must not be tracked as connected")
}