natter.py 49 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442
  1. #!/usr/bin/env python3
  2. '''
  3. Natter - https://github.com/MikeWang000000/Natter
  4. Copyright (C) 2023 MikeWang000000
  5. This program is free software: you can redistribute it and/or modify
  6. it under the terms of the GNU General Public License as published by
  7. the Free Software Foundation, either version 3 of the License, or
  8. (at your option) any later version.
  9. This program is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. GNU General Public License for more details.
  13. You should have received a copy of the GNU General Public License
  14. along with this program. If not, see <http://www.gnu.org/licenses/>.
  15. '''
  16. import os
  17. import re
  18. import sys
  19. import json
  20. import time
  21. import errno
  22. import shlex
  23. import atexit
  24. import codecs
  25. import random
  26. import signal
  27. import socket
  28. import struct
  29. import argparse
  30. import threading
  31. import subprocess
  32. __version__ = "2.0.0-rc1"
  33. class Logger(object):
  34. DEBUG = 0
  35. INFO = 1
  36. WARN = 2
  37. ERROR = 3
  38. rep = {DEBUG: "D", INFO: "I", WARN: "W", ERROR: "E"}
  39. level = INFO
  40. if "256color" in os.environ.get("TERM", ""):
  41. GREY = "\033[90;20m"
  42. YELLOW_BOLD = "\033[33;1m"
  43. RED_BOLD = "\033[31;1m"
  44. RESET = "\033[0m"
  45. else:
  46. GREY = YELLOW_BOLD = RED_BOLD = RESET = ""
  47. @staticmethod
  48. def set_level(level):
  49. Logger.level = level
  50. @staticmethod
  51. def debug(text=""):
  52. if Logger.level <= Logger.DEBUG:
  53. sys.stderr.write((Logger.GREY + "%s [%s] %s\n" + Logger.RESET) % (
  54. time.strftime("%Y-%m-%d %H:%M:%S"), Logger.rep[Logger.DEBUG], text
  55. ))
  56. @staticmethod
  57. def info(text=""):
  58. if Logger.level <= Logger.INFO:
  59. sys.stderr.write(("%s [%s] %s\n") % (
  60. time.strftime("%Y-%m-%d %H:%M:%S"), Logger.rep[Logger.INFO], text
  61. ))
  62. @staticmethod
  63. def warning(text=""):
  64. if Logger.level <= Logger.WARN:
  65. sys.stderr.write((Logger.YELLOW_BOLD + "%s [%s] %s\n" + Logger.RESET) % (
  66. time.strftime("%Y-%m-%d %H:%M:%S"), Logger.rep[Logger.WARN], text
  67. ))
  68. @staticmethod
  69. def error(text=""):
  70. if Logger.level <= Logger.ERROR:
  71. sys.stderr.write((Logger.RED_BOLD + "%s [%s] %s\n" + Logger.RESET) % (
  72. time.strftime("%Y-%m-%d %H:%M:%S"), Logger.rep[Logger.ERROR], text
  73. ))
  74. class NatterExit(object):
  75. atexit.register(lambda : NatterExit._atexit[0]())
  76. _atexit = [lambda : None]
  77. @staticmethod
  78. def set_atexit(func):
  79. NatterExit._atexit[0] = func
  80. class PortTest(object):
  81. def test_lan(self, addr, info=False):
  82. print_status = Logger.info if info else Logger.debug
  83. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  84. sock.settimeout(1)
  85. try:
  86. if sock.connect_ex(addr) == 0:
  87. print_status("LAN > %-21s [ OPEN ]" % addr_to_str(addr))
  88. return 1
  89. else:
  90. print_status("LAN > %-21s [ CLOSED ]" % addr_to_str(addr))
  91. return -1
  92. except (OSError, socket.error) as ex:
  93. print_status("LAN > %-21s [ UNKNOWN ]" % addr_to_str(addr))
  94. Logger.debug("Cannot test port %s from LAN because: %s" % (addr_to_str(addr), ex))
  95. return 0
  96. finally:
  97. sock.close()
  98. def test_wan(self, addr, source_ip = None, info=False):
  99. # only port number in addr is used, WAN IP will be ignored
  100. print_status = Logger.info if info else Logger.debug
  101. ret01 = self._test_ifconfigco(addr[1], source_ip)
  102. if ret01 == 1:
  103. print_status("WAN > %-21s [ OPEN ]" % addr_to_str(addr))
  104. return 1
  105. ret02 = self._test_transmission(addr[1], source_ip)
  106. if ret02 == 1:
  107. print_status("WAN > %-21s [ OPEN ]" % addr_to_str(addr))
  108. return 1
  109. if ret01 == ret02 == -1:
  110. print_status("WAN > %-21s [ CLOSED ]" % addr_to_str(addr))
  111. return -1
  112. print_status("WAN > %-21s [ UNKNOWN ]" % addr_to_str(addr))
  113. return 0
  114. def _test_ifconfigco(self, port, source_ip = None):
  115. # repo: https://github.com/mpolden/echoip
  116. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  117. sock.settimeout(8)
  118. try:
  119. if source_ip:
  120. sock.bind((source_ip, 0))
  121. sock.connect(("ifconfig.co", 80))
  122. sock.sendall((
  123. "GET /port/%d HTTP/1.0\r\n"
  124. "Host: ifconfig.co\r\n"
  125. "User-Agent: curl/8.0.0 (Natter)\r\n"
  126. "Accept: */*\r\n"
  127. "Connection: close\r\n"
  128. "\r\n" % port
  129. ).encode())
  130. response = b""
  131. while True:
  132. buff = sock.recv(4096)
  133. if not buff:
  134. break
  135. response += buff
  136. Logger.debug("port-test: ifconfig.co: %s" % response)
  137. _, content = response.split(b"\r\n\r\n", 1)
  138. dat = json.loads(content)
  139. return 1 if dat["reachable"] else -1
  140. except (OSError, LookupError, ValueError, TypeError, socket.error) as ex:
  141. Logger.debug("Cannot test port %d from ifconfig.co because: %s" % (port, ex))
  142. return 0
  143. finally:
  144. sock.close()
  145. def _test_transmission(self, port, source_ip = None):
  146. # repo: https://github.com/transmission/portcheck
  147. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
  148. try:
  149. sock.settimeout(8)
  150. if source_ip:
  151. sock.bind((source_ip, 0))
  152. sock.connect(("portcheck.transmissionbt.com", 80))
  153. sock.sendall((
  154. "GET /%d HTTP/1.0\r\n"
  155. "Host: portcheck.transmissionbt.com\r\n"
  156. "User-Agent: curl/8.0.0 (Natter)\r\n"
  157. "Accept: */*\r\n"
  158. "Connection: close\r\n"
  159. "\r\n" % port
  160. ).encode())
  161. response = b""
  162. while True:
  163. buff = sock.recv(4096)
  164. if not buff:
  165. break
  166. response += buff
  167. Logger.debug("port-test: portcheck.transmissionbt.com: %s" % response)
  168. _, content = response.split(b"\r\n\r\n", 1)
  169. if content.strip() == b"1":
  170. return 1
  171. elif content.strip() == b"0":
  172. return -1
  173. raise ValueError("Unexpected response: %s" % response)
  174. except (OSError, LookupError, ValueError, TypeError, socket.error) as ex:
  175. Logger.debug(
  176. "Cannot test port %d from portcheck.transmissionbt.com "
  177. "because: %s" % (port, ex)
  178. )
  179. return 0
  180. finally:
  181. sock.close()
  182. class StunClient(object):
  183. class ServerUnavailable(Exception):
  184. pass
  185. def __init__(self, stun_server_list, source_host="0.0.0.0", source_port=0,
  186. interface=None, udp=False):
  187. if not stun_server_list:
  188. raise ValueError("STUN server list is empty")
  189. self.stun_server_list = stun_server_list
  190. self.source_host = source_host
  191. self.source_port = source_port
  192. self.interface = interface
  193. self.udp = udp
  194. def get_mapping(self):
  195. first = self.stun_server_list[0]
  196. while True:
  197. try:
  198. return self._get_mapping()
  199. except StunClient.ServerUnavailable as ex:
  200. Logger.warning("stun: STUN server %s is unavailable: %s" % (
  201. addr_to_uri(self.stun_server_list[0], udp = self.udp), ex
  202. ))
  203. self.stun_server_list.append(self.stun_server_list.pop(0))
  204. if self.stun_server_list[0] == first:
  205. Logger.error("stun: No STUN server is avaliable right now")
  206. # force sleep for 10 seconds, then try the next loop
  207. time.sleep(10)
  208. def _get_mapping(self):
  209. # ref: https://www.rfc-editor.org/rfc/rfc5389
  210. socket_type = socket.SOCK_DGRAM if self.udp else socket.SOCK_STREAM
  211. stun_host, stun_port = self.stun_server_list[0]
  212. sock = new_socket_reuse(socket.AF_INET, socket_type)
  213. sock.settimeout(3)
  214. if self.interface is not None:
  215. if not hasattr(socket, "SO_BINDTODEVICE"):
  216. raise RuntimeError(
  217. "Binding to an interface is not supported by current version of "
  218. "Python or operating system"
  219. )
  220. sock.setsockopt(
  221. socket.SOL_SOCKET, socket.SO_BINDTODEVICE, self.interface.encode() + b"\0"
  222. )
  223. sock.bind((self.source_host, self.source_port))
  224. try:
  225. sock.connect((stun_host, stun_port))
  226. inner_addr = sock.getsockname()
  227. self.source_host, self.source_port = inner_addr
  228. sock.send(struct.pack(
  229. "!LLLLL", 0x00010000, 0x2112a442, 0x4e415452,
  230. random.getrandbits(32), random.getrandbits(32)
  231. ))
  232. buff = sock.recv(1500)
  233. ip = port = 0
  234. payload = buff[20:]
  235. while payload:
  236. attr_type, attr_len = struct.unpack("!HH", payload[:4])
  237. if attr_type in [1, 32]:
  238. _, _, port, ip = struct.unpack("!BBHL", payload[4:4+attr_len])
  239. if attr_type == 32:
  240. port ^= 0x2112
  241. ip ^= 0x2112a442
  242. break
  243. payload = payload[4 + attr_len:]
  244. else:
  245. raise ValueError("Invalid STUN response")
  246. outer_addr = socket.inet_ntop(socket.AF_INET, struct.pack("!L", ip)), port
  247. Logger.debug("stun: Got address %s from %s, source %s" % (
  248. addr_to_uri(outer_addr, udp=self.udp),
  249. addr_to_uri((stun_host, stun_port), udp=self.udp),
  250. addr_to_uri(inner_addr, udp=self.udp)
  251. ))
  252. return inner_addr, outer_addr
  253. except (OSError, ValueError, struct.error, socket.error) as ex:
  254. raise StunClient.ServerUnavailable(ex)
  255. finally:
  256. sock.close()
  257. class KeepAlive(object):
  258. def __init__(self, host, port, source_host, source_port, udp=False):
  259. self.sock = None
  260. self.host = host
  261. self.port = port
  262. self.source_host = source_host
  263. self.source_port = source_port
  264. self.udp = udp
  265. self.reconn = False
  266. def __del__(self):
  267. if self.sock:
  268. self.sock.close()
  269. def _connect(self):
  270. sock_type = socket.SOCK_DGRAM if self.udp else socket.SOCK_STREAM
  271. sock = new_socket_reuse(socket.AF_INET, sock_type)
  272. sock.bind((self.source_host, self.source_port))
  273. sock.settimeout(3)
  274. sock.connect((self.host, self.port))
  275. Logger.debug("keep-alive: Connected to host %s" % (
  276. addr_to_uri((self.host, self.port), udp=self.udp)
  277. ))
  278. self.sock = sock
  279. if self.reconn and not self.udp:
  280. Logger.info("keep-alive: connection restored")
  281. self.reconn = False
  282. def keep_alive(self):
  283. if self.sock is None:
  284. self._connect()
  285. if self.udp:
  286. self._keep_alive_udp()
  287. else:
  288. self._keep_alive_tcp()
  289. Logger.debug("keep-alive: OK")
  290. def reset(self):
  291. if self.sock is not None:
  292. self.sock.close()
  293. self.sock = None
  294. self.reconn = True
  295. def _keep_alive_tcp(self):
  296. # send a HTTP request
  297. self.sock.sendall((
  298. "GET /keep-alive HTTP/1.1\r\n"
  299. "Host: %s\r\n"
  300. "User-Agent: curl/8.0.0 (Natter)\r\n"
  301. "Accept: */*\r\n"
  302. "Connection: keep-alive\r\n"
  303. "\r\n" % self.host
  304. ).encode())
  305. buff = b""
  306. try:
  307. while True:
  308. buff = self.sock.recv(4096)
  309. if not buff:
  310. raise OSError("Keep-alive server closed connection")
  311. except socket.timeout as ex:
  312. if not buff:
  313. raise ex
  314. return
  315. def _keep_alive_udp(self):
  316. # send a DNS request
  317. self.sock.send(
  318. struct.pack(
  319. "!HHHHHH", random.getrandbits(16), 0x0100, 0x0001, 0x0000, 0x0000, 0x0000
  320. ) + b"\x09keepalive\x06natter\x00" + struct.pack("!HH", 0x0001, 0x0001)
  321. )
  322. buff = b""
  323. try:
  324. while True:
  325. buff = self.sock.recv(1500)
  326. if not buff:
  327. raise OSError("Keep-alive server closed connection")
  328. except socket.timeout as ex:
  329. if not buff:
  330. raise ex
  331. return
  332. class ForwardNone(object):
  333. # Do nothing. Don't forward.
  334. def start_forward(self, ip, port, toip, toport, udp=False):
  335. pass
  336. def stop_forward(self):
  337. pass
  338. class ForwardTestServer(object):
  339. def __init__(self):
  340. self.active = False
  341. self.sock = None
  342. self.sock_type = None
  343. self.buff_size = 8192
  344. self.timeout = 3
  345. # Start a socket server for testing purpose
  346. # target address is ignored
  347. def start_forward(self, ip, port, toip, toport, udp=False):
  348. self.sock_type = socket.SOCK_DGRAM if udp else socket.SOCK_STREAM
  349. self.sock = new_socket_reuse(socket.AF_INET, self.sock_type)
  350. self.sock.bind(('', port))
  351. Logger.debug("fwd-test: Starting test server at %s" % addr_to_uri((ip, port), udp=udp))
  352. if udp:
  353. start_daemon_thread(self._test_server_run_udp)
  354. else:
  355. start_daemon_thread(self._test_server_run_http)
  356. self.active = True
  357. def _test_server_run_http(self):
  358. self.sock.listen(5)
  359. while self.sock.fileno() != -1:
  360. try:
  361. conn, addr = self.sock.accept()
  362. Logger.debug("fwd-test: got client %s" % (addr,))
  363. except (OSError, socket.error):
  364. return
  365. try:
  366. conn.settimeout(self.timeout)
  367. conn.recv(self.buff_size)
  368. content = b"<html><body><h1>It works!</h1><hr/>Natter</body></html>"
  369. conn.sendall(
  370. b"HTTP/1.1 200 OK\r\n"
  371. b"Content-Type: text/html\r\n"
  372. b"Content-Length: %d\r\n"
  373. b"Connection: close\r\n"
  374. b"Server: Natter\r\n"
  375. b"\r\n"
  376. b"%s\r\n" % (len(content), content)
  377. )
  378. conn.shutdown(socket.SHUT_RDWR)
  379. except (OSError, socket.error):
  380. pass
  381. finally:
  382. conn.close()
  383. def _test_server_run_udp(self):
  384. while self.sock.fileno() != -1:
  385. try:
  386. msg, addr = self.sock.recvfrom(self.buff_size)
  387. Logger.debug("fwd-test: got client %s" % (addr,))
  388. self.sock.sendto(b"It works! - Natter\r\n", addr)
  389. except (OSError, socket.error):
  390. return
  391. def stop_forward(self):
  392. Logger.debug("fwd-test: Stopping test server")
  393. self.sock.close()
  394. self.active = False
  395. class ForwardIptables(object):
  396. def __init__(self, snat=False, sudo=False):
  397. self.uuid = self._get_uuid4()
  398. self.active = False
  399. self.min_ver = (1, 4, 1)
  400. self.curr_ver = (0, 0, 0)
  401. self.snat = snat
  402. self.sudo = sudo
  403. if sudo:
  404. self.iptables_cmd = ["sudo", "-n", "iptables"]
  405. else:
  406. self.iptables_cmd = ["iptables"]
  407. if not self._iptables_check():
  408. raise OSError("iptables >= %s not available" % str(self.min_ver))
  409. # wait for iptables lock, since iptables 1.4.20
  410. if self.curr_ver >= (1, 4, 20):
  411. self.iptables_cmd += ["-w"]
  412. self._iptables_init()
  413. self._iptables_clean()
  414. def __del__(self):
  415. if self.active:
  416. self.stop_forward()
  417. def _iptables_check(self):
  418. if os.name != "posix":
  419. return False
  420. if not self.sudo and os.getuid() != 0:
  421. Logger.warning("fwd-iptables: You are not root")
  422. try:
  423. output = subprocess.check_output(
  424. self.iptables_cmd + ["--version"]
  425. ).decode()
  426. except (OSError, subprocess.CalledProcessError) as e:
  427. return False
  428. m = re.search(r"iptables v([0-9]+)\.([0-9]+)\.([0-9]+)", output)
  429. if m:
  430. self.curr_ver = tuple(int(v) for v in m.groups())
  431. Logger.debug("fwd-iptables: Found iptables %s" % str(self.curr_ver))
  432. if self.curr_ver < self.min_ver:
  433. return False
  434. # check nat table
  435. try:
  436. subprocess.check_output(
  437. self.iptables_cmd + ["-t", "nat", "--list-rules"]
  438. )
  439. except (OSError, subprocess.CalledProcessError) as e:
  440. return False
  441. return True
  442. def _iptables_init(self):
  443. try:
  444. subprocess.check_output(
  445. self.iptables_cmd + ["-t", "nat", "--list-rules", "NATTER"],
  446. stderr=subprocess.STDOUT
  447. )
  448. return
  449. except subprocess.CalledProcessError:
  450. pass
  451. Logger.debug("fwd-iptables: Creating Natter chain")
  452. subprocess.check_output(
  453. self.iptables_cmd + ["-t", "nat", "-N", "NATTER"]
  454. )
  455. subprocess.check_output(
  456. self.iptables_cmd + ["-t", "nat", "-I", "PREROUTING", "-j", "NATTER"]
  457. )
  458. subprocess.check_output(
  459. self.iptables_cmd + ["-t", "nat", "-I", "OUTPUT", "-j", "NATTER"]
  460. )
  461. subprocess.check_output(
  462. self.iptables_cmd + ["-t", "nat", "-N", "NATTER_SNAT"]
  463. )
  464. subprocess.check_output(
  465. self.iptables_cmd + ["-t", "nat", "-I", "POSTROUTING", "-j", "NATTER_SNAT"]
  466. )
  467. subprocess.check_output(
  468. self.iptables_cmd + ["-t", "nat", "-I", "INPUT", "-j", "NATTER_SNAT"]
  469. )
  470. def _iptables_clean(self):
  471. Logger.debug("fwd-iptables: Cleaning up Natter rules")
  472. rules = subprocess.check_output(
  473. self.iptables_cmd + ["-t", "nat", "--list-rules", "NATTER"]
  474. ).decode().splitlines()
  475. rules += subprocess.check_output(
  476. self.iptables_cmd + ["-t", "nat", "--list-rules", "NATTER_SNAT"]
  477. ).decode().splitlines()
  478. for rule in rules:
  479. m = re.search(r"NATTER_UUID=([0-9a-f\-]+)", rule)
  480. if not rule.startswith("-A NATTER") or not m:
  481. continue
  482. rule_uuid = m.group(1)
  483. if rule_uuid == self.uuid:
  484. subprocess.check_output(
  485. self.iptables_cmd + ["-t", "nat", "-D"] + shlex.split(rule[2:])
  486. )
  487. def start_forward(self, ip, port, toip, toport, udp=False):
  488. if ip != toip:
  489. self._check_sys_forward_config()
  490. if (ip, port) == (toip, toport):
  491. raise ValueError("Cannot forward to the same address %s" % addr_to_str((ip, port)))
  492. proto = "udp" if udp else "tcp"
  493. Logger.debug("fwd-iptables: Adding rule %s forward to %s" % (
  494. addr_to_uri((ip, port), udp=udp), addr_to_uri((toip, toport), udp=udp)
  495. ))
  496. subprocess.check_output(self.iptables_cmd + [
  497. "-t", "nat",
  498. "-I", "NATTER",
  499. "-p", proto,
  500. "--dst", ip,
  501. "--dport", "%d" % port,
  502. "-j", "DNAT",
  503. "--to-destination", "%s:%d" % (toip, toport),
  504. "-m", "comment", "--comment", "NATTER_UUID=%s" % self.uuid
  505. ])
  506. if self.snat:
  507. subprocess.check_output(self.iptables_cmd + [
  508. "-t", "nat",
  509. "-I", "NATTER_SNAT",
  510. "-p", proto,
  511. "--dst", toip,
  512. "--dport", "%d" % toport,
  513. "-j", "SNAT",
  514. "--to-source", ip,
  515. "-m", "comment", "--comment", "NATTER_UUID=%s" % self.uuid
  516. ])
  517. self.active = True
  518. def stop_forward(self):
  519. self._iptables_clean()
  520. self.active = False
  521. def _check_sys_forward_config(self):
  522. fpath = "/proc/sys/net/ipv4/ip_forward"
  523. if os.path.exists(fpath):
  524. fin = open(fpath, "r")
  525. buff = fin.read()
  526. fin.close()
  527. if buff.strip() != "1":
  528. raise OSError("IP forwarding is not allowed. Please do `sysctl net.ipv4.ip_forward=1`")
  529. else:
  530. Logger.warning("fwd-iptables: '%s' not found" % str(fpath))
  531. def _get_uuid4(self):
  532. fpath = "/proc/sys/kernel/random/uuid"
  533. if os.path.exists(fpath):
  534. fin = open(fpath, "r")
  535. buff = fin.read()
  536. fin.close()
  537. return buff.strip()
  538. else:
  539. return "%08x-%04x-%04x-%04x-%04x%08x" % (
  540. random.getrandbits(32),
  541. random.getrandbits(16),
  542. random.getrandbits(12) | 0x4000,
  543. random.getrandbits(14) | 0x8000,
  544. random.getrandbits(16), random.getrandbits(32)
  545. )
  546. class ForwardSudoIptables(ForwardIptables):
  547. def __init__(self):
  548. super().__init__(sudo=True)
  549. class ForwardIptablesSnat(ForwardIptables):
  550. def __init__(self):
  551. super().__init__(snat=True)
  552. class ForwardSudoIptablesSnat(ForwardIptables):
  553. def __init__(self):
  554. super().__init__(snat=True, sudo=True)
  555. class ForwardNftables(object):
  556. def __init__(self, snat=False, sudo=False):
  557. self.handle = -1
  558. self.handle_snat = -1
  559. self.active = False
  560. self.min_ver = (0, 9, 0)
  561. self.snat = snat
  562. self.sudo = sudo
  563. if sudo:
  564. self.nftables_cmd = ["sudo", "-n", "nft"]
  565. else:
  566. self.nftables_cmd = ["nft"]
  567. if not self._nftables_check():
  568. raise OSError("nftables >= %s not available" % str(self.min_ver))
  569. self._nftables_init()
  570. self._nftables_clean()
  571. def __del__(self):
  572. if self.active:
  573. self.stop_forward()
  574. def _nftables_check(self):
  575. if os.name != "posix":
  576. return False
  577. if not self.sudo and os.getuid() != 0:
  578. Logger.warning("fwd-nftables: You are not root")
  579. try:
  580. output = subprocess.check_output(
  581. self.nftables_cmd + ["--version"]
  582. ).decode()
  583. except (OSError, subprocess.CalledProcessError) as e:
  584. return False
  585. m = re.search(r"nftables v([0-9]+)\.([0-9]+)\.([0-9]+)", output)
  586. if m:
  587. curr_ver = tuple(int(v) for v in m.groups())
  588. Logger.debug("fwd-nftables: Found nftables %s" % str(curr_ver))
  589. if curr_ver < self.min_ver:
  590. return False
  591. # check nat table
  592. try:
  593. subprocess.check_output(
  594. self.nftables_cmd + ["list table ip nat"]
  595. )
  596. except (OSError, subprocess.CalledProcessError) as e:
  597. return False
  598. return True
  599. def _nftables_init(self):
  600. try:
  601. subprocess.check_output(
  602. self.nftables_cmd + ["list chain ip nat NATTER"],
  603. stderr=subprocess.STDOUT
  604. )
  605. return
  606. except subprocess.CalledProcessError:
  607. pass
  608. Logger.debug("fwd-nftables: Creating Natter chain")
  609. subprocess.check_output(
  610. self.nftables_cmd + ["add chain ip nat NATTER"]
  611. )
  612. subprocess.check_output(
  613. self.nftables_cmd + ["insert rule ip nat PREROUTING counter jump NATTER"]
  614. )
  615. subprocess.check_output(
  616. self.nftables_cmd + ["insert rule ip nat OUTPUT counter jump NATTER"]
  617. )
  618. subprocess.check_output(
  619. self.nftables_cmd + ["add chain ip nat NATTER_SNAT"]
  620. )
  621. subprocess.check_output(
  622. self.nftables_cmd + ["insert rule ip nat PREROUTING counter jump NATTER_SNAT"]
  623. )
  624. subprocess.check_output(
  625. self.nftables_cmd + ["insert rule ip nat OUTPUT counter jump NATTER_SNAT"]
  626. )
  627. def _nftables_clean(self):
  628. Logger.debug("fwd-nftables: Cleaning up Natter rules")
  629. if self.handle > 0:
  630. subprocess.check_output(
  631. self.nftables_cmd + ["delete rule ip nat NATTER handle %d" % self.handle]
  632. )
  633. if self.handle_snat > 0:
  634. subprocess.check_output(
  635. self.nftables_cmd + ["delete rule ip nat NATTER_SNAT handle %d" % self.handle_snat]
  636. )
  637. def start_forward(self, ip, port, toip, toport, udp=False):
  638. if ip != toip:
  639. self._check_sys_forward_config()
  640. if (ip, port) == (toip, toport):
  641. raise ValueError("Cannot forward to the same address %s" % addr_to_str((ip, port)))
  642. proto = "udp" if udp else "tcp"
  643. Logger.debug("fwd-nftables: Adding rule %s forward to %s" % (
  644. addr_to_uri((ip, port), udp=udp), addr_to_uri((toip, toport), udp=udp)
  645. ))
  646. output = subprocess.check_output(self.nftables_cmd + [
  647. "--echo", "--handle",
  648. "insert rule ip nat NATTER ip daddr %s %s dport %d counter dnat to %s:%d" % (
  649. ip, proto, port, toip, toport
  650. )
  651. ]).decode()
  652. m = re.search(r"# handle ([0-9]+)$", output, re.MULTILINE)
  653. if not m:
  654. raise ValueError("Unknown nftables handle")
  655. self.handle = int(m.group(1))
  656. if self.snat:
  657. output = subprocess.check_output(self.nftables_cmd + [
  658. "--echo", "--handle",
  659. "insert rule ip nat NATTER_SNAT ip daddr %s %s dport %d counter snat to %s" % (
  660. toip, proto, toport, ip
  661. )
  662. ]).decode()
  663. m = re.search(r"# handle ([0-9]+)$", output, re.MULTILINE)
  664. if not m:
  665. raise ValueError("Unknown nftables handle")
  666. self.handle_snat = int(m.group(1))
  667. self.active = True
  668. def stop_forward(self):
  669. self._nftables_clean()
  670. self.active = False
  671. def _check_sys_forward_config(self):
  672. fpath = "/proc/sys/net/ipv4/ip_forward"
  673. if os.path.exists(fpath):
  674. fin = open(fpath, "r")
  675. buff = fin.read()
  676. fin.close()
  677. if buff.strip() != "1":
  678. raise OSError("IP forwarding is disabled by system. Please do `sysctl net.ipv4.ip_forward=1`")
  679. else:
  680. Logger.warning("fwd-nftables: '%s' not found" % str(fpath))
  681. class ForwardSudoNftables(ForwardNftables):
  682. def __init__(self):
  683. super().__init__(sudo=True)
  684. class ForwardNftablesSnat(ForwardNftables):
  685. def __init__(self):
  686. super().__init__(snat=True)
  687. class ForwardSudoNftablesSnat(ForwardNftables):
  688. def __init__(self):
  689. super().__init__(snat=True, sudo=True)
  690. class ForwardGost(object):
  691. def __init__(self):
  692. self.active = False
  693. self.min_ver = (2, 3)
  694. self.proc = None
  695. self.udp_timeout = 60
  696. if not self._gost_check():
  697. raise OSError("gost >= %s not available" % str(self.min_ver))
  698. def __del__(self):
  699. if self.active:
  700. self.stop_forward()
  701. def _gost_check(self):
  702. try:
  703. output = subprocess.check_output(
  704. ["gost", "-V"], stderr=subprocess.STDOUT
  705. ).decode()
  706. except (OSError, subprocess.CalledProcessError) as e:
  707. return False
  708. m = re.search(r"gost ([0-9]+)\.([0-9]+)", output)
  709. if m:
  710. current_ver = tuple(int(v) for v in m.groups())
  711. Logger.debug("fwd-gost: Found gost %s" % str(current_ver))
  712. return current_ver >= self.min_ver
  713. return False
  714. def start_forward(self, ip, port, toip, toport, udp=False):
  715. if (ip, port) == (toip, toport):
  716. raise ValueError("Cannot forward to the same address %s" % addr_to_str((ip, port)))
  717. proto = "udp" if udp else "tcp"
  718. Logger.debug("fwd-gost: Starting gost %s forward to %s" % (
  719. addr_to_uri((ip, port), udp=udp), addr_to_uri((toip, toport), udp=udp)
  720. ))
  721. gost_arg = "-L=%s://:%d/%s:%d" % (proto, port, toip, toport)
  722. if udp:
  723. gost_arg += "?ttl=%ds" % self.udp_timeout
  724. self.proc = subprocess.Popen(["gost", gost_arg])
  725. time.sleep(1)
  726. if self.proc.poll() is not None:
  727. raise OSError("gost exited too quickly")
  728. self.active = True
  729. def stop_forward(self):
  730. Logger.debug("fwd-gost: Stopping gost")
  731. if self.proc and self.proc.returncode is not None:
  732. return
  733. self.proc.terminate()
  734. self.active = False
  735. class ForwardSocat(object):
  736. def __init__(self):
  737. self.active = False
  738. self.min_ver = (1, 7, 2)
  739. self.proc = None
  740. self.udp_timeout = 60
  741. self.max_children = 128
  742. if not self._socat_check():
  743. raise OSError("socat >= %s not available" % str(self.min_ver))
  744. def __del__(self):
  745. if self.active:
  746. self.stop_forward()
  747. def _socat_check(self):
  748. try:
  749. output = subprocess.check_output(
  750. ["socat", "-V"], stderr=subprocess.STDOUT
  751. ).decode()
  752. except (OSError, subprocess.CalledProcessError) as e:
  753. return False
  754. m = re.search(r"socat version ([0-9]+)\.([0-9]+)\.([0-9]+)", output)
  755. if m:
  756. current_ver = tuple(int(v) for v in m.groups())
  757. Logger.debug("fwd-socat: Found socat %s" % str(current_ver))
  758. return current_ver >= self.min_ver
  759. return False
  760. def start_forward(self, ip, port, toip, toport, udp=False):
  761. if (ip, port) == (toip, toport):
  762. raise ValueError("Cannot forward to the same address %s" % addr_to_str((ip, port)))
  763. proto = "UDP" if udp else "TCP"
  764. Logger.debug("fwd-socat: Starting socat %s forward to %s" % (
  765. addr_to_uri((ip, port), udp=udp), addr_to_uri((toip, toport), udp=udp)
  766. ))
  767. if udp:
  768. socat_cmd = ["socat", "-T%d" % self.udp_timeout]
  769. else:
  770. socat_cmd = ["socat"]
  771. self.proc = subprocess.Popen(socat_cmd + [
  772. "%s4-LISTEN:%d,reuseaddr,fork,max-children=%d" % (proto, port, self.max_children),
  773. "%s4:%s:%d" % (proto, toip, toport)
  774. ])
  775. time.sleep(1)
  776. if self.proc.poll() is not None:
  777. raise OSError("socat exited too quickly")
  778. self.active = True
  779. def stop_forward(self):
  780. Logger.debug("fwd-socat: Stopping socat")
  781. if self.proc and self.proc.returncode is not None:
  782. return
  783. self.proc.terminate()
  784. self.active = False
  785. class ForwardSocket(object):
  786. def __init__(self):
  787. self.active = False
  788. self.sock = None
  789. self.sock_type = None
  790. self.outbound_addr = None
  791. self.buff_size = 8192
  792. self.udp_timeout = 60
  793. self.max_threads = 128
  794. def __del__(self):
  795. if self.active:
  796. self.stop_forward()
  797. def start_forward(self, ip, port, toip, toport, udp=False):
  798. if (ip, port) == (toip, toport):
  799. raise ValueError("Cannot forward to the same address %s" % addr_to_str((ip, port)))
  800. self.sock_type = socket.SOCK_DGRAM if udp else socket.SOCK_STREAM
  801. self.sock = new_socket_reuse(socket.AF_INET, self.sock_type)
  802. self.sock.bind(("", port))
  803. self.outbound_addr = toip, toport
  804. Logger.debug("fwd-socket: Starting socket %s forward to %s" % (
  805. addr_to_uri((ip, port), udp=udp), addr_to_uri((toip, toport), udp=udp)
  806. ))
  807. if udp:
  808. start_daemon_thread(self._socket_udp_recvfrom)
  809. else:
  810. start_daemon_thread(self._socket_tcp_listen)
  811. self.active = True
  812. def _socket_tcp_listen(self):
  813. self.sock.listen(5)
  814. while True:
  815. try:
  816. sock_inbound, _ = self.sock.accept()
  817. except (OSError, socket.error) as ex:
  818. if not closed_socket_ex(ex):
  819. Logger.error("fwd-socket: socket listening thread is exiting: %s" % ex)
  820. return
  821. sock_outbound = socket.socket(socket.AF_INET, self.sock_type)
  822. try:
  823. sock_outbound.settimeout(3)
  824. sock_outbound.connect(self.outbound_addr)
  825. sock_outbound.settimeout(None)
  826. if threading.active_count() >= self.max_threads:
  827. raise OSError("Too many threads")
  828. start_daemon_thread(self._socket_tcp_forward, args=(sock_inbound, sock_outbound))
  829. start_daemon_thread(self._socket_tcp_forward, args=(sock_outbound, sock_inbound))
  830. except (OSError, socket.error) as ex:
  831. Logger.error("fwd-socket: cannot forward port: %s" % ex)
  832. sock_inbound.close()
  833. sock_outbound.close()
  834. continue
  835. def _socket_tcp_forward(self, sock_to_recv, sock_to_send):
  836. try:
  837. while sock_to_recv.fileno() != -1:
  838. buff = sock_to_recv.recv(self.buff_size)
  839. if buff and sock_to_send.fileno() != -1:
  840. sock_to_send.sendall(buff)
  841. else:
  842. sock_to_recv.close()
  843. sock_to_send.close()
  844. return
  845. except (OSError, socket.error) as ex:
  846. if not closed_socket_ex(ex):
  847. Logger.error("fwd-socket: socket forwarding thread is exiting: %s" % ex)
  848. sock_to_recv.close()
  849. sock_to_send.close()
  850. return
  851. def _socket_udp_recvfrom(self):
  852. outbound_socks = {}
  853. while True:
  854. try:
  855. buff, addr = self.sock.recvfrom(self.buff_size)
  856. s = outbound_socks.get(addr)
  857. except (OSError, socket.error) as ex:
  858. if not closed_socket_ex(ex):
  859. Logger.error("fwd-socket: socket recvfrom thread is exiting: %s" % ex)
  860. return
  861. try:
  862. if not s:
  863. s = outbound_socks[addr] = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  864. s.settimeout(self.udp_timeout)
  865. s.connect((self.outbound_addr))
  866. if threading.active_count() >= self.max_threads:
  867. raise OSError("Too many threads")
  868. start_daemon_thread(self._socket_udp_send, args=(self.sock, s, addr))
  869. if buff:
  870. s.send(buff)
  871. else:
  872. s.close()
  873. del outbound_socks[addr]
  874. except (OSError, socket.error):
  875. if addr in outbound_socks:
  876. outbound_socks[addr].close()
  877. del outbound_socks[addr]
  878. continue
  879. def _socket_udp_send(self, server_sock, outbound_sock, client_addr):
  880. try:
  881. while outbound_sock.fileno() != -1:
  882. buff = outbound_sock.recv(self.buff_size)
  883. if buff:
  884. server_sock.sendto(buff, client_addr)
  885. else:
  886. outbound_sock.close()
  887. except (OSError, socket.error) as ex:
  888. if not closed_socket_ex(ex):
  889. Logger.error("fwd-socket: socket send thread is exiting: %s" % ex)
  890. outbound_sock.close()
  891. return
  892. def stop_forward(self):
  893. Logger.debug("fwd-socket: Stopping socket")
  894. self.sock.close()
  895. self.active = False
  896. class NatterExitException(Exception):
  897. pass
  898. class NatterRetryException(Exception):
  899. pass
  900. def new_socket_reuse(family, type):
  901. sock = socket.socket(family, type)
  902. if hasattr(socket, "SO_REUSEADDR"):
  903. sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
  904. if hasattr(socket, "SO_REUSEPORT"):
  905. sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
  906. return sock
  907. def start_daemon_thread(target, args=()):
  908. th = threading.Thread(target=target, args=args)
  909. th.daemon = True
  910. th.start()
  911. def closed_socket_ex(ex):
  912. if not hasattr(ex, "errno"):
  913. return False
  914. if hasattr(errno, "ECONNABORTED") and ex.errno != errno.ECONNABORTED:
  915. return True
  916. if hasattr(errno, "EBADFD") and ex.errno != errno.EBADFD:
  917. return True
  918. return False
  919. def fix_codecs(codec_list = ["utf-8", "idna"]):
  920. missing_codecs = []
  921. for codec_name in codec_list:
  922. try:
  923. codecs.lookup(codec_name)
  924. except LookupError:
  925. missing_codecs.append(codec_name.lower())
  926. def search_codec(name):
  927. if name.lower() in missing_codecs:
  928. return codecs.CodecInfo(codecs.ascii_encode, codecs.ascii_decode, name="ascii")
  929. if missing_codecs:
  930. codecs.register(search_codec)
  931. def check_docker_network():
  932. if not sys.platform.startswith("linux"):
  933. return
  934. if not os.path.exists("/.dockerenv"):
  935. return
  936. if not os.path.isfile("/sys/class/net/eth0/address"):
  937. return
  938. fo = open("/sys/class/net/eth0/address", "r")
  939. macaddr = fo.read().strip()
  940. fo.close()
  941. fqdn = socket.getfqdn()
  942. ipaddr = socket.gethostbyname(fqdn)
  943. docker_macaddr = "02:42:" + ":".join(["%02x" % int(x) for x in ipaddr.split(".")])
  944. if macaddr == docker_macaddr:
  945. raise RuntimeError("Docker's `--net=host` option is required.")
  946. if not os.path.isfile("/proc/sys/kernel/osrelease"):
  947. return
  948. fo = open("/proc/sys/kernel/osrelease", "r")
  949. uname_r = fo.read().strip()
  950. fo.close()
  951. uname_r_sfx = uname_r.rsplit("-").pop()
  952. if uname_r_sfx.lower() in ["linuxkit", "wsl2"] and fqdn.lower() == "docker-desktop":
  953. raise RuntimeError("Network from Docker Desktop is not supported.")
  954. def addr_to_str(addr):
  955. return "%s:%d" % addr
  956. def addr_to_uri(addr, udp=False):
  957. if udp:
  958. return "udp://%s:%d" % addr
  959. else:
  960. return "tcp://%s:%d" % addr
  961. def validate_ip(s, err=True):
  962. try:
  963. socket.inet_aton(s)
  964. return True
  965. except (OSError, socket.error):
  966. if err:
  967. raise ValueError("Invalid IP address: %s" % s)
  968. return False
  969. def validate_port(s, err=True):
  970. if str(s).isdigit() and int(s) in range(65536):
  971. return True
  972. if err:
  973. raise ValueError("Invalid port number: %s" % s)
  974. return False
  975. def validate_addr_str(s, err=True):
  976. l = str(s).split(":", 1)
  977. if len(l) == 1:
  978. return True
  979. return validate_port(l[1], err)
  980. def validate_positive(s, err=True):
  981. if str(s).isdigit() and int(s) > 0:
  982. return True
  983. if err:
  984. raise ValueError("Not a positive integer: %s" % s)
  985. return False
  986. def validate_filepath(s, err=True):
  987. if os.path.isfile(s):
  988. return True
  989. if err:
  990. raise ValueError("File not found: %s" % s)
  991. return False
  992. def ip_normalize(ipaddr):
  993. return socket.inet_ntoa(socket.inet_aton(ipaddr))
  994. def natter_main(show_title = True):
  995. argp = argparse.ArgumentParser(
  996. description="Expose your port behind full-cone NAT to the Internet.", add_help=False
  997. )
  998. group = argp.add_argument_group("options")
  999. group.add_argument(
  1000. "--version", '-V', action="version", version="Natter %s" % __version__,
  1001. help="show the version of Natter and exit"
  1002. )
  1003. group.add_argument(
  1004. "--help", action="help", help="show this help message and exit"
  1005. )
  1006. group.add_argument(
  1007. "-v", action="store_true", help="verbose mode, printing debug messages"
  1008. )
  1009. group.add_argument(
  1010. "-q", action="store_true", help="exit when mapped address is changed"
  1011. )
  1012. group.add_argument(
  1013. "-u", action="store_true", help="UDP mode"
  1014. )
  1015. group.add_argument(
  1016. "-k", type=int, metavar="<interval>", default=15,
  1017. help="seconds between each keep-alive"
  1018. )
  1019. group.add_argument(
  1020. "-s", metavar="<address>", action="append",
  1021. help="hostname or address to STUN server"
  1022. )
  1023. group.add_argument(
  1024. "-h", type=str, metavar="<address>", default=None,
  1025. help="hostname or address to keep-alive server"
  1026. )
  1027. group.add_argument(
  1028. "-e", type=str, metavar="<path>", default=None,
  1029. help="script path for notifying mapped address"
  1030. )
  1031. group = argp.add_argument_group("bind options")
  1032. group.add_argument(
  1033. "-i", type=str, metavar="<interface>", default="0.0.0.0",
  1034. help="network interface name or IP to bind"
  1035. )
  1036. group.add_argument(
  1037. "-b", type=int, metavar="<port>", default=0,
  1038. help="port number to bind"
  1039. )
  1040. group = argp.add_argument_group("forward options")
  1041. group.add_argument(
  1042. "-m", type=str, metavar="<method>", default=None,
  1043. help="forward method, common values are 'iptables', 'nftables', "
  1044. "'socat', 'gost' and 'socket'"
  1045. )
  1046. group.add_argument(
  1047. "-t", type=str, metavar="<address>", default="0.0.0.0",
  1048. help="IP address of forward target"
  1049. )
  1050. group.add_argument(
  1051. "-p", type=int, metavar="<port>", default=0,
  1052. help="port number of forward target"
  1053. )
  1054. group.add_argument(
  1055. "-r", action="store_true", help="keep retrying until the port of forward target is open"
  1056. )
  1057. args = argp.parse_args()
  1058. verbose = args.v
  1059. udp_mode = args.u
  1060. interval = args.k
  1061. stun_list = args.s
  1062. keepalive_srv = args.h
  1063. notify_sh = args.e
  1064. bind_ip = args.i
  1065. bind_interface = None
  1066. bind_port = args.b
  1067. method = args.m
  1068. to_ip = args.t
  1069. to_port = args.p
  1070. keep_retry = args.r
  1071. exit_when_changed = args.q
  1072. sys.tracebacklimit = 0
  1073. if verbose:
  1074. sys.tracebacklimit = None
  1075. Logger.set_level(Logger.DEBUG)
  1076. validate_positive(interval)
  1077. if stun_list:
  1078. for stun_srv in stun_list:
  1079. validate_addr_str(stun_srv)
  1080. validate_addr_str(keepalive_srv)
  1081. if notify_sh:
  1082. validate_filepath(notify_sh)
  1083. if not validate_ip(bind_ip, err=False):
  1084. bind_interface = bind_ip
  1085. bind_ip = "0.0.0.0"
  1086. validate_port(bind_port)
  1087. validate_ip(to_ip)
  1088. validate_port(to_port)
  1089. # Normalize IPv4 in dotted-decimal notation
  1090. # e.g. 10.1 -> 10.0.0.1
  1091. bind_ip = ip_normalize(bind_ip)
  1092. to_ip = ip_normalize(to_ip)
  1093. if not stun_list:
  1094. stun_list = [
  1095. "fwa.lifesizecloud.com",
  1096. "stun.isp.net.au",
  1097. "stun.nextcloud.com",
  1098. "stun.freeswitch.org",
  1099. "stun.voip.blackberry.com",
  1100. "stunserver.stunprotocol.org",
  1101. "stun.sipnet.com",
  1102. "stun.radiojar.com",
  1103. "stun.sonetel.com",
  1104. "stun.voipgate.com"
  1105. ]
  1106. if udp_mode:
  1107. stun_list = ["stun.miwifi.com", "stun.qq.com", "stun.chat.bilibili.com"] + stun_list
  1108. if not keepalive_srv:
  1109. keepalive_srv = "www.baidu.com"
  1110. if udp_mode:
  1111. keepalive_srv = "8.8.8.8"
  1112. stun_srv_list = []
  1113. for item in stun_list:
  1114. l = item.split(":", 2) + ["3478"]
  1115. stun_srv_list.append((l[0], int(l[1])),)
  1116. if udp_mode:
  1117. l = keepalive_srv.split(":", 2) + ["53"]
  1118. keepalive_host, keepalive_port = l[0], int(l[1])
  1119. else:
  1120. l = keepalive_srv.split(":", 2) + ["80"]
  1121. keepalive_host, keepalive_port = l[0], int(l[1])
  1122. # forward method defaults
  1123. if not method:
  1124. if to_ip == "0.0.0.0" and to_port == 0 and \
  1125. bind_ip == "0.0.0.0" and bind_port == 0 and bind_interface is None:
  1126. method = "test"
  1127. elif to_ip == "0.0.0.0" and to_port == 0:
  1128. method = "none"
  1129. else:
  1130. method = "socket"
  1131. if method == "none":
  1132. ForwardImpl = ForwardNone
  1133. elif method == "test":
  1134. ForwardImpl = ForwardTestServer
  1135. elif method == "iptables":
  1136. ForwardImpl = ForwardIptables
  1137. elif method == "sudo-iptables":
  1138. ForwardImpl = ForwardSudoIptables
  1139. elif method == "iptables-snat":
  1140. ForwardImpl = ForwardIptablesSnat
  1141. elif method == "sudo-iptables-snat":
  1142. ForwardImpl = ForwardSudoIptablesSnat
  1143. elif method == "nftables":
  1144. ForwardImpl = ForwardNftables
  1145. elif method == "sudo-nftables":
  1146. ForwardImpl = ForwardSudoNftables
  1147. elif method == "nftables-snat":
  1148. ForwardImpl = ForwardNftablesSnat
  1149. elif method == "sudo-nftables-snat":
  1150. ForwardImpl = ForwardSudoNftablesSnat
  1151. elif method == "socat":
  1152. ForwardImpl = ForwardSocat
  1153. elif method == "gost":
  1154. ForwardImpl = ForwardGost
  1155. elif method == "socket":
  1156. ForwardImpl = ForwardSocket
  1157. else:
  1158. raise ValueError("Unknown method name: %s" % method)
  1159. #
  1160. # Natter
  1161. #
  1162. if show_title:
  1163. Logger.info("Natter v%s" % __version__)
  1164. if len(sys.argv) == 1:
  1165. Logger.info("Tips: Use `--help` to see help messages")
  1166. check_docker_network()
  1167. forwarder = ForwardImpl()
  1168. port_test = PortTest()
  1169. stun = StunClient(stun_srv_list, bind_ip, bind_port, udp=udp_mode, interface=bind_interface)
  1170. natter_addr, outer_addr = stun.get_mapping()
  1171. # set actual ip and port for keep-alive socket to bind, instead of zero
  1172. bind_ip, bind_port = natter_addr
  1173. keep_alive = KeepAlive(keepalive_host, keepalive_port, bind_ip, bind_port, udp=udp_mode)
  1174. keep_alive.keep_alive()
  1175. # get the mapped address again after the keep-alive connection is established
  1176. outer_addr_prev = outer_addr
  1177. natter_addr, outer_addr = stun.get_mapping()
  1178. if outer_addr != outer_addr_prev:
  1179. Logger.warning("Network is unstable, or not full cone")
  1180. # set actual ip of localhost for correct forwarding
  1181. if socket.inet_aton(to_ip) in [socket.inet_aton("127.0.0.1"), socket.inet_aton("0.0.0.0")]:
  1182. to_ip = natter_addr[0]
  1183. # if not specified, the target port is set to be the same as the outer port
  1184. if not to_port:
  1185. to_port = outer_addr[1]
  1186. # some exceptions: ForwardNone and ForwardTestServer are not real forward methods,
  1187. # so let target ip and port equal to natter's
  1188. if ForwardImpl in (ForwardNone, ForwardTestServer):
  1189. to_ip, to_port = natter_addr
  1190. to_addr = (to_ip, to_port)
  1191. forwarder.start_forward(natter_addr[0], natter_addr[1], to_addr[0], to_addr[1], udp=udp_mode)
  1192. NatterExit.set_atexit(forwarder.stop_forward)
  1193. # Display route infomation
  1194. Logger.info()
  1195. route_str = ""
  1196. if ForwardImpl not in (ForwardNone, ForwardTestServer):
  1197. route_str += "%s <--%s--> " % (addr_to_uri(to_addr, udp=udp_mode), method)
  1198. route_str += "%s <--Natter--> %s" % (
  1199. addr_to_uri(natter_addr, udp=udp_mode), addr_to_uri(outer_addr, udp=udp_mode)
  1200. )
  1201. Logger.info(route_str)
  1202. Logger.info()
  1203. # Test mode notice
  1204. if ForwardImpl == ForwardTestServer:
  1205. Logger.info("Test mode in on.")
  1206. Logger.info("Please check [ %s://%s ]" % ("udp" if udp_mode else "http", addr_to_str(outer_addr)))
  1207. Logger.info()
  1208. # Call notification script
  1209. if notify_sh:
  1210. protocol = "udp" if udp_mode else "tcp"
  1211. inner_ip, inner_port = to_addr if method else natter_addr
  1212. outer_ip, outer_port = outer_addr
  1213. Logger.info("Calling script: %s" % notify_sh)
  1214. subprocess.call([
  1215. os.path.abspath(notify_sh), protocol, str(inner_ip), str(inner_port), str(outer_ip), str(outer_port)
  1216. ], shell=False)
  1217. # Display check results, TCP only
  1218. if not udp_mode:
  1219. ret1 = port_test.test_lan(to_addr, info=True)
  1220. ret2 = port_test.test_lan(natter_addr, info=True)
  1221. ret3 = port_test.test_lan(outer_addr, info=True)
  1222. ret4 = port_test.test_wan(outer_addr, source_ip=natter_addr[0], info=True)
  1223. if ret1 == -1:
  1224. Logger.warning("!! Target port is closed !!")
  1225. elif ret1 == 1 and ret3 == ret4 == -1:
  1226. Logger.warning("!! Hole punching failed !!")
  1227. elif ret3 == 1 and ret4 == -1:
  1228. Logger.warning("!! You may be behind a firewall !!")
  1229. Logger.info()
  1230. # retry
  1231. if keep_retry and ret1 == -1:
  1232. Logger.info("Retry after %d seconds..." % interval)
  1233. time.sleep(interval)
  1234. forwarder.stop_forward()
  1235. raise NatterRetryException("Target port is closed")
  1236. #
  1237. # Main loop
  1238. #
  1239. need_recheck = False
  1240. cnt = 0
  1241. while True:
  1242. # force recheck every 20th loop
  1243. cnt = (cnt + 1) % 20
  1244. if cnt == 0:
  1245. need_recheck = True
  1246. if need_recheck:
  1247. Logger.debug("Start recheck")
  1248. need_recheck = False
  1249. # check LAN port first
  1250. if udp_mode or port_test.test_lan(outer_addr) == -1:
  1251. # then check through STUN
  1252. _, outer_addr_curr = stun.get_mapping()
  1253. if outer_addr_curr != outer_addr:
  1254. forwarder.stop_forward()
  1255. # exit or retry
  1256. if exit_when_changed:
  1257. Logger.info("Natter is exiting because mapped address has changed")
  1258. raise NatterExitException("Mapped address has changed")
  1259. raise NatterRetryException("Mapped address has changed")
  1260. # end of recheck
  1261. ts = time.time()
  1262. try:
  1263. keep_alive.keep_alive()
  1264. except (OSError, socket.error) as ex:
  1265. if udp_mode:
  1266. Logger.debug("keep-alive: UDP response not received: %s" % ex)
  1267. else:
  1268. Logger.error("keep-alive: connection broken: %s" % ex)
  1269. keep_alive.reset()
  1270. need_recheck = True
  1271. sleep_sec = interval - (time.time() - ts)
  1272. if sleep_sec > 0:
  1273. time.sleep(sleep_sec)
  1274. def main():
  1275. signal.signal(signal.SIGTERM, lambda s,f:exit(143))
  1276. fix_codecs()
  1277. show_title = True
  1278. while True:
  1279. try:
  1280. natter_main(show_title)
  1281. except NatterRetryException:
  1282. pass
  1283. except (NatterExitException, KeyboardInterrupt):
  1284. exit()
  1285. show_title = False
  1286. if __name__ == "__main__":
  1287. main()