Topologia e âmbito da execução
O script cria um único contentor sem rede externa, com quatro namespaces: blue, red e dois extremos correspondentes. Zebra e mgmtd arrancam com -w, o backend de VRF do FRRouting baseado em namespaces. Blue e red usam cada um 192.0.2.1/24; o respetivo peer usa 192.0.2.2/24 e tem 198.51.100.10/32 na loopback. Uma rota estática em cada contexto aponta para o seu peer. Assim, o mesmo destino numérico refere extremos distintos. Não existem dispositivos Linux l3mdev VRF neste ensaio; a tentativa inicial de os criar falhou no ambiente disponível. A implementação executada é FRRouting 10.4.5, não IOS XE.
Caso: o monitor consulta a tabela errada
Na primeira fase, os dois contextos alcançam o respetivo destino e a tabela global continua sem rota. Uma sonda global falha, enquanto a sonda dentro de blue ou red passa. Este resultado não é contraditório: cada comando escolhe um contexto diferente. Num cenário APS fictício, um monitor criado sem VRF gera um incidente enquanto o fluxo do cliente funciona. Corrige a representatividade do monitor antes de concluir avaria geral. O laboratório usa ip netns exec para executar ping no namespace escolhido e ip -n para consultar a sua tabela. Não usa um ping global como substituto dos dois testes.
Retirar uma rota mantendo o trânsito
O script retira apenas a rota 198.51.100.10/32 de blue. O lookup nesse contexto falha e o destino deixa de responder. O peer 192.0.2.2 continua acessível, o que conserva evidência de conectividade no segmento de trânsito. Red mantém a sua própria rota e resposta ao mesmo endereço numérico. A reposição da rota blue recupera a sonda. A sequência mostra por que razão se devem guardar consulta de rota, sonda ao peer e sonda ao destino: os três resultados respondem a perguntas diferentes. Não foi necessário reiniciar red nem importar a sua rota para recuperar blue.
Retirar o destino mantendo a rota
A segunda falha remove 198.51.100.10/32 da loopback do peer red. A rota estática em red continua instalada e o lookup ainda devolve o next hop. Mesmo assim, o destino deixa de responder; o peer de trânsito continua acessível e blue não perde conectividade. Repor o endereço recupera red. Uma rota válida não confirma presença do endereço remoto, estado do serviço ou autorização da aplicação. O ensaio apenas observa ICMP IPv4. Para um coletor de logs real, seria ainda necessário verificar transporte, autenticação e receção da mensagem com a origem usada pelos agentes.
Reproduzir, observar e encerrar
Guarda o script completo abaixo como run.py. Com Docker ativo e a imagem indicada já disponível, executa python3 run.py /tmp/vrf-evidence.json. O script regista versões, hash, cinco fases e 24 verificações, e remove o contentor no fim. Duas execuções completas passaram. As capacidades NET_ADMIN, NET_RAW e SYS_ADMIN são usadas apenas no contentor temporário para configurar namespaces; não existem portas publicadas, volumes ou rede do anfitrião. Todos os contextos partilham a mesma máquina, pelo que não se mede redundância física. Também não se executam BGP, MPLS, importação por RT, route leaking, IPv6, TLS ou débito.
# Save as run.py; execute: python3 run.py /tmp/vrf-evidence.json
"""Original FRR namespace-backed VRF lab; only one disposable container is changed."""
import datetime,hashlib,json,pathlib,subprocess,sys,time,uuid
IMAGE='quay.io/frrouting/frr@sha256:b0faf7c8f3d8b09aea1e38f77603c745a66dbf5e26ef9718527c2fbb53cc3aed'
name='dr-encor-vrf-'+uuid.uuid4().hex[:8];created=False;checks=[];phases=[];DEST='198.51.100.10'
def docker(args,check=True):
r=subprocess.run(['docker',*args],capture_output=True,text=True,timeout=30)
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 cli(command):
r=ex('vtysh','-c',command).stdout
if '% Unknown' in r or '% Ambiguous' in r:raise RuntimeError(r)
return r
def verify(label,condition):
if not condition:raise AssertionError(label)
checks.append(label)
def result(r):return {'exit':r.returncode,'stdout':r.stdout,'stderr':r.stderr}
def lookup(vrf=None):return ex('ip',*(['-n',vrf]if vrf else []),'-j','route','get',DEST,check=False)
def ping(vrf=None,dest=DEST):return ex(*(['ip','netns','exec',vrf]if vrf else []),'ping','-c','1','-W','1',dest,check=False)
def snapshot(label):
time.sleep(0.3)
d={'name':label,'observedAt':datetime.datetime.now(datetime.timezone.utc).isoformat(),'frrVRFs':cli('show vrf'),'main':json.loads(ex('ip','-j','route','show','table','main').stdout),'vrfs':{}}
for vrf in ['blue','red']:
d['vrfs'][vrf]={'routes':json.loads(ex('ip','-n',vrf,'-j','route').stdout),'frrRoutes':json.loads(cli('show ip route vrf '+vrf+' json')),'lookup':result(lookup(vrf)),'probe':result(ping(vrf)),'neighbors':json.loads(ex('ip','-n',vrf,'-j','neigh').stdout),'peerAddresses':json.loads(ex('ip','-n',vrf+'-peer','-j','address').stdout)}
d['globalLookup']=result(lookup());d['globalProbe']=result(ping());phases.append(d);return d
try:
image=json.loads(docker(['image','inspect',IMAGE]).stdout)[0]
docker(['run','-d','--name',name,'--label','dr.lab=encor-vrf','--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();ipversion=ex('ip','-Version').stdout.strip()
for vrf in ['blue','red']:
peer=vrf+'-peer';link=vrf+'-if';other=vrf+'-end'
for ns in [vrf,peer]:ex('ip','netns','add',ns);ex('ip','-n',ns,'link','set','lo','up')
ex('ip','link','add',link,'type','veth','peer','name',other);ex('ip','link','set',link,'netns',vrf);ex('ip','link','set',other,'netns',peer)
ex('ip','-n',vrf,'address','add','192.0.2.1/24','dev',link);ex('ip','-n',vrf,'link','set',link,'up')
ex('ip','-n',peer,'address','add','192.0.2.2/24','dev',other);ex('ip','-n',peer,'link','set',other,'up');ex('ip','-n',peer,'address','add',DEST+'/32','dev','lo')
ex('touch','/etc/frr/frr.conf','/etc/frr/vtysh.conf');ex('chown','frr:frr','/etc/frr/frr.conf');ex('/usr/lib/frr/mgmtd','-w','-d','-A','127.0.0.1');ex('/usr/lib/frr/zebra','-w','-d','-A','127.0.0.1','--log','file:/tmp/zebra-vrf.log')
deadline=time.monotonic()+20
while time.monotonic()<deadline:
r=ex('vtysh','-c','show ip route vrf blue json',check=False)
if r.returncode==0 and '192.0.2.0/24' in r.stdout:break
time.sleep(0.5)
else:raise AssertionError('FRR namespace-backed VRF not ready')
for vrf in ['blue','red']:ex('ip','-n',vrf,'route','add',DEST+'/32','via','192.0.2.2','proto','static')
version=cli('show version');verify('FRR is version 10.4.5','10.4.5' in version)
base=snapshot('overlapping-prefixes')
verify('global main table has no route',base['main']==[]);verify('global lookup cannot resolve endpoint',base['globalLookup']['exit']!=0);verify('global probe cannot reach endpoint',base['globalProbe']['exit']!=0)
for vrf in ['blue','red']:
v=base['vrfs'][vrf];verify(vrf+' scoped probe reaches its endpoint',v['probe']['exit']==0)
route=json.loads(v['lookup']['stdout'])[0];verify(vrf+' lookup uses its own interface',route['dev']==vrf+'-if')
verify(vrf+' lookup resolves the transit next hop',route['gateway']=='192.0.2.2')
verify(vrf+' route exists in the FRR VRF',DEST+'/32' in v['frrRoutes'])
ex('ip','-n','blue','route','del',DEST+'/32');bad=snapshot('blue-route-withdrawn')
verify('blue withdrawal breaks lookup',bad['vrfs']['blue']['lookup']['exit']!=0);verify('blue withdrawal breaks probe',bad['vrfs']['blue']['probe']['exit']!=0)
verify('blue transit peer remains reachable',ping('blue','192.0.2.2').returncode==0)
verify('red survives blue route withdrawal',bad['vrfs']['red']['probe']['exit']==0)
ex('ip','-n','blue','route','add',DEST+'/32','via','192.0.2.2','proto','static');fixed=snapshot('blue-route-restored')
verify('blue route restoration restores probe',fixed['vrfs']['blue']['probe']['exit']==0);verify('red remains reachable after blue recovery',fixed['vrfs']['red']['probe']['exit']==0)
ex('ip','-n','red-peer','address','del',DEST+'/32','dev','lo');endpoint=snapshot('red-endpoint-withdrawn')
verify('red endpoint loss leaves route lookup valid',endpoint['vrfs']['red']['lookup']['exit']==0);verify('red endpoint loss breaks probe',endpoint['vrfs']['red']['probe']['exit']!=0)
verify('red transit peer remains reachable',ping('red','192.0.2.2').returncode==0);verify('blue survives red endpoint loss',endpoint['vrfs']['blue']['probe']['exit']==0)
ex('ip','-n','red-peer','address','add',DEST+'/32','dev','lo');final=snapshot('all-changes-reverted')
verify('both scoped probes recover',all(x['probe']['exit']==0 for x in final['vrfs'].values()));verify('global context remains unreachable',final['globalProbe']['exit']!=0)
evidence={'checkedAt':datetime.datetime.now(datetime.timezone.utc).isoformat(),'image':IMAGE,'imageId':image['Id'],'architecture':image['Architecture'],'kernel':kernel,'iproute2':ipversion,'frrVersion':version,'backend':'FRRouting zebra -w: Linux network namespaces as VRFs','scriptSha256':hashlib.sha256(pathlib.Path(__file__).read_bytes()).hexdigest(),'checks':checks,'phases':phases,'limitations':['FRRouting namespace-backed VRFs, not Cisco IOS XE or Linux l3mdev VRF device execution','Two studied routing namespaces plus two endpoint namespaces; Zebra discovers all four','IPv4 ICMP and route lookup only; no TCP, TLS, application identity, IPv6 or throughput claim','No BGP, MPLS, route-target import/export or inter-VRF route-leak execution','One physical host; logical isolation does not demonstrate physical resilience','No external network, ports or mounts; capabilities limited to disposable container'],'cleanup':None}
except Exception:
try:pathlib.Path(sys.argv[1]+'.failure.json').write_text(json.dumps({'checks':checks,'phases':phases,'namespaces':ex('ip','netns','list',check=False).stdout,'vrfs':ex('vtysh','-c','show vrf',check=False).stdout,'routes':ex('vtysh','-c','show ip route vrf all json',check=False).stdout,'interfaces':ex('vtysh','-c','show interface vrf all',check=False).stdout,'log':ex('tail','-n','40','/tmp/zebra-vrf.log',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),'kernel':kernel,'iproute2':ipversion,'cleanup':True}))
Blue sem rota: lookup e ping falham. Red sem endereço no extremo: lookup passa, ping falha.
Armadilhas comuns
Comparar sondas de contextos diferentes; atribuir toda a falha de ping a routing; apresentar namespaces como dispositivos l3mdev ou execução IOS XE.
Tópicos relacionados: VRF: contexto e partilha controlada · Virtualização, VRFs e overlays
Relaciona a falha injetada com a tabela, o trânsito e o destino antes de escolher a recuperação.
Referência: Zebra and Virtual Routing and Forwarding · 350-401 ENCOR v1.2, effective 2026-03-19; core component of CCNP Enterprise