← CCNP Security: núcleo SCOR e operação
24 / 25 · 55 MIN

SSE: proxy, origem e desvio de tráfego

Distingue seleção do proxy, autenticação, política, túnel e resposta da origem usando um ensaio HTTP local.

1. Identificar quem envia o tráfego

Um portal funciona no browser, mas o job de exportação da mesma máquina segue outro caminho. A configuração PAC do browser não comprova o comportamento de todos os processos. Inventaria cliente, versão, origem, método de encaminhamento e políticas do dispositivo. A documentação Cisco distingue o PAC gerido do ficheiro personalizado: alterações de bypass no portal não modificam automaticamente o personalizado. Confirma qual URL e conteúdo foram efetivamente carregados, incluindo caches e pré-requisitos de acesso. Não alteres as definições do sistema para executar este laboratório: ele escolhe explicitamente dois sockets locais. O objetivo é observar diferenças de caminho sem alegar validação do browser ou do tenant Cisco.

2. Localizar a recusa

No ensaio original, um pedido sem a credencial sintética recebe 407 com Proxy-Authenticate e não chega à origem. Depois da autenticação no proxy, /private devolve 401 com WWW-Authenticate vindo da aplicação. Há dois 403 diferentes: /blocked é recusado pelo proxy, enquanto /restricted chega à origem e é recusado aí. O código isolado não distingue estes dois casos; correlaciona camada, headers e presença do pedido nos registos. Estes resultados são do laboratório, não uma tabela universal Cisco. O caso oficial 226262 descreve um 401 num endpoint de diagnóstico associado à identificação da origem NAT. Antes de atribuir um erro à aplicação, identifica quem produziu a resposta.

3. Entender o que CONNECT demonstra

O proxy local aceita CONNECT apenas para a origem local exata e só depois de validar a credencial de exercício. A resposta 200 abre um túnel de bytes. Dentro dele, o cliente envia HTTP em texto simples, propositadamente: CONNECT não é, por si, TLS. Uma segunda tentativa abre o túnel e recebe 401 da origem. Portanto, sucesso do túnel não comprova autorização da aplicação. Em produção, distingue TLS até ao proxy, TLS dentro do túnel, inspeção e identidade do destino segundo o desenho real. O laboratório não instala CA, não interceta TLS e não envia credenciais a sistemas externos. As credenciais do proxy não são encaminhadas nos pedidos à origem do ensaio.

4. Rever bypass e limites de domínio

O cliente direto consegue obter /blocked porque o bloqueio do exemplo existe apenas no proxy. Isso não significa que DIRECT evita todos os controlos de uma rede real: um agente, túnel ou firewall pode continuar no caminho. O seletor original normaliza maiúsculas e ponto final e compara o domínio exato ou um sufixo precedido de ponto. Testa funds.example, batch.funds.example, evilfunds.example e funds.example.attacker.invalid. Usar apenas endsWith("funds.example") aceita indevidamente o terceiro. Este seletor não é um interpretador PAC nem pretende reproduzir todos os casos de nomes DNS. Exceções precisam de dono, motivo e teste do âmbito permitido e recusado, incluindo clientes que não usam PAC.

5. Aceitar a saída do serviço

Executa node run.mjs e compara as 30 verificações das duas passagens registadas. São sete pedidos recebidos pela origem e onze operações recebidas pelo proxy, incluindo recusas e túneis; contar ambas as camadas como utilizadores únicos seria incorreto. Uma migração real deve testar mudança de NAT, identidade reconhecida, política aplicada, exceções, cliente não-browser e logs no destino. DNS resolvido e TCP aberto não demonstram aceitação da sessão pelo serviço SSE. Resumo: observa cliente, caminho, autenticação, política e resultado de aplicação separadamente. Conserva critérios de rollback e comunica a RUN se o tráfego está inspecionado, desviado por exceção ou sem evidência suficiente para o afirmar.

