Skip to content

Commit fc3034d

Browse files
h4x3rotabclaude
andcommitted
feat(consul-postgres-ha): mesh-conn reads allowlist from env
Drop the hardcoded peerVIPAllowlist const. MESH_CONN_ALLOWLIST env (JSON [{port, udp}, ...]) is now the source of truth; the platform sidecar entrypoint generates it from SERVICES_JSON at startup. This makes mesh-conn invisible to developers — adding a service is purely a cluster.tf edit, no mesh-conn rebuild. validateConfig extends to fail-fast on malformed allowlists (empty, duplicate ports, out-of-range ports). Tests cover the new cases plus the existing peer-validation set. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent ea9d37c commit fc3034d

2 files changed

Lines changed: 189 additions & 99 deletions

File tree

consul-postgres-ha/mesh-conn/main.go

Lines changed: 99 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// mesh-conn — userspace port-forwarding agent over pion/ice + QUIC.
22
//
3-
// Addressing model: peer VIPs + static infra-port allowlist.
3+
// Addressing model: peer VIPs + platform-supplied port allowlist.
44
//
55
// Every peer is identified by a /8 loopback IP `127.50.0.<vip>`. The
66
// `vip` is a small integer (typically the peer's ordinal in the
@@ -10,30 +10,37 @@
1010
// - the entrypoint provisions `127.50.0.<vip>/32 dev lo` for ALL
1111
// peers, including self — so dialing self's VIP loops back through
1212
// the kernel without crossing mesh-conn.
13-
// - mesh-conn binds the static infra-port allowlist on every OTHER
14-
// peer's VIP — and forwards each accepted connection to the right
15-
// remote peer over QUIC.
13+
// - mesh-conn binds the allowlist on every OTHER peer's VIP — and
14+
// forwards each accepted connection to the right remote peer over
15+
// QUIC.
1616
//
17-
// Allowlist (every port in this list, every other peer):
17+
// Allowlist source: MESH_CONN_ALLOWLIST env, JSON shape `[{port, udp}, …]`.
18+
// Populated by the platform sidecar entrypoint at startup from
19+
// SERVICES_JSON (per-service Connect-sidecar ports) plus the two
20+
// static Consul-infra ports {8300, 8301}. The substance — which ports
21+
// cross peer boundaries — is platform state declared one level up in
22+
// `cluster.tf`'s `local.services`; mesh-conn just reads what the
23+
// platform tells it.
24+
//
25+
// Today's defaults (3-service example: webdemo + postgres-master/-replica):
1826
//
1927
// 21000 Envoy Connect public mTLS — webdemo sidecar (TCP)
2028
// 21001 Envoy Connect public mTLS — postgres sidecar (TCP)
2129
// 8300 Consul server RPC (TCP)
2230
// 8301 Consul serf-LAN gossip (UDP + TCP)
2331
//
32+
// Sidecar ports come in one-per-backend because stock Consul Connect
33+
// requires one sidecar Envoy process per Connect-mesh-accessible
34+
// service. Services sharing a canonical port (postgres-master and
35+
// postgres-replica both on :5432, same Patroni backend) collapse onto
36+
// one sidecar; distinct canonical ports get distinct sidecars and
37+
// distinct allowlist entries.
38+
//
2439
// The allowlist is intentionally minimal: only platform infrastructure
2540
// that needs unmediated peer-to-peer reachability. App-level traffic
2641
// (including Patroni's Postgres replication) goes through the Connect
2742
// mesh — mesh-conn knows peers, never services.
2843
//
29-
// Sidecar ports come in pairs because stock Consul Connect requires
30-
// one sidecar Envoy process per Connect-mesh-accessible service:
31-
// 21000 terminates inbound mTLS for `webdemo`, 21001 terminates inbound
32-
// mTLS for `postgres`. Both are reserved cluster-wide; any new
33-
// mesh-accessible service either reuses one of these (by SNI-routing
34-
// custom Envoy config — out of scope here) or is added explicitly to
35-
// this list.
36-
//
3744
// Wire format on each QUIC stream is unchanged from the old shape:
3845
// the first 3 bytes are (tag, port-big-endian-uint16), where port is
3946
// the receiver's local port to dial / write into. The receiver
@@ -100,43 +107,34 @@ func (p *Peer) vipAddr() net.IP {
100107
return net.IPv4(127, 50, 0, byte(p.Vip))
101108
}
102109

