1. Identify which client sends traffic
A portal works in the browser, but the export job on the same machine takes another path. Browser PAC configuration does not establish every process’s behavior. Inventory client, version, source, steering method and device policies. Cisco documentation distinguishes its managed PAC from custom files: portal bypass changes do not automatically modify custom files. Confirm which URL and contents actually loaded, including caches and access prerequisites. Do not change system settings to run this lab: it explicitly selects two local sockets. Its purpose is to observe path differences without claiming browser or Cisco tenant validation. Test each relevant application as a separate consumer.
2. Locate the rejection
In the original experiment, a request lacking the synthetic credential receives 407 with Proxy-Authenticate and never reaches the origin. After proxy authentication, /private returns application-originated 401 with WWW-Authenticate. Two different 403 responses occur: /blocked is denied by the proxy, while /restricted reaches and is denied by the origin. The code alone does not distinguish these cases; correlate layer, headers and request presence in records. These are lab outcomes, not a universal Cisco mapping. Official case 226262 describes a 401 on a diagnostic endpoint associated with NAT source identification. Identify the response producer before assigning an error to the application.
3. Understand what CONNECT establishes
The local proxy accepts CONNECT only for the exact local origin and after checking the exercise credential. Its 200 response opens a byte tunnel. Inside it, the client intentionally sends plaintext HTTP: CONNECT is not itself TLS. A second attempt opens the tunnel and receives origin 401. Tunnel success therefore does not establish application authorization. In production, distinguish TLS to the proxy, TLS inside the tunnel, inspection and destination identity according to the actual design. The lab installs no CA, intercepts no TLS and sends no credentials to external systems. Proxy credentials are not forwarded in the experiment’s origin requests.
4. Review bypass and domain boundaries
The direct client retrieves /blocked because the example’s block exists only at the proxy. This does not mean DIRECT avoids every real-network control: an agent, tunnel or firewall may remain on the path. The original selector normalizes case and a trailing dot and compares the exact domain or a dot-prefixed suffix. Test funds.example, batch.funds.example, evilfunds.example and funds.example.attacker.invalid. Using only endsWith("funds.example") incorrectly accepts the third. This selector is not a PAC interpreter and does not reproduce every DNS-name case. Exceptions need an owner, rationale and allowed/denied-scope tests, including clients that do not use PAC.
5. Accept service egress
Run node run.mjs and compare all 30 checks in the two recorded runs. There are seven requests received by the origin and eleven operations received by the proxy, including rejections and tunnels; counting both layers as unique users would be incorrect. A real migration must test NAT changes, recognized identity, applied policy, exceptions, non-browser clients and destination logs. Successful DNS resolution and open TCP do not establish SSE session acceptance. Summary: observe client, path, authentication, policy and application result separately. Retain rollback criteria and tell RUN whether traffic is inspected, bypassed under an exception or lacks sufficient evidence to claim either.
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));}
A proxy 403 adds zero origin requests; an application 403 adds one. A CONNECT 200 can precede an application 401.
Common pitfalls
Browser PAC as universal coverage; 403 without layer identification; CONNECT as TLS; broad wildcard as an exact domain; DIRECT as absence of every control.
Related topics: Secure Internet Access · HTTP and CONNECT · Bypass and observability
Prove the effective path and locate the response before changing policy or declaring inspection and authorization complete.
Reference: Manage PAC Files · 350-701 SCOR v2.0, effective 2026-08-27; core component of CCNP Security