Skip to content

Commit bd8bc6e

Browse files
committed
Treat missing remote ICE auth as peer liveness
1 parent 1dc71f0 commit bd8bc6e

2 files changed

Lines changed: 55 additions & 30 deletions

File tree

consul-postgres-ha/ROBUSTNESS.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,16 @@ whose direct path goes down can't fail over to TURN until it's back".
119119
`:1006-1016`; the original bug described in earlier revisions of
120120
this doc is closed.
121121

122+
- **Staggered peer startup is a liveness wait, not a failed ICE
123+
attempt.** A real Phala deployment can start one CVM more than 60 s
124+
before its peer. Before both sides have exchanged ICE auth, there is
125+
no connectivity check in progress and retrying the local agent only
126+
republishes equivalent local credentials. `dialICE` now requires the
127+
local auth publish to succeed, then waits for the first remote auth
128+
until the process context is cancelled, logging the wait every 60 s.
129+
Once remote auth is available, the existing Dial/Accept failure path
130+
and `FailedTimeout` still bound real ICE failures.
131+
122132
- **ICE auth-race convergence.** When two peers handshake in near-
123133
simultaneous restart (the normal startup case on a fresh cluster
124134
or after a redeploy), the supersession check in `pollLoop`

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

Lines changed: 45 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -196,13 +196,13 @@ func readSelfID() string {
196196

197197
// readTurnSecret resolves the TURN shared secret in priority order:
198198
//
199-
// 1. TURN_SHARED_SECRET env (set when using an external coturn whose
200-
// static-auth-secret was configured out-of-band — e.g. the Vultr
201-
// coordinator path). When this is present it MUST win, because
202-
// the local TEE-derived value won't match what coturn is checking
203-
// against.
204-
// 2. /run/secrets/turn (TEE-derived path; matches the embedded
205-
// coordinator's coturn which reads the same file).
199+
// 1. TURN_SHARED_SECRET env (set when using an external coturn whose
200+
// static-auth-secret was configured out-of-band — e.g. the Vultr
201+
// coordinator path). When this is present it MUST win, because
202+
// the local TEE-derived value won't match what coturn is checking
203+
// against.
204+
// 2. /run/secrets/turn (TEE-derived path; matches the embedded
205+
// coordinator's coturn which reads the same file).
206206
//
207207
// Order matters: env beats file so that "use external coturn" can be
208208
// configured purely at the cluster.tf layer.
@@ -515,10 +515,11 @@ func sleepCtx(ctx context.Context, d time.Duration) bool {
515515
}
516516

517517
// Stream header layout: 3 bytes per stream open.
518-
// byte 0 = tag (streamUDP or streamTCP)
519-
// bytes 1-2 = receiver-side port (big-endian uint16) — the port number
520-
// the receiver itself binds locally; receiver looks it up in its own
521-
// Ports list to find the index/protocol slot
518+
//
519+
// byte 0 = tag (streamUDP or streamTCP)
520+
// bytes 1-2 = receiver-side port (big-endian uint16) — the port number
521+
// the receiver itself binds locally; receiver looks it up in its own
522+
// Ports list to find the index/protocol slot
522523
const (
523524
streamUDP byte = 0x55 // long-lived per-port UDP datagram pipe
524525
streamTCP byte = 0x33 // per-conn TCP byte-stream forwarder
@@ -633,7 +634,7 @@ func (m *Mesh) dialAndPump(parentCtx context.Context, peer Peer, sess *peerSessi
633634
// mesh-sidecar/entrypoint.sh before mesh-conn starts; failing
634635
// here means the alias didn't get set up.
635636
peerVip := peer.vipAddr()
636-
udpSocks := map[int]*net.UDPConn{} // port -> listener (only UDP-bearing ports)
637+
udpSocks := map[int]*net.UDPConn{} // port -> listener (only UDP-bearing ports)
637638
tcpListeners := make([]*net.TCPListener, 0, len(cfg.Allowlist))
638639
for _, ip := range cfg.Allowlist {
639640
tl, err := net.ListenTCP("tcp", &net.TCPAddr{IP: peerVip, Port: ip.Port})
@@ -1212,7 +1213,9 @@ func (m *Mesh) dialICE(remoteID string, sess *peerSession, attemptCtx context.Co
12121213
if c == nil {
12131214
return
12141215
}
1215-
publish(cfg, remoteID, "candidate", c.Marshal())
1216+
if err := publish(cfg, remoteID, "candidate", c.Marshal()); err != nil {
1217+
log.Printf("[%s] publish candidate: %v", remoteID, err)
1218+
}
12161219
}); err != nil {
12171220
return nil, err
12181221
}
@@ -1233,7 +1236,9 @@ func (m *Mesh) dialICE(remoteID string, sess *peerSession, attemptCtx context.Co
12331236
if err != nil {
12341237
return nil, err
12351238
}
1236-
publish(cfg, remoteID, "auth", localUfrag+":"+localPwd)
1239+
if err = publish(cfg, remoteID, "auth", localUfrag+":"+localPwd); err != nil {
1240+
return nil, fmt.Errorf("publish auth to %s: %w", remoteID, err)
1241+
}
12371242

12381243
if err = agent.GatherCandidates(); err != nil {
12391244
return nil, err
@@ -1255,18 +1260,25 @@ func (m *Mesh) dialICE(remoteID string, sess *peerSession, attemptCtx context.Co
12551260

12561261
// Wait for remote auth. pollLoop drain-then-pushes the freshest
12571262
// peer auth into sess.authCh independent of state, so by the time
1258-
// we wake we always have the latest value. attemptCtx covers the
1259-
// case where pollLoop already aborted us mid-wait (peer rolled
1260-
// creds again between agent install and now).
1263+
// we wake we always have the latest value. Absence of remote auth
1264+
// means the peer has not started or has not reached signalling yet;
1265+
// there is no stale ICE attempt to tear down until both sides have
1266+
// exchanged credentials. attemptCtx still covers process shutdown
1267+
// and the rare case where pollLoop explicitly aborts this attempt.
12611268
var remote [2]string
1262-
select {
1263-
case remote = <-sess.authCh:
1264-
case <-time.After(60 * time.Second):
1265-
err = fmt.Errorf("timeout waiting for remote auth from %s", remoteID)
1266-
return nil, err
1267-
case <-attemptCtx.Done():
1268-
err = fmt.Errorf("attempt aborted before auth from %s", remoteID)
1269-
return nil, err
1269+
waitLog := time.NewTicker(60 * time.Second)
1270+
defer waitLog.Stop()
1271+
waitRemoteAuth:
1272+
for {
1273+
select {
1274+
case remote = <-sess.authCh:
1275+
break waitRemoteAuth
1276+
case <-attemptCtx.Done():
1277+
err = fmt.Errorf("attempt aborted before auth from %s", remoteID)
1278+
return nil, err
1279+
case <-waitLog.C:
1280+
log.Printf("[%s] waiting for remote auth", remoteID)
1281+
}
12701282
}
12711283

12721284
// Record what we're about to dial against, so pollLoop can detect
@@ -1334,16 +1346,19 @@ type Message struct {
13341346
Data string `json:"data"`
13351347
}
13361348

1337-
func publish(cfg *Config, to, typ, data string) {
1349+
func publish(cfg *Config, to, typ, data string) error {
13381350
body, _ := json.Marshal(Message{From: cfg.SelfID, Type: typ, Data: data})
13391351
resp, err := http.Post(cfg.SignalingURL+"/publish?to="+url.QueryEscape(to),
13401352
"application/json", strings.NewReader(string(body)))
13411353
if err != nil {
1342-
log.Printf("publish err: %v", err)
1343-
return
1354+
return err
1355+
}
1356+
defer resp.Body.Close()
1357+
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))
1358+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
1359+
return fmt.Errorf("%s: %s", resp.Status, strings.TrimSpace(string(respBody)))
13441360
}
1345-
io.Copy(io.Discard, resp.Body)
1346-
resp.Body.Close()
1361+
return nil
13471362
}
13481363

13491364
func (m *Mesh) pollLoop(ctx context.Context) {

0 commit comments

Comments
 (0)