Outbound connections: pinning dials only the first resolved address, so one dead address fails the connection #547
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#547
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo at
95348908d, reading the address pinning in the HTTP guards and incore/destinations.py, and httpcore's connection code. Not reproduced against a real host.Checked again on 2026-10-01 against
mainat0a5cad587: still present. The code is now atplugins/http.py:78(_guard),plugins/http.py:134(_public_only) andplugins/http.py:96(approve_host);core/destinations.py:73and:85(approve);core/mail.py:180(_open) andcore/mail.py:240(GuardedBackend.open).The guards approve every address a name resolves to and then connect to the first one only. Before pinning (#58, #148, #215), httpcore and
smtplibopened sockets withsocket.create_connection, which tries each address in turn. Pinning a single address removed that fallback, so a name with one unreachable address now fails whenever that address comes first.plugins/http.py:76(_guard:_pin(request, addresses[0])) andplugins/http.py:132(_public_only, the same);core/destinations.py:72and:84(approvereturnsfound[0]), used bycore/mail.py:190(_open),GuardedBackend.openandplugins/http.py:94(approve_host).socket.create_connectionhas one address to try. Two cases meet it. A host whose IPv6 is configured but broken upstream gets the AAAA answer sorted first and times out on every webhook, capture, link check and SMTP connection to a dual-stack name, while the IPv4 address works. A round-robin name with one node down fails on the passes where the resolver lists that node first. The HTTP guard only pins under the default public-only policy (withPOSTULO_CONNECTIONS_ALLOW_PRIVATEon,_guardreturns early and httpx resolves and falls back as before);destinations.approvepins in both cases.Hostheader: inPinnedSMTP._get_socketloop over the approved addresses, and for HTTP use a transport or network backend that receives the approved list (or retry the request over the remaining addresses onhttpx.ConnectError). Every address was already approved, so trying the next one opens no gap.Done when
tests/security/test_outbound.pystubs the resolver to answer[unreachable, reachable]for one public name and asserts a request throughhttp.client()and a connection throughmail.check_connectionboth succeed on the second address, with the certificate name still the typed host.