103-
// infraPort is one entry in the static peer-VIP port allowlist. mesh-conn
110+
// InfraPort is one entry in the peer-VIP port allowlist. mesh-conn
104111
// binds (peer.vipAddr(), Port) on every OTHER peer for each entry.
105-
type infraPort struct {
106-
Port int
107-
UDP bool // some ports carry UDP (e.g. serf gossip); most are TCP-only
112+
// JSON-tagged because the value comes in over MESH_CONN_ALLOWLIST env.
113+
type InfraPort struct {
114+
Port int `json:"port"`
115+
UDP bool `json:"udp"` // some ports carry UDP (e.g. serf gossip); most are TCP-only
108116
}
109117

110-
// peerVIPAllowlist is the entire cluster-wide set of ports any host may
111-
// dial at a peer VIP. Adding a port here is a deliberate, reviewable
112-
// change — see consul-postgres-ha/ARCHITECTURE.md for the layering
113-
// invariant ("mesh-conn knows peers, not services").
114-
var peerVIPAllowlist = []infraPort{
115-
{Port: 21000, UDP: false}, // Envoy Connect sidecar — webdemo
116-
{Port: 21001, UDP: false}, // Envoy Connect sidecar — postgres
117-
{Port: 8300, UDP: false}, // Consul server RPC (server-to-server + client-to-server)
118-
{Port: 8301, UDP: true}, // Consul serf-LAN gossip (UDP datagrams + TCP fallback)
118+
type Config struct {
119+
SelfID string
120+
Peers []Peer
121+
Allowlist []InfraPort
122+
SignalingURL string
123+
TurnHost string
124+
TurnSecret string
119125
}
120126

121127
// allowedPort reports whether the given port is in the allowlist.
122128
// Used by the receiver to reject streams targeting unknown ports.
123-
func allowedPort(port int) bool {
124-
for _, ip := range peerVIPAllowlist {
129+
func (c *Config) allowedPort(port int) bool {
130+
for _, ip := range c.Allowlist {
125131
if ip.Port == port {
126132
return true
127133
}
128134
}
129135
return false
130136
}
131137

132-
type Config struct {
133-
SelfID string
134-
Peers []Peer
135-
SignalingURL string
136-
TurnHost string
137-
TurnSecret string
138-
}
139-
140138
func loadConfig() *Config {
141139
// Primary sources of truth (with env fallback so this binary
142140
// stays runnable in a bare-metal smoke test outside the dstack
@@ -162,8 +160,16 @@ func loadConfig() *Config {
162160
if err := json.Unmarshal([]byte(mustEnv("PEERS_JSON")), &cfg.Peers); err != nil {
163161
log.Fatalf("PEERS_JSON: %v", err)
164162
}
165-
if err := validatePeers(cfg); err != nil {
166-
log.Fatalf("PEERS_JSON: %v", err)
163+
// MESH_CONN_ALLOWLIST: JSON list `[{port, udp}, …]` generated by
164+
// the platform sidecar entrypoint from SERVICES_JSON (per-service
165+
// Connect-sidecar ports) plus the static Consul-infra ports
166+
// (8300 RPC + 8301 gossip). This is platform plumbing; developers
167+
// edit `local.services` in cluster.tf, not this env.
168+
if err := json.Unmarshal([]byte(mustEnv("MESH_CONN_ALLOWLIST")), &cfg.Allowlist); err != nil {
169+
log.Fatalf("MESH_CONN_ALLOWLIST: %v", err)
170+
}
171+
if err := validateConfig(cfg); err != nil {
172+
log.Fatalf("config: %v", err)
167173
}
168174
return cfg
169175
}
@@ -215,11 +221,27 @@ func readTurnSecret() string {
215221
return ""
216222
}
217223

218-
// validatePeers fails fast on any silent mis-configuration that would
219-
// otherwise manifest as confusing runtime failures: collided VIPs,
220-
// missing self, etc. Bound at startup because a peer's PEERS_JSON is
221-
// shared with every other peer's configuration and must round-trip
222-
// identically across the cluster.
224+
// validateConfig fails fast on any silent mis-configuration that
225+
// would otherwise manifest as confusing runtime failures: collided
226+
// peer VIPs, missing self, malformed allowlist, etc. Bound at startup
227+
// because PEERS_JSON and MESH_CONN_ALLOWLIST are shared cluster-wide
228+
// state and must round-trip identically across peers.
229+
func validateConfig(cfg *Config) error {
230+
if err := validatePeers(cfg); err != nil {
231+
return err
232+
}
233+
if err := validateAllowlist(cfg); err != nil {
234+
return err
235+
}
236+
// Log a digest of the validated config so operators can check that
237+
// every peer in the cluster sees the same PEERS_JSON. Differences
238+
// across peers would indicate a deploy-script discrepancy.
239+
digest := peersDigest(cfg.Peers)
240+
log.Printf("config validated: %d peers, allowlist=%d ports/peer (%v), digest=%s",
241+
len(cfg.Peers), len(cfg.Allowlist), allowlistPorts(cfg.Allowlist), digest)
242+
return nil
243+
}
244+
223245
func validatePeers(cfg *Config) error {
224246
if len(cfg.Peers) < 2 {
225247
return fmt.Errorf("need at least 2 peers in PEERS_JSON, got %d", len(cfg.Peers))
@@ -257,13 +279,27 @@ func validatePeers(cfg *Config) error {
257279
if !selfFound {
258280
return fmt.Errorf("PEER_ID %q not in PEERS_JSON (peers: %v)", cfg.SelfID, knownIDs(cfg.Peers))
259281
}
282+
return nil
283+
}
260284

261-
// Log a digest of the validated config so operators can check that
262-
// every peer in the cluster sees the same PEERS_JSON. Differences
263-
// across peers would indicate a deploy-script discrepancy.
264-
digest := peersDigest(cfg.Peers)
265-
log.Printf("PEERS_JSON validated: %d peers, allowlist=%d ports/peer, digest=%s",
266-
len(cfg.Peers), len(peerVIPAllowlist), digest)
285+
// validateAllowlist fails fast on MESH_CONN_ALLOWLIST malformations:
286+
// empty list (mesh-conn with nothing to forward is always a config
287+
// bug), out-of-range ports, or duplicate port entries (would race for
288+
// the same listener bind at runtime).
289+
func validateAllowlist(cfg *Config) error {
290+
if len(cfg.Allowlist) == 0 {
291+
return fmt.Errorf("MESH_CONN_ALLOWLIST is empty — platform sidecar should always emit at least the Consul-infra ports")
292+
}
293+
seen := map[int]bool{}
294+
for i, ip := range cfg.Allowlist {
295+
if ip.Port < 1 || ip.Port > 65535 {
296+
return fmt.Errorf("allowlist[%d] port=%d out of range (1..65535)", i, ip.Port)
297+
}
298+
if seen[ip.Port] {
299+
return fmt.Errorf("allowlist port %d appears twice — each port may only be entered once", ip.Port)
300+
}
301+
seen[ip.Port] = true
302+
}
267303
return nil
268304
}
269305

@@ -276,9 +312,9 @@ func knownIDs(peers []Peer) []string {
276312
}
277313

278314
// allowlistPorts returns the bare port numbers for log output.
279-
func allowlistPorts() []int {
280-
out := make([]int, len(peerVIPAllowlist))
281-
for i, ip := range peerVIPAllowlist {
315+
func allowlistPorts(allowlist []InfraPort) []int {
316+
out := make([]int, len(allowlist))
317+
for i, ip := range allowlist {
282318
out[i] = ip.Port
283319
}
284320
return out
@@ -348,7 +384,7 @@ func main() {
348384
}
349385
}
350386
log.Printf("mesh-conn: self=%s vip=%d allowlist=%v other=%d",
351-
cfg.SelfID, self.Vip, allowlistPorts(), len(others))
387+
cfg.SelfID, self.Vip, allowlistPorts(cfg.Allowlist), len(others))
352388

353389
go pollLoop(cfg)
354390

@@ -501,8 +537,8 @@ func dialAndPump(cfg *Config, self, peer Peer, sess *peerSession) error {
501537
// here means the alias didn't get set up.
502538
peerVip := peer.vipAddr()
503539
udpSocks := map[int]*net.UDPConn{} // port -> listener (only UDP-bearing ports)
504-
tcpListeners := make([]*net.TCPListener, 0, len(peerVIPAllowlist))
505-
for _, ip := range peerVIPAllowlist {
540+
tcpListeners := make([]*net.TCPListener, 0, len(cfg.Allowlist))
541+
for _, ip := range cfg.Allowlist {
506542
tl, err := net.ListenTCP("tcp", &net.TCPAddr{IP: peerVip, Port: ip.Port})
507543
if err != nil {
508544
return fmt.Errorf("tcp listen %s:%d: %w", peerVip, ip.Port, err)
@@ -525,15 +561,15 @@ func dialAndPump(cfg *Config, self, peer Peer, sess *peerSession) error {
525561
// loop to handle ad-hoc incoming TCP streams.
526562
udpStreams := map[int]*quic.Stream{}
527563
allUDPReady := make(chan struct{})
528-
errCh := make(chan error, 4*len(peerVIPAllowlist))
564+
errCh := make(chan error, 4*len(cfg.Allowlist))
529565

530566
udpPortCount := len(udpSocks)
531567
go func() {
532-
errCh <- runAcceptLoop(connCtx, qconn, &self, udpStreams, udpPortCount, allUDPReady)
568+
errCh <- runAcceptLoop(connCtx, qconn, cfg, &self, udpStreams, udpPortCount, allUDPReady)
533569
}()
534570

535571
if isClient {
536-
for _, ip := range peerVIPAllowlist {
572+
for _, ip := range cfg.Allowlist {
537573
if !ip.UDP {
538574
continue
539575
}
@@ -559,12 +595,12 @@ func dialAndPump(cfg *Config, self, peer Peer, sess *peerSession) error {
559595
}
560596

561597
log.Printf("[%s] link up — peer vip=127.50.0.%d, allowlist=%v (udp=%d, tcp=%d)",
562-
peer.ID, peer.Vip, allowlistPorts(), udpPortCount, len(tcpListeners))
598+
peer.ID, peer.Vip, allowlistPorts(cfg.Allowlist), udpPortCount, len(tcpListeners))
563599

564600
// 5. Start pumps. UDP: one bidirectional pump pair per UDP port.
565601
// TCP: one accept-loop per port (each accepted conn opens an
566602
// ephemeral streamTCP).
567-
for _, ip := range peerVIPAllowlist {
603+
for _, ip := range cfg.Allowlist {
568604
port := ip.Port
569605
if ip.UDP {
570606
us := udpSocks[port]
@@ -583,7 +619,7 @@ func dialAndPump(cfg *Config, self, peer Peer, sess *peerSession) error {
583619
}()
584620
}
585621
}
586-
for i, ip := range peerVIPAllowlist {
622+
for i, ip := range cfg.Allowlist {
587623
tl := tcpListeners[i]
588624
port := ip.Port
589625
go func() { errCh <- acceptLocalTCP(connCtx, tl, qconn, port) }()
@@ -597,7 +633,7 @@ func dialAndPump(cfg *Config, self, peer Peer, sess *peerSession) error {
597633
// local Consul / Envoy bound on the peer's own loopback alias). The
598634
// receiver validates port against the static allowlist; anything
599635
// else is rejected — that's the only port-knowledge the receiver needs.
600-
func runAcceptLoop(ctx context.Context, qconn *quic.Conn, self *Peer, udpStreams map[int]*quic.Stream, expectedUDP int, allUDPReady chan struct{}) error {
636+
func runAcceptLoop(ctx context.Context, qconn *quic.Conn, cfg *Config, self *Peer, udpStreams map[int]*quic.Stream, expectedUDP int, allUDPReady chan struct{}) error {
601637
udpRegistered := 0
602638
for {
603639
s, err := qconn.AcceptStream(ctx)
@@ -612,7 +648,7 @@ func runAcceptLoop(ctx context.Context, qconn *quic.Conn, self *Peer, udpStreams
612648
}
613649
tag := hdr[0]
614650
port := int(hdr[1])<<8 | int(hdr[2])
615-
if !allowedPort(port) {
651+
if !cfg.allowedPort(port) {
616652
log.Printf("stream for non-allowlist port %d (tag=0x%x)", port, tag)
617653
s.CancelRead(0)
618654
s.Close()

0 commit comments

Comments
 (0)