Executar e registar o âmbito
Guarda o código completo abaixo como run.py e executa python3 run.py --output evidence.json. Precisas de um sistema com IPv6 loopback e permissão para criar sockets locais. A execução registada usou Python 3.13.1 em macOS; a documentação consultada da linha 3.13 identificava 3.13.16. O script liga apenas a 127.0.0.1 e ::1, sem listeners wildcard ou alterações globais de rede. As portas são temporárias. Se IPv6 não estiver disponível, regista a limitação em vez de apresentar o grupo como aprovado ou desativar verificações silenciosamente.
Preparar um endereço não é ligar
O primeiro grupo usa getaddrinfo com endereços numéricos e transporte TCP. O resultado inclui família, tipo e sockaddr para uma tentativa posterior. Não significa que a porta 443 esteja aberta, que exista HTTPS ou que o DNS tenha respondido. O grupo de rejeição usa AI_NUMERICHOST e AI_NUMERICSERV: um nome de host e um nome de serviço são recusados antes de qualquer connect. Num adaptador fictício, classificar ambos como avaria de firewall enviaria o ticket para a equipa errada. Regista a fase e as flags juntamente com o erro.
Normalizar o valor e preservar o contexto
A forma ::1 e a forma expandida 0:0:0:0:0:0:0:1 produzem os mesmos 16 bytes. O ensaio compara esses bytes e volta a apresentar ::1. Esta observação evita falsos incidentes causados por comparação literal de strings. Para endereços link-local, preserva também a zona quando necessária: fe80::42%7 e fe80::42%9 não devem ser fundidos apenas porque os bits do endereço coincidem. Este segundo exemplo é análise documental baseada no RFC 4007; não foi executado tráfego link-local. A numeração da zona é local ao nó e não deve ser copiada entre hosts como identificador universal.
Comparar listeners com a mesma porta
O laboratório cria um listener TCP IPv4 em 127.0.0.1 e um listener TCP IPv6 em ::1, ambos com a mesma porta numérica. No IPv6 define e lê IPV6_V6ONLY=1. As respostas v4-endpoint e v6-endpoint identificam endpoints didáticos distintos dentro do script. Não se trata de um único listener dual-stack. A documentação Python descreve endereços IPv4 mapeados noutra configuração, mas essa configuração não foi executada aqui. Num rollout fictício, versões diferentes por família podem resultar de listeners distintos. Identifica socket, processo e configuração antes de culpar a cache ou alterar clientes.
Inverter a perspetiva sem perder o fluxo
Para cada ligação, compara getsockname e getpeername no cliente com o endereço do socket aceite no servidor. Sem NAT ou proxy neste ensaio, a origem local do cliente coincide com o peer aceite. Duas ligações simultâneas para o mesmo listener conservam portas de origem diferentes. O grupo de bind explícito pede porta zero e regista a porta atribuída antes e depois de ligar. Zero é uma política de seleção, não a porta efetiva a procurar nos logs. Em produção com intermediários, regista cada salto; esta correspondência direta não deve ser presumida através de NAT ou proxy.
Acrescentar o transporte à grelha
Outro grupo liga um socket UDP ao mesmo endereço IPv4 e número de porta do listener TCP. O datagrama UDP-ONLY chega ao socket UDP, mantendo o listener TCP aberto. Usa a observação para completar a grelha: protocolo, família, origem, porta de origem, destino, porta de destino, contexto e instante. A porta isolada é insuficiente para atribuir tráfego a uma aplicação. O ensaio não converte protocolos nem mede perda de datagramas. Na recolha operacional, distingue ainda o endereço configurado do candidato efetivamente selecionado e do peer observado, para que a equipa seguinte consiga reproduzir a tentativa.
"""Original endpoint observations using only numeric IPv4/IPv6 loopback addresses.
python3 run.py --output evidence.json
No DNS traffic, wildcard binds, packet capture or global network changes.
"""
import argparse,errno,hashlib,json,pathlib,platform,socket,time
from contextlib import ExitStack
from datetime import datetime,timezone
def run():
checks={};details={};tracked=[]
flags=socket.AI_NUMERICHOST|socket.AI_NUMERICSERV
def check(name,values,ok):
checks[name]={'passed':bool(ok),**values}
if not ok:raise AssertionError(name+': '+json.dumps(values))
def candidates(host,port):return socket.getaddrinfo(host,str(port),socket.AF_UNSPEC,socket.SOCK_STREAM,socket.IPPROTO_TCP,flags)
def receive(sock,count):
data=bytearray()
while len(data)<count:
part=sock.recv(count-len(data))
if not part:raise EOFError('fixture response incomplete')
data.extend(part)
return bytes(data)
with ExitStack() as stack:
def make(family,kind=socket.SOCK_STREAM):
s=stack.enter_context(socket.socket(family,kind));s.settimeout(2);tracked.append(s);return s
def accepted(listener):
s,peer=listener.accept();stack.enter_context(s);tracked.append(s);s.settimeout(2);return s,peer
v4=candidates('127.0.0.1',443);v6=candidates('::1',443)
check('numeric-candidates-are-family-specific',{'ipv4Families':sorted(set(socket.AddressFamily(x[0]).name for x in v4)),'ipv6Families':sorted(set(socket.AddressFamily(x[0]).name for x in v6)),'allTCP':all(x[1]==socket.SOCK_STREAM and x[2]==socket.IPPROTO_TCP for x in v4+v6),'socketConnectCalls':0},all(x[0]==socket.AF_INET for x in v4) and all(x[0]==socket.AF_INET6 for x in v6) and len(v4)>0 and len(v6)>0)
expanded='0:0:0:0:0:0:0:1';packed=socket.inet_pton(socket.AF_INET6,expanded);short=socket.inet_ntop(socket.AF_INET6,packed)
check('equivalent-ipv6-text-has-identical-address-bytes',{'inputStringsEqual':expanded=='::1','packedAddressesEqual':packed==socket.inet_pton(socket.AF_INET6,'::1'),'normalized':short,'bytes':len(packed)},short=='::1' and packed==socket.inet_pton(socket.AF_INET6,'::1'))
rejected={}
for name,host,service in [('host','fixture.invalid','443'),('service','127.0.0.1','https')]:
try:candidates(host,service)
except socket.gaierror as exc:rejected[name]={'type':type(exc).__name__,'code':exc.errno}
else:raise AssertionError('numeric-only input accepted a name')
details['numericRejections']=rejected
check('numeric-only-flags-reject-host-and-service-names',{'hostRejected':True,'serviceRejected':True,'socketConnectCalls':0,'numericHostFlag':True,'numericServiceFlag':True},len(rejected)==2)
listen4=make(socket.AF_INET);listen4.bind(('127.0.0.1',0));listen4.listen(4);port=listen4.getsockname()[1]
listen6=make(socket.AF_INET6);listen6.setsockopt(socket.IPPROTO_IPV6,socket.IPV6_V6ONLY,1);listen6.bind(('::1',port,0,0));listen6.listen(4)
observed={}
for label,family,listener,target in [('v4',socket.AF_INET,listen4,('127.0.0.1',port)),('v6',socket.AF_INET6,listen6,('::1',port,0,0))]:
client=make(family);client.connect(target);server,peer=accepted(listener);tag=(label+'-endpoint').encode();server.sendall(tag);body=receive(client,len(tag));observed[label]={'clientLocal':client.getsockname(),'clientPeer':client.getpeername(),'acceptedPeer':peer,'serverLocal':server.getsockname(),'body':body.decode()}
details['familyEndpoints']=observed
check('same-port-number-can-identify-separate-family-listeners',{'samePort':listen4.getsockname()[1]==listen6.getsockname()[1],'ipv6Only':listen6.getsockopt(socket.IPPROTO_IPV6,socket.IPV6_V6ONLY),'ipv4Body':observed['v4']['body'],'ipv6Body':observed['v6']['body'],'wildcardBound':False},observed['v4']['body']=='v4-endpoint' and observed['v6']['body']=='v6-endpoint')
flows=[]
for n in range(2):
client=make(socket.AF_INET);client.connect(listen4.getsockname());server,peer=accepted(listen4);flows.append({'source':client.getsockname(),'destination':client.getpeername(),'acceptedPeer':peer})
details['simultaneousFlows']=flows
check('source-port-distinguishes-simultaneous-flows',{'sameDestination':flows[0]['destination']==flows[1]['destination'],'differentSourcePorts':flows[0]['source'][1]!=flows[1]['source'][1],'acceptedPeersMatch':all(x['source']==x['acceptedPeer'] for x in flows)},flows[0]['destination']==flows[1]['destination'] and flows[0]['source'][1]!=flows[1]['source'][1] and all(x['source']==x['acceptedPeer'] for x in flows))
client=make(socket.AF_INET6);client.connect(listen6.getsockname());before={'connected':True,'acceptCallsForThisConnection':0,'applicationReads':0,'applicationAuthentication':False};server,peer=accepted(listen6)
check('connect-can-complete-before-application-accept',before,client.getpeername()[:2]==listen6.getsockname()[:2])
client=make(socket.AF_INET6);client.bind(('::1',0,0,0));before=client.getsockname();client.connect(listen6.getsockname());after=client.getsockname();server,peer=accepted(listen6)
details['boundSource']={'beforeConnect':before,'afterConnect':after,'acceptedPeer':peer}
check('explicit-source-bind-is-visible-at-the-peer',{'sourcePortPreserved':before[1]==after[1],'sourceAddressPreserved':before[0]==after[0],'acceptedPeerMatches':after==peer,'scopeId':after[3]},before==after==peer)
udp=make(socket.AF_INET,socket.SOCK_DGRAM);udp.bind(('127.0.0.1',port));sender=make(socket.AF_INET,socket.SOCK_DGRAM);sender.bind(('127.0.0.1',0));sender.sendto(b'UDP-ONLY',udp.getsockname());body,peer=udp.recvfrom(32)
details['udp']={'source':sender.getsockname(),'destination':udp.getsockname(),'receivedFrom':peer}
check('transport-protocol-separates-the-same-address-and-port',{'sameAddressPort':udp.getsockname()==listen4.getsockname(),'udpBody':body.decode(),'tcpListenerStillOpen':listen4.fileno()!=-1},udp.getsockname()==listen4.getsockname() and body==b'UDP-ONLY' and peer==sender.getsockname())
reserve=make(socket.AF_INET6);reserve.setsockopt(socket.IPPROTO_IPV6,socket.IPV6_V6ONLY,1);reserve.bind(('::1',0,0,0));unused=reserve.getsockname();reserve.close()
attempts=[];selected=None;started=time.monotonic();deadline=started+2
for family,target in [(socket.AF_INET6,unused),(socket.AF_INET,listen4.getsockname())]:
remaining=deadline-time.monotonic()
if remaining<=0:raise TimeoutError('candidate budget expired')
candidate=make(family);candidate.settimeout(remaining)
try:candidate.connect(target)
except OSError as exc:
attempts.append({'family':int(family),'address':target,'outcome':type(exc).__name__,'errno':exc.errno});candidate.close()
else:
selected=candidate;attempts.append({'family':int(family),'address':target,'outcome':'connected'});break
assert selected is not None
server,peer=accepted(listen4);server.sendall(b'FALLBACK-OK');body=receive(selected,11)
details['candidateAttempts']={'attempts':attempts,'elapsedSeconds':time.monotonic()-started,'releasedIPv6Port':unused,'portReuseRacePossible':True}
check('failed-candidate-does-not-prove-service-unavailable',{'attemptCount':len(attempts),'firstErrnoIsConnectionRefused':attempts[0].get('errno')==errno.ECONNREFUSED,'secondConnected':attempts[-1]['outcome']=='connected','body':body.decode(),'parallelRacingImplemented':False},len(attempts)==2 and attempts[0].get('errno')==errno.ECONNREFUSED and body==b'FALLBACK-OK')
host,service=socket.getnameinfo(listen6.getsockname(),socket.NI_NUMERICHOST|socket.NI_NUMERICSERV)
check('numeric-endpoint-logging-keeps-host-and-port-separate',{'host':host,'serviceMatchesPort':service==str(port),'serviceIsNumeric':service.isdecimal(),'reverseLookupRequested':False},host=='::1' and service==str(port))
return {'executedAt':datetime.now(timezone.utc).isoformat(),'pythonVersion':platform.python_version(),'platform':platform.platform(),'checks':checks,'details':details,'passed':sum(x['passed'] for x in checks.values()),'failed':sum(not x['passed'] for x in checks.values()),'allSocketsClosed':all(x.fileno()==-1 for x in tracked),'scriptSha256':hashlib.sha256(pathlib.Path(__file__).read_bytes()).hexdigest(),'scope':'Actual IPv4 and IPv6 TCP plus IPv4 UDP loopback sockets; numeric address conversion and lookup APIs. No DNS queries, wildcard listeners, NAT, proxy, link-local interface traffic, routing changes, packet capture, PMTU measurement, TLS, external traffic, load or business persistence. Sequential supplied-candidate fallback is not a Happy Eyeballs implementation. The released test port can be reused by another process; explicit refusal is checked rather than assumed.'}
if __name__=='__main__':
p=argparse.ArgumentParser();p.add_argument('--output',required=True);args=p.parse_args();result=run();pathlib.Path(args.output).write_text(json.dumps(result,indent=2)+'\n');print(json.dumps({k:result[k] for k in ['passed','failed','allSocketsClosed']}))
Exercício: compara cliente local=127.0.0.1:51000, peer=127.0.0.1:42000 com o socket aceite. Depois acrescenta uma segunda ligação e identifica o campo que as distingue.
Armadilhas comuns
Guardar apenas a porta, comparar IPv6 como strings, eliminar a zona de um endereço scoped ou copiar uma porta efémera como configuração universal.
Tópicos relacionados: Endereços, prefixos e âmbito · Rotas e resolução do próximo salto · Diagnóstico no contexto da aplicação
A evidência deve identificar o fluxo observado: transporte, família, duas extremidades e contexto. Configuração e observação são campos diferentes.
Referência: Python socket interface · DR TCP/IP 2026-09; TCP RFC 9293; IPv6 RFC 8200 with RFC 9673 update; Linux socket and iproute2 guidance