Dois caminhos para cada pacote
Num túnel GRE, distingue o destino interno do endereço externo usado para chegar ao outro extremo. Neste laboratório, 198.51.100.1 e 198.51.100.2 são os extremos internos; 192.0.2.1 e 192.0.2.2 pertencem ao underlay. A rota do destino interno aponta para gre-lab, enquanto a consulta do endereço externo deve sair pela interface wire. Esta separação permite transportar um pacote por outra rede, mas não demonstra confidencialidade. O GRE básico sobre IPv4 usa o número de protocolo IP 47, não a porta TCP ou UDP 47. Ao pedir uma regra de firewall, indica protocolo, extremos e sentido; confirma também o retorno e a política aplicada ao tráfego depois da desencapsulação.
Um túnel ativo pode ter o destino errado
O ensaio começa por observar lookup e sondas em ambos os sentidos. Depois muda o remote do túnel right para 192.0.2.99. Os endereços externos originais continuam a responder, e as interfaces GRE mantêm a flag administrativa UP, mas a sonda entre os extremos internos falha. Isto separa três factos: a interface foi ativada, o underlay alcança o peer original e o overlay entrega tráfego útil. Não uses um deles como substituto dos restantes. Compara local e remote dos dois lados antes de alterar rotas da aplicação. A reposição de 192.0.2.1 no extremo right recupera as sondas internas. Este comportamento foi observado em Linux; não constitui ensaio do line protocol ou dos keepalives de IOS XE.
Detetar uma dependência circular de encaminhamento
A fase seguinte instala em left uma rota /32 para 192.0.2.2 através do próprio gre-lab. Por ser mais específica, a consulta do destino externo passa a selecionar o túnel que depende desse destino para funcionar; a sonda interna falha. Remove-se apenas a rota introduzida e confirma-se a recuperação. Num incidente real, guarda a origem e a preferência da rota, além do resultado do lookup: uma redistribuição ou anúncio pelo overlay pode ter criado a dependência. A documentação Cisco descreve situações de RECURDOWN relacionadas com este problema, mas o laboratório não produz esse log Cisco. Uma rota estática pode ser adequada a um desenho aprovado; não a uses para esconder indefinidamente uma política de anúncios incorreta.
Calcular MTU sem confundir payload, ICMP e TCP
Com underlay de 1500 bytes, IPv4 externo sem opções de 20 bytes e GRE básico de 4 bytes, o limite interno usado é 1476. No iputils ping, -s conta apenas os dados: 1448 + 8 de ICMP + 20 de IPv4 interno = 1476. Com -M do, esse tamanho passa e 1449 é rejeitado localmente com Message too large. Ao reduzir a MTU do túnel para 1400, passam 1372 bytes de dados e deixam de passar 1448; o underlay mantém 1500. Estes resultados não demonstram um black hole de PMTUD numa rede remota. Opções GRE, IPv6 ou proteção IPsec alteram a conta. MSS refere-se aos dados TCP e requer cálculo próprio; ajustar MSS não resolve genericamente tráfego UDP nem prova transferência aplicacional.
Aceitar o serviço e a proteção com evidência própria
Numa mudança fictícia de APS, um ficheiro de reconciliação pequeno chega, mas o lote maior não termina antes do limite operacional. Mantém tamanho, origem, destino, protocolo e janela temporal na evidência; compara limites de MTU e erros antes de atribuir a falha à aplicação. PMTUD IPv4 clássico usa feedback ICMP quando um router não pode encaminhar sem fragmentar um pacote com DF; o bloqueio desse feedback é uma hipótese a investigar, não a conclusão deste ensaio local. Se o contrato exigir confidencialidade, GRE sozinho não basta. Valida a política IPsec aplicável ao fluxo, as associações de segurança e os serviços criptográficos acordados. Uma sessão de negociação ativa não prova que o lote percorreu a política protegida. Recolhe evidência de envio, receção, integridade e duração do lote.
Reproduzir, interpretar e reverter o laboratório
O script completo abaixo requer Python 3 e Docker. Prepara a imagem dr-encor-gre-tools:20240905-r0 com o Dockerfile do laboratório: usa a imagem FRR fixada por digest e acrescenta apenas iputils-ping=20240905-r0. O ping BusyBox da imagem original não suporta -M; esse erro inicial não foi contado como falha de rede. O ensaio cria dois namespaces num contentor sem rede externa, portas publicadas ou volumes e não arranca daemons FRR. Guarda versões, lookups, sondas, sete fases e 24 verificações. Duas execuções independentes confirmaram recuperação e remoção dos contentores. A MTU final volta a 1476. O pacote não valida IOS XE, IPsec, TCP MSS, aplicações, desempenho ou redundância física. Para produção, acrescenta esses critérios conforme o contrato e atribui responsáveis pelo rollback e pela entrega ao RUN.
# Toolbox preparation (shell commands, run once before this Python script):
# Save the following two lines as Dockerfile:
# FROM quay.io/frrouting/frr@sha256:b0faf7c8f3d8b09aea1e38f77603c745a66dbf5e26ef9718527c2fbb53cc3aed
# RUN apk add --no-cache iputils-ping=20240905-r0
# docker build --pull=false -t dr-encor-gre-tools:20240905-r0 .
# python3 run.py /tmp/gre-evidence.json
"""Original Linux GRE lab. Only a disposable networkless Docker container is changed."""
import datetime,hashlib,json,pathlib,subprocess,sys,uuid
IMAGE='dr-encor-gre-tools:20240905-r0'
name='dr-encor-gre-'+uuid.uuid4().hex[:8];created=False;checks=[];phases=[]
def docker(args,check=True):
r=subprocess.run(['docker',*args],capture_output=True,text=True,timeout=25)
if check and r.returncode:raise RuntimeError(str(args)+'\n'+r.stdout+r.stderr)
return r
def ex(*args,check=True):return docker(['exec',name,*args],check)
def ip(ns,*args,check=True):return ex('ip','-n',ns,*args,check=check)
def pack(r):return {'exit':r.returncode,'stdout':r.stdout,'stderr':r.stderr}
def verify(label,value):
if not value:raise AssertionError(label)
checks.append(label)
def probe(ns='left',size=56,outer=False):
dest=('192.0.2.2' if ns=='left' else '192.0.2.1')if outer else ('198.51.100.2'if ns=='left'else'198.51.100.1')
source=('192.0.2.1' if ns=='left'else'192.0.2.2')if outer else ('198.51.100.1'if ns=='left'else'198.51.100.2')
return ex('ip','netns','exec',ns,'ping','-c','1','-W','1','-M','do','-I',source,'-s',str(size),dest,check=False)
def snapshot(label):
d={'name':label,'observedAt':datetime.datetime.now(datetime.timezone.utc).isoformat(),'nodes':{}}
for ns,remote in [('left','192.0.2.2'),('right','192.0.2.1')]:
d['nodes'][ns]={'links':json.loads(ip(ns,'-j','-d','link').stdout),'routes':json.loads(ip(ns,'-j','route').stdout),'outerLookup':pack(ip(ns,'-j','route','get',remote,check=False)),'innerLookup':pack(ip(ns,'-j','route','get','198.51.100.2'if ns=='left'else'198.51.100.1',check=False)),'smallProbe':pack(probe(ns)),'underlayProbe':pack(probe(ns,outer=True))}
phases.append(d);return d
try:
image=json.loads(docker(['image','inspect',IMAGE]).stdout)[0]
docker(['run','-d','--name',name,'--label','dr.lab=encor-gre','--network','none','--memory','128m','--cpus','0.5','--pids-limit','80','--cap-add','NET_ADMIN','--cap-add','NET_RAW','--cap-add','SYS_ADMIN','--entrypoint','/bin/sh',IMAGE,'-c','sleep 900']);created=True
kernel=ex('uname','-r').stdout.strip();version=ex('ip','-Version').stdout.strip();ping_version=ex('ping','-V').stdout.strip()
for ns in ['left','right']:ex('ip','netns','add',ns);ip(ns,'link','set','lo','up')
ex('ip','link','add','left-wire','type','veth','peer','name','right-wire')
for ns,i,peer in [('left',1,2),('right',2,1)]:
wire=ns+'-wire';ex('ip','link','set',wire,'netns',ns);ip(ns,'link','set',wire,'mtu','1500');ip(ns,'address','add',f'192.0.2.{i}/24','dev',wire);ip(ns,'link','set',wire,'up')
ip(ns,'tunnel','add','gre-lab','mode','gre','local',f'192.0.2.{i}','remote',f'192.0.2.{peer}','ttl','64');ip(ns,'link','set','gre-lab','mtu','1476');ip(ns,'address','add',f'10.240.0.{i}/30','dev','gre-lab');ip(ns,'link','set','gre-lab','up')
ip(ns,'address','add',f'198.51.100.{i}/32','dev','lo');ip(ns,'route','add',f'198.51.100.{peer}/32','via',f'10.240.0.{peer}','dev','gre-lab')
verify('container global table is empty',json.loads(ex('ip','-j','route').stdout)==[])
base=snapshot('baseline')
for ns in ['left','right']:
n=base['nodes'][ns];verify(ns+' underlay responds',n['underlayProbe']['exit']==0);verify(ns+' inner endpoint responds',n['smallProbe']['exit']==0)
verify(ns+' outer destination uses underlay',json.loads(n['outerLookup']['stdout'])[0]['dev']==ns+'-wire')
verify(ns+' inner destination uses GRE',json.loads(n['innerLookup']['stdout'])[0]['dev']=='gre-lab')
size1476=probe(size=1448);size1477=probe(size=1449)
verify('1476-byte inner packet with DF passes',size1476.returncode==0);verify('1477-byte inner packet with DF is rejected',size1477.returncode!=0)
base['boundary']={'payload1448':pack(size1476),'payload1449':pack(size1477)}
ip('right','tunnel','change','gre-lab','mode','gre','local','192.0.2.2','remote','192.0.2.99','ttl','64');bad=snapshot('wrong-remote')
verify('wrong remote breaks overlay',bad['nodes']['left']['smallProbe']['exit']!=0)
verify('wrong remote retains underlay reachability',all(n['underlayProbe']['exit']==0 for n in bad['nodes'].values()))
verify('wrong remote leaves administrative UP flags',all('UP' in next(l for l in n['links']if l['ifname']=='gre-lab')['flags']for n in bad['nodes'].values()))
ip('right','tunnel','change','gre-lab','mode','gre','local','192.0.2.2','remote','192.0.2.1','ttl','64');fixed=snapshot('remote-restored')
verify('remote restoration recovers both directions',all(n['smallProbe']['exit']==0 for n in fixed['nodes'].values()))
ip('left','route','add','192.0.2.2/32','dev','gre-lab');recursive=snapshot('recursive-underlay-route')
verify('bad host route sends outer destination into GRE',json.loads(recursive['nodes']['left']['outerLookup']['stdout'])[0]['dev']=='gre-lab')
verify('recursive underlay route breaks left overlay probe',recursive['nodes']['left']['smallProbe']['exit']!=0)
ip('left','route','del','192.0.2.2/32');fixed=snapshot('underlay-route-restored')
verify('underlay route recovery restores both directions',all(n['smallProbe']['exit']==0 for n in fixed['nodes'].values()))
for ns in ['left','right']:ip(ns,'link','set','gre-lab','mtu','1400')
reduced=snapshot('tunnel-mtu-1400');small=probe(size=1372);large=probe(size=1448);reduced['boundary']={'payload1372':pack(small),'payload1448':pack(large)}
verify('1400-byte inner packet passes reduced MTU',small.returncode==0);verify('1476-byte inner packet is rejected at reduced local MTU',large.returncode!=0)
verify('reduced tunnel MTU leaves small probes healthy',all(n['smallProbe']['exit']==0 for n in reduced['nodes'].values()))
verify('underlay MTU remains 1500',all(next(l for l in n['links']if l['ifname']==ns+'-wire')['mtu']==1500 for ns,n in reduced['nodes'].items()))
for ns in ['left','right']:ip(ns,'link','set','gre-lab','mtu','1476')
final=snapshot('all-changes-reverted');final['largeProbe']=pack(probe(size=1448))
verify('final large DF probe recovers',final['largeProbe']['exit']==0);verify('final bidirectional small probes recover',all(n['smallProbe']['exit']==0 for n in final['nodes'].values()))
evidence={'checkedAt':datetime.datetime.now(datetime.timezone.utc).isoformat(),'image':IMAGE,'imageId':image['Id'],'kernel':kernel,'iproute2':version,'ping':ping_version,'scriptSha256':hashlib.sha256(pathlib.Path(__file__).read_bytes()).hexdigest(),'checks':checks,'phases':phases,'limitations':['Linux GRE over IPv4 in two namespaces on one host; no Cisco IOS XE execution','FRR image supplies tools only; no FRR routing daemons started','Basic GRE without key, checksum or sequence options; explicit tunnel MTU 1476 over underlay 1500','MTU failures are local DF/MTU rejection, not an exercised remote PMTUD black hole','No IPsec, IKE, encryption, TCP MSS, IPv6, application, throughput or physical resilience validation','No external network, ports, mounts or host kernel changes'],'cleanup':None}
except Exception:
try:pathlib.Path(sys.argv[1]+'.failure.json').write_text(json.dumps({'checks':checks,'phases':phases,'left':ip('left','-d','link',check=False).stdout,'right':ip('right','-d','link',check=False).stdout},indent=2))
except Exception:pass
raise
finally:
cleanup=[{'container':name,'exit':docker(['rm','-f',name],False).returncode}]if created else []
if any(x['exit']for x in cleanup):raise RuntimeError(cleanup)
evidence['cleanup']=cleanup;pathlib.Path(sys.argv[1]).write_text(json.dumps(evidence,indent=2)+'\n');print(json.dumps({'checks':len(checks),'phases':len(phases),'cleanup':True}))
Com MTU 1476, ping -M do -s 1448 passa; -s 1449 recebe Message too large.
Armadilhas comuns
Tratar protocolo IP 47 como porta; confundir UP com entrega; resolver o extremo externo pelo próprio túnel; confundir -s com MTU; concluir que GRE cifra o tráfego.
Tópicos relacionados: VRF: contexto e partilha controlada · Observação e diagnóstico da rede
Verifica os dois lookups, testa tamanhos conhecidos e demonstra a recuperação e a proteção separadamente.
Referência: Generic Routing Encapsulation · 350-401 ENCOR v1.2, effective 2026-03-19; core component of CCNP Enterprise