import http from 'node:http';
import net from 'node:net';
import assert from 'node:assert/strict';
import {createHash} from 'node:crypto';
import fs from 'node:fs';
const checks=[],originEvents=[],proxyEvents=[],sockets=new Set();
const check=(name,value)=>{assert(value,name);checks.push(name);};
const credential='Basic '+Buffer.from('synthetic:exercise-only').toString('base64');
const origin=http.createServer((req,res)=>{
 originEvents.push({path:req.url,proxyCredentialPresent:!!req.headers['proxy-authorization']});
 const status=req.url==='/private'?401:req.url==='/restricted'?403:200;
 const body=JSON.stringify({layer:'origin',path:req.url,status});
 res.writeHead(status,{'Content-Type':'application/json','Content-Length':Buffer.byteLength(body),'Connection':'close',...(status===401?{'WWW-Authenticate':'Basic realm="fictional-origin"'}:{})});res.end(body);
});
const proxy=http.createServer((req,res)=>{
 const send=(status,label,headers={})=>{proxyEvents.push({method:req.method,result:label});res.writeHead(status,{Connection:'close',...headers});res.end(label);};
 if(req.headers['proxy-authorization']!==credential)return send(407,'proxy-auth',{'Proxy-Authenticate':'Basic realm="fictional-proxy"'});
 let target;try{target=new URL(req.url);}catch{return send(400,'absolute-target-required');}
 if(target.protocol!=='http:'||target.hostname!=='127.0.0.1'||Number(target.port)!==origin.address().port||target.username||target.password)return send(403,'target-outside-lab');
 if(target.pathname==='/blocked')return send(403,'proxy-policy');
 proxyEvents.push({method:req.method,result:'forward'});
 // Fixed destination and explicit header allowlist; never forward proxy credentials.
 const upstream=http.request({host:'127.0.0.1',port:origin.address().port,path:target.pathname+target.search,method:'GET',agent:false,headers:{Connection:'close'}},r=>{res.writeHead(r.statusCode,r.headers);r.pipe(res);});
 upstream.on('error',()=>{if(!res.headersSent)res.writeHead(502);res.end();});upstream.end();
});
proxy.on('connect',(req,client,head)=>{
 const reject=(status,label,extra='')=>{proxyEvents.push({method:'CONNECT',result:label});client.end(`HTTP/1.1 ${status}\r\n${extra}Content-Length: 0\r\nConnection: close\r\n\r\n`);};
 if(req.headers['proxy-authorization']!==credential)return reject('407 Proxy Authentication Required','proxy-auth','Proxy-Authenticate: Basic realm="fictional-proxy"\r\n');
 if(req.url!==`127.0.0.1:${origin.address().port}`)return reject('403 Forbidden','target-outside-lab');
 const upstream=net.connect({host:'127.0.0.1',port:origin.address().port});sockets.add(upstream);upstream.on('close',()=>sockets.delete(upstream));
 upstream.on('connect',()=>{proxyEvents.push({method:'CONNECT',result:'tunnel'});client.write('HTTP/1.1 200 Connection Established\r\n\r\n');if(head.length)upstream.write(head);client.pipe(upstream);upstream.pipe(client);});
 upstream.on('error',()=>client.destroy());client.on('error',()=>upstream.destroy());client.on('close',()=>upstream.destroy());
});
for(const server of[origin,proxy])server.on('connection',s=>{sockets.add(s);s.on('close',()=>sockets.delete(s));});
const listen=s=>new Promise((resolve,reject)=>{s.once('error',reject);s.listen(0,'127.0.0.1',resolve);});
const request=(port,path,authenticated=false)=>new Promise((resolve,reject)=>{
 const r=http.request({host:'127.0.0.1',port,path,method:'GET',agent:false,headers:{Connection:'close',...(authenticated?{'Proxy-Authorization':credential}:{})}},res=>{let body='';res.setEncoding('utf8');res.on('data',d=>body+=d);res.on('end',()=>resolve({status:res.statusCode,headers:res.headers,body}));});r.setTimeout(3000,()=>r.destroy(Error('timeout')));r.on('error',reject);r.end();
});
const connect=(authority,authenticated,path='/health')=>new Promise((resolve,reject)=>{
 const r=http.request({host:'127.0.0.1',port:proxy.address().port,method:'CONNECT',path:authority,agent:false,headers:authenticated?{'Proxy-Authorization':credential}:{}});
 r.on('connect',(res,socket,head)=>{if(res.statusCode!==200){socket.destroy();resolve({status:res.statusCode,headers:res.headers,tunneled:null});return;}
 let text=head.toString('utf8');socket.setTimeout(3000,()=>socket.destroy(Error('tunnel timeout')));socket.on('error',reject);socket.on('data',d=>text+=d.toString('utf8'));socket.on('end',()=>resolve({status:res.statusCode,headers:res.headers,tunneled:text}));
 // Plain HTTP inside CONNECT intentionally demonstrates that CONNECT is not TLS.
 socket.write(`GET ${path} HTTP/1.1\r\nHost: ${authority}\r\nConnection: close\r\n\r\n`);
 });r.setTimeout(3000,()=>r.destroy(Error('connect timeout')));r.on('error',reject);r.end();
});
// Original bounded selector, not a browser PAC interpreter or Cisco implementation.
const select=host=>{const h=host.toLowerCase().replace(/\.$/,'');return h==='funds.example'||h.endsWith('.funds.example')?'DIRECT':'PROXY';};
try{
 await listen(origin);await listen(proxy);const op=origin.address().port,pp=proxy.address().port,url=p=>`http://127.0.0.1:${op}${p}`;
 check('both servers bind only IPv4 loopback',origin.address().address==='127.0.0.1'&&proxy.address().address==='127.0.0.1');
 const direct=await request(op,'/health');check('direct origin request succeeds',direct.status===200);check('direct request has no proxy event',proxyEvents.length===0);
 let n=originEvents.length;const missing=await request(pp,url('/health'));check('proxy requires its own authentication',missing.status===407);check('407 carries proxy challenge',!!missing.headers['proxy-authenticate']);check('proxy authentication failure never reaches origin',originEvents.length===n);
 const success=await request(pp,url('/health'),true);check('authenticated absolute-form request reaches origin',success.status===200&&JSON.parse(success.body).layer==='origin');check('forwarded request records origin event',originEvents.length===n+1);
 const unauthorized=await request(pp,url('/private'),true);check('origin 401 survives proxy forwarding',unauthorized.status===401);check('origin challenge is distinct from proxy challenge',!!unauthorized.headers['www-authenticate']&&!unauthorized.headers['proxy-authenticate']);
 const denied=await request(pp,url('/restricted'),true);check('origin authorization denial is 403',denied.status===403&&JSON.parse(denied.body).layer==='origin');
 n=originEvents.length;const blocked=await request(pp,url('/blocked'),true);check('proxy policy denial is also 403',blocked.status===403&&blocked.body==='proxy-policy');check('proxy policy denial never reaches origin',originEvents.length===n);
 const before=proxyEvents.length;const bypass=await request(op,'/blocked');check('direct client reaches path blocked only by proxy policy',bypass.status===200);check('direct bypass adds no proxy event',proxyEvents.length===before);
 n=originEvents.length;const outside=await request(pp,'http://outside.invalid/health',true);check('off-lab target is rejected without DNS or external connection',outside.status===403&&originEvents.length===n);
 const relative=await request(pp,'/health',true);check('bounded proxy rejects non-absolute target',relative.status===400);
 const ca=await connect(`127.0.0.1:${op}`,false);check('CONNECT also requires proxy authentication',ca.status===407);
 const co=await connect('outside.invalid:443',true);check('CONNECT rejects targets outside its exact loopback allowlist',co.status===403);
 const tunnel=await connect(`127.0.0.1:${op}`,true);check('authorized CONNECT establishes tunnel',tunnel.status===200);check('tunnel carries actual HTTP bytes to origin',tunnel.tunneled.includes('HTTP/1.1 200 OK')&&tunnel.tunneled.includes('"layer":"origin"'));
 const tunneledDenial=await connect(`127.0.0.1:${op}`,true,'/private');check('successful CONNECT can be followed by origin rejection',tunneledDenial.status===200&&tunneledDenial.tunneled.includes('HTTP/1.1 401 Unauthorized'));
 check('proxy credentials are absent from all origin requests',originEvents.every(e=>!e.proxyCredentialPresent));
 check('exact approved domain selects DIRECT',select('funds.example')==='DIRECT');check('subdomain selects DIRECT',select('batch.funds.example')==='DIRECT');check('case and terminal dot normalized in this selector',select('BATCH.FUNDS.EXAMPLE.')==='DIRECT');check('lookalike prefix domain stays proxied',select('evilfunds.example')==='PROXY');check('suffix extension stays proxied',select('funds.example.attacker.invalid')==='PROXY');check('plain suffix matching would overmatch lookalike','evilfunds.example'.endsWith('funds.example'));
 check('actual CONNECT differs from a TLS handshake',tunnel.tunneled.startsWith('HTTP/1.1'));
 const report={passed:checks.length,failed:0,checks,node:process.version,scriptSHA256:createHash('sha256').update(fs.readFileSync(new URL(import.meta.url))).digest('hex'),actualHTTPForwarding:true,actualCONNECTTunnel:true,loopbackOnly:true,actualTLS:false,actualDNS:false,actualBrowserPAC:false,actualCisco:false,actualAWS:false,systemProxyChanged:false,externalConnections:false,observations:{originRequests:originEvents.length,proxyRequests:proxyEvents.length,proxyAuthStatus:407,originAuthStatus:401,proxyPolicyStatus:403,originPolicyStatus:403,directBypassStatus:200,connectStatus:200,connectThenOriginStatus:401,proxyCredentialsForwarded:false},scope:'Original bounded HTTP/CONNECT exercise. No production proxy, TLS inspection, browser PAC engine, DNS, cloud control or Cisco product acceptance. Synthetic credential is instructional only.'};
 process.stdout.write(JSON.stringify(report,null,2)+'\n');
}finally{for(const s of sockets)s.destroy();for(const s of[origin,proxy])if(s.listening)await new Promise(r=>s.close(r));}
NA PRÁTICA

Um 403 do proxy deixa zero pedidos adicionais na origem; um 403 da aplicação acrescenta um pedido. Um CONNECT 200 pode anteceder um 401 da aplicação.

Armadilhas comuns

PAC do browser como cobertura universal; 403 sem identificar a camada; CONNECT como TLS; wildcard amplo como domínio exato; DIRECT como ausência de qualquer controlo.

Tópicos relacionados: Secure Internet Access · HTTP e CONNECT · Bypass e observabilidade

Leva esta ideia contigo

Prova o caminho efetivo e localiza a resposta antes de alterar a política ou declarar inspeção e autorização concluídas.

Criar conta

Referência: Manage PAC Files · 350-701 SCOR v2.0, effective 2026-08-27; core component of CCNP Security

CCNP® e Cisco® são marcas registadas da Cisco Systems, Inc. e/ou das suas afiliadas. A dr.pt é uma plataforma de preparação independente e não está afiliada, associada, patrocinada, autorizada nem aprovada por Cisco. Os conteúdos e as perguntas são originais, não são perguntas oficiais de exame, e concluir os nossos testes não atribui nem garante qualquer certificação. Os nomes são usados apenas para identificar o tema. Todas as outras marcas pertencem aos respetivos titulares.