network_gateway.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. """A network namespace with only loopback + TUN. No physical/default internet NIC.
  2. All TCP sockets belong to the authenticated parent broker. Guest processes never
  3. receive proxy credentials. Fake DNS maps back to hostnames at the SOCKS boundary;
  4. the parent resolves those through the upstream proxy and checks public addresses.
  5. """
  6. import asyncio
  7. import base64
  8. import contextlib
  9. import ipaddress
  10. import json
  11. import os
  12. from pathlib import Path
  13. import secrets
  14. import socket
  15. import struct
  16. import subprocess
  17. import sys
  18. import time
  19. import uuid
  20. SOCKS = 3129
  21. DNS = '198.18.0.1'
  22. class Tunnel:
  23. def __init__(self, send):
  24. self.send = send
  25. self.streams = {}
  26. self.names = {}
  27. self.addresses = {}
  28. def dns(self, packet):
  29. if len(packet) < 12 or packet[2] & 0x80 or struct.unpack('!H', packet[4:6])[0] != 1:
  30. raise ValueError('invalid DNS')
  31. end, labels = 12, []
  32. while end < len(packet) and packet[end]:
  33. length = packet[end]
  34. if length > 63 or end + length + 1 > len(packet): raise ValueError('invalid label')
  35. labels.append(packet[end+1:end+1+length].decode('ascii'))
  36. end += length + 1
  37. end += 1
  38. kind, cls = struct.unpack('!HH', packet[end:end+4])
  39. question = packet[12:end+4]
  40. name = '.'.join(labels).lower()
  41. answer = b''
  42. if cls == 1 and kind == 1 and name and len(name) <= 253:
  43. if name not in self.names:
  44. if len(self.names) >= 65000: raise ValueError('DNS limit')
  45. address = str(ipaddress.IPv4Address(int(ipaddress.IPv4Address('198.19.0.1')) + len(self.names)))
  46. self.names[name] = address
  47. self.addresses[address] = name
  48. answer = b'\xc0\x0c' + struct.pack('!HHIH', 1, 1, 60, 4) + socket.inet_aton(self.names[name])
  49. return packet[:2] + struct.pack('!HHHHH', 0x8180, 1, bool(answer), 0, 0) + question + answer
  50. async def socks(self, reader, writer):
  51. identity = str(uuid.uuid4())
  52. opened = asyncio.get_running_loop().create_future()
  53. if len(self.streams) >= 32:
  54. writer.close(); return
  55. self.streams[identity] = (writer, opened)
  56. try:
  57. version, count = await asyncio.wait_for(reader.readexactly(2), 10)
  58. methods = await reader.readexactly(count)
  59. if version != 5 or 0 not in methods: raise ValueError('SOCKS version')
  60. writer.write(b'\x05\x00'); await writer.drain()
  61. version, command, reserved, kind = await reader.readexactly(4)
  62. if version != 5 or command != 1 or reserved: raise ValueError('TCP CONNECT only')
  63. if kind == 1: host = socket.inet_ntop(socket.AF_INET, await reader.readexactly(4))
  64. elif kind == 4: host = socket.inet_ntop(socket.AF_INET6, await reader.readexactly(16))
  65. elif kind == 3: host = (await reader.readexactly((await reader.readexactly(1))[0])).decode('ascii')
  66. else: raise ValueError('SOCKS address')
  67. port = struct.unpack('!H', await reader.readexactly(2))[0]
  68. await self.send({'op':'open','id':identity,'host':self.addresses.get(host,host),'port':port})
  69. await asyncio.wait_for(opened, 25)
  70. writer.write(b'\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00'); await writer.drain()
  71. while True:
  72. data = await reader.read(32768)
  73. if not data:
  74. await self.send({'op':'end','id':identity}); break
  75. await self.send({'op':'data','id':identity,'data':base64.b64encode(data).decode()})
  76. except Exception:
  77. with contextlib.suppress(Exception): writer.write(b'\x05\x01\x00\x01'+b'\x00'*6); await writer.drain()
  78. finally:
  79. self.streams.pop(identity,None); writer.close()
  80. with contextlib.suppress(Exception): await self.send({'op':'close','id':identity})
  81. async def receive(self, frame):
  82. writer, opened = self.streams.get(frame.get('id'), (None,None))
  83. if writer is None: return
  84. if frame['op'] == 'opened':
  85. if not opened.done(): opened.set_result(True)
  86. elif frame['op'] == 'data':
  87. data = base64.b64decode(frame['data'],validate=True)
  88. if len(data) > 65536: raise ValueError('frame limit')
  89. writer.write(data); await asyncio.wait_for(writer.drain(),10)
  90. elif frame['op'] in ('error','end','close'):
  91. if not opened.done(): opened.set_exception(ConnectionError('network unavailable'))
  92. writer.close()
  93. def close(self):
  94. for writer, opened in list(self.streams.values()):
  95. if not opened.done(): opened.cancel()
  96. writer.close()
  97. self.streams.clear()
  98. class DNSProtocol(asyncio.DatagramProtocol):
  99. def __init__(self, tunnel): self.tunnel=tunnel
  100. def connection_made(self, transport): self.transport=transport
  101. def datagram_received(self, data, addr):
  102. with contextlib.suppress(Exception): self.transport.sendto(self.tunnel.dns(data),addr)
  103. async def main():
  104. tun_process = None
  105. # This container has no workspace mounts or business processes.
  106. if '--init' in sys.argv:
  107. subprocess.run(['ip','tuntap','add','mode','tun','dev','tun0'],check=True)
  108. subprocess.run(['ip','addr','add',DNS+'/15','dev','tun0'],check=True)
  109. subprocess.run(['ip','link','set','tun0','up'],check=True)
  110. subprocess.run(['ip','route','add','default','dev','tun0'],check=True)
  111. argv=['tun2socks','--device','tun0','--proxy','socks5://127.0.0.1:'+str(SOCKS),'--loglevel','error']
  112. if '--socket' not in sys.argv: os.execvp('tun2socks',argv)
  113. tun_process=subprocess.Popen(argv,stdout=subprocess.DEVNULL,stderr=subprocess.DEVNULL)
  114. loop=asyncio.get_running_loop()
  115. queue=asyncio.Queue(maxsize=128)
  116. async def send(frame): await asyncio.wait_for(queue.put(frame),10)
  117. tunnel=Tunnel(send)
  118. tcp=await asyncio.start_server(tunnel.socks,'127.0.0.1',SOCKS)
  119. dns,_=await loop.create_datagram_endpoint(lambda:DNSProtocol(tunnel),local_addr=(DNS,53))
  120. async def dns_tcp(reader,writer):
  121. try:
  122. while True:
  123. length=struct.unpack('!H',await reader.readexactly(2))[0]
  124. answer=tunnel.dns(await reader.readexactly(length))
  125. writer.write(struct.pack('!H',len(answer))+answer);await writer.drain()
  126. except Exception: pass
  127. finally: writer.close()
  128. dns_server=await asyncio.start_server(dns_tcp,DNS,53)
  129. try:
  130. if '--socket' not in sys.argv:
  131. transport,protocol=await loop.connect_write_pipe(asyncio.streams.FlowControlMixin,sys.stdout.buffer)
  132. wire=asyncio.StreamWriter(transport,protocol,None,loop)
  133. incoming=asyncio.StreamReader(limit=200000);await loop.connect_read_pipe(lambda:asyncio.StreamReaderProtocol(incoming),sys.stdin.buffer)
  134. async def output():
  135. while True: wire.write((json.dumps(await queue.get())+'\n').encode());await wire.drain()
  136. task=asyncio.create_task(output());await send({'op':'ready','port':SOCKS})
  137. try:
  138. while True:
  139. line=await incoming.readline()
  140. if not line: break
  141. await tunnel.receive(json.loads(line))
  142. finally: task.cancel();wire.close()
  143. else:
  144. path=Path('/run/yaoyao-network/gateway.sock');path.parent.mkdir(parents=True,exist_ok=True)
  145. path.unlink(missing_ok=True)
  146. token=None;last_seen=0
  147. async def http(reader,writer):
  148. nonlocal token,last_seen
  149. try:
  150. header=await asyncio.wait_for(reader.readuntil(b'\r\n\r\n'),10)
  151. lines=header.decode('ascii').split('\r\n');op=lines[0].split(' ')[1].removeprefix('/network-')
  152. length=int(next(line.split(':',1)[1] for line in lines[1:] if line.lower().startswith('content-length:')))
  153. if not 0<=length<=4*1024*1024: raise ValueError('limit')
  154. body=json.loads(await reader.readexactly(length))
  155. if body.get('desktopId')!=os.environ['YAOYAO_COMPOSE_DESKTOP_ID']: raise ValueError('identity')
  156. if op=='health': result={'ready':Path('/sys/class/net/tun0').exists() and (tun_process is None or tun_process.poll() is None),'version':1}
  157. elif op=='open':
  158. if token and time.monotonic()-last_seen<30: raise ValueError('busy')
  159. tunnel.close()
  160. while not queue.empty(): queue.get_nowait()
  161. token=secrets.token_hex(32);result={'token':token,'ready':True}
  162. else:
  163. if not token or not secrets.compare_digest(str(body.get('token','')),token): raise ValueError('token')
  164. if op=='close': tunnel.close();token=None;result={'ok':True}
  165. elif op=='exchange':
  166. for frame in body.get('frames',[]): await tunnel.receive(frame)
  167. frames=[]
  168. if queue.empty():
  169. with contextlib.suppress(asyncio.TimeoutError): frames.append(await asyncio.wait_for(queue.get(),1))
  170. while not queue.empty() and len(frames)<32: frames.append(queue.get_nowait())
  171. result={'frames':frames}
  172. else: raise ValueError('operation')
  173. last_seen=time.monotonic()
  174. data=json.dumps(result).encode();status='200 OK'
  175. except Exception: data=b'{"error":"network gateway unavailable"}';status='409 Conflict'
  176. writer.write(('HTTP/1.1 '+status+'\r\nContent-Type: application/json\r\nx-yaoyao-desktop-id: '+os.environ['YAOYAO_COMPOSE_DESKTOP_ID']+'\r\nContent-Length: '+str(len(data))+'\r\nConnection: close\r\n\r\n').encode()+data)
  177. with contextlib.suppress(Exception): await writer.drain()
  178. writer.close()
  179. async def watchdog():
  180. nonlocal token
  181. while True:
  182. await asyncio.sleep(5)
  183. if tun_process is not None and tun_process.poll() is not None: raise RuntimeError('TUN process stopped')
  184. if token and time.monotonic()-last_seen>30: tunnel.close();token=None
  185. server=await asyncio.start_unix_server(http,str(path));os.chmod(path,0o666)
  186. async with server: await asyncio.gather(server.serve_forever(),watchdog())
  187. finally: tcp.close();dns.close();dns_server.close();tunnel.close()
  188. if __name__=='__main__': asyncio.run(main())