@@ -86,9 +86,14 @@ func classifyFailover(statusCode int, body []byte, isTokenEndpoint bool) (class
8686 if bodyContainsAny (body , "insufficient_quota" , "quota_exceeded" , "quota exhausted" , "rate_limit_exceeded" ) {
8787 return failoverRateLimited , ""
8888 }
89- return failoverNone , ""
89+ // NOT a quota signal: do not early-return. A 403 is still a non-2xx
90+ // status, so a real token-endpoint body of invalid_grant/invalid_token
91+ // must classify as auth-failure (consistent with the 400/401 path).
92+ // The shared non-2xx token-endpoint check below handles it; a 403 from
93+ // a non-token-endpoint with an unrelated body still resolves to
94+ // failoverNone there (the body is only trusted on a real token URL).
9095 }
91- // Non-4xx -status path. Only a real token-endpoint body may be classified
96+ // Non-2xx -status path. Only a real token-endpoint body may be classified
9297 // (invalid_grant/invalid_token), and only when the status is not a 2xx
9398 // success. A 2xx token response is a healthy refresh, never a failover.
9499 if isTokenEndpoint && (statusCode < 200 || statusCode > 299 ) {
@@ -133,21 +138,26 @@ type FailoverEvent struct {
133138// poolForResponse maps a response's CONNECT destination back to a pooled
134139// binding and returns the pool name + the member that was active for this
135140// request. Returns ok=false when the destination is not bound to a pool.
136- func (a * SluiceAddon ) poolForResponse (f * mitmproxy.Flow ) (pool , activeMember string , pr * vault.PoolResolver , ok bool ) {
141+ //
142+ // proto is the protocol detected for THIS request (the same value used for
143+ // the protocol-scoped binding lookup). The caller threads it into the
144+ // cred_failover audit event so the audit records the real protocol of the
145+ // pooled binding (grpc / http2 / etc.) instead of a hardcoded "https".
146+ func (a * SluiceAddon ) poolForResponse (f * mitmproxy.Flow ) (pool , activeMember , proto string , pr * vault.PoolResolver , ok bool ) {
137147 if a .poolResolver == nil || a .resolver == nil {
138- return "" , "" , nil , false
148+ return "" , "" , "" , nil , false
139149 }
140150 pr = a .poolResolver .Load ()
141151 if pr == nil {
142- return "" , "" , nil , false
152+ return "" , "" , "" , nil , false
143153 }
144154 res := a .resolver .Load ()
145155 if res == nil {
146- return "" , "" , nil , false
156+ return "" , "" , "" , nil , false
147157 }
148158 host , port := connectTargetForFlow (a , f )
149159 if host == "" {
150- return "" , "" , nil , false
160+ return "" , "" , "" , nil , false
151161 }
152162 // Finding 3: the failover binding lookup MUST use the same protocol the
153163 // request-side injection (injectHeaders / buildPhantomPairs) used, not a
@@ -157,7 +167,7 @@ func (a *SluiceAddon) poolForResponse(f *mitmproxy.Flow) (pool, activeMember str
157167 // detectRequestProtocol mirrors the injection path exactly (URL scheme
158168 // then header refinement); for the common unscoped-binding case the
159169 // result is still https-equivalent so behavior is unchanged.
160- proto : = a .detectRequestProtocol (f , port ).String ()
170+ proto = a .detectRequestProtocol (f , port ).String ()
161171 for _ , boundName := range res .CredentialsForDestination (host , port , proto ) {
162172 if ! pr .IsPool (boundName ) {
163173 continue
@@ -176,15 +186,15 @@ func (a *SluiceAddon) poolForResponse(f *mitmproxy.Flow) (pool, activeMember str
176186 // member of this pool (a membership change could have
177187 // raced); otherwise fall through to ResolveActive.
178188 if pr .PoolForMember (injected ) == boundName {
179- return boundName , injected , pr , true
189+ return boundName , injected , proto , pr , true
180190 }
181191 }
182192 }
183193 member , mok := pr .ResolveActive (boundName )
184194 if ! mok || member == "" {
185195 continue
186196 }
187- return boundName , member , pr , true
197+ return boundName , member , proto , pr , true
188198 }
189199
190200 // Token-endpoint path. An OAuth refresh hits the credential's token-URL
@@ -246,7 +256,7 @@ func (a *SluiceAddon) poolForResponse(f *mitmproxy.Flow) (pool, activeMember str
246256 realRefresh := extractRequestRefreshToken (f .Request .Body , reqCT )
247257 if owner , ok := a .refreshAttr .Peek (realRefresh ); ok && owner != "" {
248258 if ownerPool := pr .PoolForMember (owner ); ownerPool != "" {
249- return ownerPool , owner , pr , true
259+ return ownerPool , owner , proto , pr , true
250260 }
251261 // owner is no longer in any pool (membership change
252262 // raced the failure); fall through to the active-member
@@ -261,20 +271,20 @@ func (a *SluiceAddon) poolForResponse(f *mitmproxy.Flow) (pool, activeMember str
261271 log .Printf ("[POOL-FAILOVER] pool %q: could not attribute " +
262272 "token-endpoint failure via injected refresh token; " +
263273 "falling back to active member %q" , pool , active )
264- return pool , active , pr , true
274+ return pool , active , proto , pr , true
265275 }
266276 // Last resort: a pooled index match if any (preserves prior
267277 // behavior when even ResolveActive cannot decide; better than
268278 // no attribution at all).
269279 for _ , c := range matches {
270280 if pr .PoolForMember (c ) != "" {
271- return pool , c , pr , true
281+ return pool , c , proto , pr , true
272282 }
273283 }
274- return pool , matched , pr , true
284+ return pool , matched , proto , pr , true
275285 }
276286 }
277- return "" , "" , nil , false
287+ return "" , "" , "" , nil , false
278288}
279289
280290// handlePoolFailover is the Phase 2 entry point invoked from Response for
@@ -302,7 +312,7 @@ func (a *SluiceAddon) handlePoolFailover(f *mitmproxy.Flow) {
302312 if f == nil || f .Response == nil || f .Request == nil {
303313 return
304314 }
305- pool , from , pr , ok := a .poolForResponse (f )
315+ pool , from , proto , pr , ok := a .poolForResponse (f )
306316 if ! ok {
307317 return
308318 }
@@ -352,11 +362,14 @@ func (a *SluiceAddon) handlePoolFailover(f *mitmproxy.Flow) {
352362 evt := audit.Event {
353363 Destination : host ,
354364 Port : port ,
355- Protocol : "https" ,
356- Verdict : "failover" ,
357- Action : "cred_failover" ,
358- Reason : fmt .Sprintf ("%s:%s->%s:%s" , pool , from , to , tag ),
359- Credential : from ,
365+ // Same protocol used for the protocol-scoped binding lookup in
366+ // poolForResponse, NOT a hardcoded "https". For a grpc/http2
367+ // scoped pooled binding the audit must record the real protocol.
368+ Protocol : proto ,
369+ Verdict : "failover" ,
370+ Action : "cred_failover" ,
371+ Reason : fmt .Sprintf ("%s:%s->%s:%s" , pool , from , to , tag ),
372+ Credential : from ,
360373 }
361374 if err := a .auditLog .Log (evt ); err != nil {
362375 log .Printf ("[POOL-FAILOVER] audit log error: %v" , err )
0 commit comments