publicNetwork.test.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
  1. // @vitest-environment node
  2. import {expect,it,vi} from 'vitest'
  3. import {createServer} from 'node:net'
  4. import {randomUUID} from 'node:crypto'
  5. import {publicAddress,publicDestination,normalizePublicHost} from '../../src/runner/network/publicDestination'
  6. import {PublicSocketBroker,type NetworkFrame} from '../../src/runner/network/socketBroker'
  7. it.each(['127.0.0.1','127.20.30.40','0.0.0.0','10.1.2.3','100.64.0.1','169.254.169.254','172.31.2.3','192.168.1.1','192.0.0.9','192.0.2.1','198.19.1.1','198.51.100.1','203.0.113.1','224.0.0.1','255.255.255.255','::1','::','::ffff:127.0.0.1','fe80::1','fd00::1','64:ff9b::a00:1','2001:db8::1','2002:7f00:1::1','3fff::1'])('blocks non-public destination %s',address=>expect(publicAddress(address)).toBe(false))
  8. it.each(['1.1.1.1','8.8.8.8','93.184.216.34','2606:4700:4700::1111'])('recognizes ordinary public unicast %s',address=>expect(publicAddress(address)).toBe(true))
  9. it.each(['127.1','2130706433','0x7f000001'])('normalizes alternate IP notation before applying the policy (%s)',async host=>{
  10. expect(normalizePublicHost(host)).toBe('127.0.0.1');await expect(publicDestination(host,443)).rejects.toThrow()
  11. })
  12. it('rejects mixed DNS, host addresses and unsafe authority syntax',async()=>{
  13. await expect(publicDestination('fixture.example',443,async()=>[{address:'1.1.1.1',family:4},{address:'127.0.0.1',family:4}],{})).rejects.toThrow()
  14. await expect(publicDestination('fixture.example',443,async()=>[{address:'1.1.1.1',family:4}],{eth0:[{address:'1.1.1.1',family:'IPv4'} as any]})).rejects.toThrow()
  15. for(const host of ['user:secret@example.com','example.com/path','example.com:8443','host%2elocal','[::1%eth0]'])expect(()=>normalizePublicHost(host)).toThrow()
  16. await expect(publicDestination('1.1.1.1',22)).rejects.toThrow()
  17. })
  18. it('pins the resolved address and revalidates each new connection',async()=>{
  19. let calls=0
  20. const lookup=async()=>[{address:calls++===0?'1.1.1.1':'127.0.0.1',family:4}]
  21. expect(await publicDestination('fixture.example',443,lookup,{})).toMatchObject({address:'1.1.1.1',port:443})
  22. await expect(publicDestination('fixture.example',443,lookup,{})).rejects.toThrow()
  23. })
  24. it('does not dial after authority changes while DNS is pending',async()=>{
  25. let release!:(value:any)=>void,allowed=true
  26. const frames:NetworkFrame[]=[],dial=vi.fn()
  27. const broker=new PublicSocketBroker(async frame=>{frames.push(frame)},()=>{if(!allowed)throw new Error('revoked')},()=>new Promise(resolve=>{release=resolve}),dial)
  28. const pending=broker.receive({op:'open',id:randomUUID(),host:'example.com',port:443})
  29. await vi.waitFor(()=>expect(release).toBeDefined());allowed=false;release({address:'1.1.1.1',family:4,port:443,host:'example.com'})
  30. await pending;expect(dial).not.toHaveBeenCalled();expect(frames.at(-1)?.op).toBe('error');broker.close()
  31. })
  32. it('bounds pending connections before asynchronous resolution',async()=>{
  33. let release!:(value:any)=>void
  34. const wait=new Promise<any>(resolve=>{release=resolve}),frames:NetworkFrame[]=[],dial=vi.fn()
  35. const broker=new PublicSocketBroker(async frame=>{frames.push(frame)},()=>{},()=>wait,dial,{connections:1,bytes:1024})
  36. const first=broker.receive({op:'open',id:randomUUID(),host:'example.com',port:443})
  37. await broker.receive({op:'open',id:randomUUID(),host:'example.com',port:443})
  38. expect(broker.activeConnections).toBe(1);expect(frames.at(-1)?.op).toBe('error')
  39. broker.close();release({address:'1.1.1.1',family:4,port:443,host:'example.com'});await first;expect(dial).not.toHaveBeenCalled()
  40. })
  41. it('transfers bytes through a real socket and rejects further writes after revocation',async()=>{
  42. const server=createServer(socket=>socket.on('data',bytes=>socket.write(bytes)))
  43. await new Promise<void>(resolve=>server.listen(0,'127.0.0.1',resolve))
  44. const frames:NetworkFrame[]=[],id=randomUUID();let active=true
  45. // The loopback fixture is injected only here; production resolution is tested above.
  46. const broker=new PublicSocketBroker(async frame=>{frames.push(frame)},()=>{if(!active)throw new Error('revoked')},async()=>({address:'127.0.0.1',family:4,port:(server.address() as {port:number}).port,host:'fixture'}))
  47. try{
  48. await broker.receive({op:'open',id,host:'fixture.example',port:443});await vi.waitFor(()=>expect(frames.some(frame=>frame.op==='opened')).toBe(true))
  49. await broker.receive({op:'data',id,data:Buffer.from('wire-proof').toString('base64')})
  50. await vi.waitFor(()=>expect(frames.some(frame=>frame.op==='data'&&Buffer.from(String(frame.data),'base64').toString()==='wire-proof')).toBe(true))
  51. active=false;await expect(broker.receive({op:'data',id,data:'eA=='})).rejects.toThrow('revoked')
  52. }finally{broker.close();await new Promise<void>(resolve=>server.close(()=>resolve()))}
  53. })