Jelajahi Sumber

v0.9.0 Updates

MikeWang000000 3 tahun lalu
induk
melakukan
42005887f9
7 mengubah file dengan 725 tambahan dan 206 penghapusan
  1. TEMPAT SAMPAH
      .img/img01.png
  2. TEMPAT SAMPAH
      .img/img01_1.png
  3. TEMPAT SAMPAH
      .img/img01_2.png
  4. 85 9
      README.md
  5. 49 0
      natter-config.template.json
  6. 9 0
      natter-hook.sh
  7. 582 197
      natter.py

TEMPAT SAMPAH
.img/img01.png


TEMPAT SAMPAH
.img/img01_1.png


TEMPAT SAMPAH
.img/img01_2.png


+ 85 - 9
README.md

@@ -4,17 +4,24 @@
 交流群组:<a href="https://jq.qq.com/?_wv=1027&k=EYXohGpC">Q 657590400</a> | <a href="https://t.me/+VS5sjOWGgzsyYjY1">TG</a>
 </p>
 
-**注意:目前的 Natter 是半成品!** Natter 的参数列表和行为将来可能会发生改变。
+**当前版本:** v0.9.0  
+**注意:目前的 Natter 仍处于开发初期阶段。** Natter 的参数列表和行为将来可能会发生改变。
 
 ## 使用例
+首次使用,检查当前网络 NAT 情况:
+```
+python natter.py --check-nat
+```
+![](.img/img01_1.png)
+如果没有告警,那么您的网络一切正常。如有报错,请参考下文“错误解读”部分。
+
 在本地 3456 号 TCP 端口上实行 TCP 打洞,并开启测试用 HTTP 服务:
 ```
 python natter.py -t 3456
 ```
-成功打洞后:
-![](.img/img01.png)
+![](.img/img01_2.png)
 
-使用外部网络访问该公网地址 `http://203.0.113.10:14500/`,可见 `It works!` 字样
+使用外部网络访问该公网地址 `http://203.0.113.10:14500/`,如果看见 `It works!` 字样,则为打洞成功
 ![](.img/img02.png)
 
 打洞测试成功后,可以去掉 `-t` 选项,然后将 3456 端口转发至您想要的目标地址上。
@@ -33,6 +40,68 @@ python natter.py -t 3456
 python natter.py 3456
 ```
 
+## 使用配置文件
+如果您不想手动设置端口转发,可以交由 Natter 处理。同时,使用配置文件,Natter 可以提供更多有用的功能。
+```
+python natter.py -c ./natter-config.json
+```
+配置文件的说明如下:
+```javascript
+// 注意:JSON 配置文件不支持代码注释,此处为说明配置用途。
+{
+    "logging": {
+        "level": "info",                        // 日志等级:可选值:"debug"、"info"、"warning"、"error"
+        "log_file": "./natter.log"              // 将日志输出到指定文件,不需要请留空:""
+    },
+    "status_report": {
+        // 当外部IP/端口发生改变时,会执行下方命令。
+        // 大括号 {...} 为占位符,命令执行时会被实际值替换。
+        // 不需要请留空:""
+        "hook": "bash ./natter-hook.sh '{protocol}' '{inner_ip}' '{inner_port}' '{outer_ip}' '{outer_port}'",
+        "status_file": "./natter-status.json"   // 将实时端口映射状态储存至指定文件,不需要请留空:""
+    },
+    "open_port": {
+        // 此处设置 Natter 打洞IP:端口。(仅打洞)
+        // 此处地址为 Natter 绑定(监听)的地址,Natter 仅对这些地址打洞,您需要手动设置端口转发。
+        // 注意:使用默认出口IP,请使用 0.0.0.0 ,而不是 127.0.0.1 。
+        "tcp": [
+            "0.0.0.0:3456",
+            "0.0.0.0:3457"
+        ],
+        "udp": [
+            "0.0.0.0:3456",
+            "0.0.0.0:3457"
+        ]
+    },
+    "forward_port": {
+        // 此处设置需要 Natter 开放至公网的 IP:端口。(打洞 + 内置转发)
+        // Natter 会全自动打洞、转发,您无需做任何干预。
+        // 注意:使用本机IP,请使用 127.0.0.1,而不是 0.0.0.0 。
+        "tcp": [
+            "127.0.0.1:80",
+            "192.168.1.100:443"
+        ],
+        "udp": [
+            "127.0.0.1:53",
+            "192.168.1.100:51820"
+        ]
+    },
+    "stun_server": {
+        // 此处设置公共 STUN 服务器。
+        // TCP 服务器请确保 TCP/3478 端口开放可用;
+        // UDP 服务器请确保 UDP/3478 端口开放可用。
+        "tcp": [
+            "stun.stunprotocol.org",
+            "stun.voip.blackberry.com"
+        ],
+        "udp": [
+            "stun.miwifi.com",
+            "stun.qq.com"
+        ]
+    },
+    "keep_alive": "www.qq.com"  // 此处设置 HTTP Keep-Alive 服务器。请确保该服务器 80 端口开放,且支持 HTTP Keep-Alive。
+}
+```
 
 ## 原理图
 ![](.img/img03.png)
@@ -58,6 +127,15 @@ python natter.py 3456
     我在外部网络使用 `nmap` 对出口 IP 地址进行 TCP 全端口扫描,发现均为 `filtered` 。
 
 
+## 更新日志
+### v0.9.0
+1. 打洞前不再强制检查 NAT 类型;
+2. 新增 UDP 功能;
+3. 新增内置端口转发功能;
+4. 新增实时状态推送/更新功能;
+5. 新增配置文件支持。
+
+
 ## 错误解读 & 解决方法
 
 ```
@@ -78,8 +156,6 @@ You cannot perform TCP hole punching in a symmetric NAT network.
 您无法在一个对称型 NAT 网络中实行 TCP 打洞。  
 **解决方法:** 此网络无法打洞。检查您的网络拓扑。检查您是否处于多层 NAT 下。
 
-```
-TCP hole punching seems to fail. Maybe you are behind a firewall.
-```
-TCP 打洞似乎失败了。您可能处于防火墙下。  
-**解决方法:** 如果外部网络可以访问 Natter 提供的地址,请忽略此条信息。尝试关闭光猫的防火墙。如果处于多层 NAT 下,请在光猫或路由器下设置 DMZ 主机或者端口转发。如果上述方案不能解决改问题,则是运营商设置了防火墙,此网络无法开放 TCP 端口。
+> Q:Natter 给出了外部地址,但我没有办法访问。
+
+**解决方法:** 检查您是否正确配置了端口转发。检查您的目标服务是否正常启动。尝试关闭光猫的防火墙。如果处于多层 NAT 下,请在光猫或路由器下设置 DMZ 主机或者端口转发。如果上述方案不能解决改问题,则是运营商设置了防火墙,此网络无法开放 TCP 端口。

+ 49 - 0
natter-config.template.json

@@ -0,0 +1,49 @@
+{
+    "logging": {
+        "level": "info",
+        "log_file": "./natter.log"
+    },
+    "status_report": {
+        "hook": "bash ./natter-hook.sh '{protocol}' '{inner_ip}' '{inner_port}' '{outer_ip}' '{outer_port}'",
+        "status_file": "./natter-status.json"
+    },
+    "open_port": {
+        "tcp": [
+            "0.0.0.0:3456",
+            "0.0.0.0:3457"
+        ],
+        "udp": [
+            "0.0.0.0:3456",
+            "0.0.0.0:3457"
+        ]
+    },
+    "forward_port": {
+        "tcp": [
+            "127.0.0.1:22",
+            "192.168.1.100:443"
+        ],
+        "udp": [
+            "127.0.0.1:53",
+            "192.168.1.100:51820"
+        ]
+    },
+    "stun_server": {
+        "tcp": [
+            "fwa.lifesizecloud.com",
+            "stun.isp.net.au",
+            "stun.freeswitch.org",
+            "stun.voip.blackberry.com",
+            "stun.nextcloud.com",
+            "stun.stunprotocol.org",
+            "stun.sipnet.com",
+            "stun.radiojar.com",
+            "stun.sonetel.com",
+            "stun.voipgate.com"
+        ],
+        "udp": [
+            "stun.miwifi.com",
+            "stun.qq.com"
+        ]
+    },
+    "keep_alive": "www.qq.com"
+}

+ 9 - 0
natter-hook.sh

@@ -0,0 +1,9 @@
+protocol=$1
+inner_ip=$2
+inner_port=$3
+outter_ip=$4
+outter_port=$5
+
+echo "[Script] - Upload to server: ${protocol}: ${inner_ip}:${inner_port} -> ${outter_ip}:${outter_port}"
+
+# Write your upload script below...

+ 582 - 197
natter.py

@@ -2,50 +2,81 @@ import threading
 import socket
 import struct
 import codecs
+import json
 import time
 import sys
 import os
 
+__version__ = "0.9.0"
+
 
 # Fix OpenWRT Python codecs issues:
 #   Always fallback to ASCII when specified codec is not available.
 try:
-    codecs.lookup('idna')
-    codecs.lookup('utf-8')
+    codecs.lookup("idna")
+    codecs.lookup("utf-8")
 except LookupError:
     def search_codec(_):
-        return codecs.CodecInfo(codecs.ascii_encode, codecs.ascii_decode, name='ascii')
+        return codecs.CodecInfo(codecs.ascii_encode, codecs.ascii_decode, name="ascii")
     codecs.register(search_codec)
 
 
+def get_free_port(udp=False):
+    if udp:
+        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+    else:
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+    # Not all OS have a SO_REUSEPORT option
+    if "SO_REUSEPORT" in dir(socket):
+        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
+    sock.bind(("", 0))
+    ret = sock.getsockname()[1]
+    sock.close()
+    return ret
+
+
+def test_port_open(dst_addr, timeout = 3):
+    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+    sock.settimeout(timeout)
+    result = sock.connect_ex(dst_addr)
+    sock.close()
+    return result == 0
+
+
 class Logger(object):
     DEBUG = 1
     INFO = 2
     WARNING = 3
     ERROR = 4
 
-    def __init__(self, level = INFO):
+    def __init__(self, level = INFO, files = (sys.stderr,)):
         self.level = level
+        self.files = files
 
     def debug(self, msg):
         if self.level <= Logger.DEBUG:
-            sys.stdout.write("[DEBUG] - " + str(msg) + "\n")
-            sys.stdout.flush()
+            for fo in self.files:
+                fo.write("[DEBUG] - " + str(msg) + "\n")
+                fo.flush()
 
     def info(self, msg):
         if self.level <= Logger.INFO:
-            sys.stdout.write("[INFO] - " + str(msg) + "\n")
-            sys.stdout.flush()
+            for fo in self.files:
+                fo.write("[INFO] - " + str(msg) + "\n")
+                fo.flush()
 
     def warning(self, msg):
         if self.level <= Logger.WARNING:
-            sys.stderr.write("[WARNING] - " + str(msg) + "\n")
-            sys.stderr.flush()
+            for fo in self.files:
+                fo.write("[WARNING] - " + str(msg) + "\n")
+                fo.flush()
 
     def error(self, msg):
         if self.level <= Logger.ERROR:
-            sys.stderr.write("[ERROR] - " + str(msg) + "\n")
-            sys.stderr.flush()
+            for fo in self.files:
+                fo.write("[ERROR] - " + str(msg) + "\n")
+                fo.flush()
 
 
 class StunClient(object):
@@ -73,6 +104,10 @@ class StunClient(object):
         "stun.miwifi.com",
         "stun.qq.com"
     ]
+
+    _stun_ip_tcp = []
+    _stun_ip_udp = []
+
     MTU         = 1500
     STUN_PORT   = 3478
     MAGIC_COOKIE    = 0x2112a442
@@ -92,54 +127,45 @@ class StunClient(object):
     NAT_SYMMETRIC        = 4
     NAT_SYM_UDP_FIREWALL = 5
 
-    def __init__(self, source_ip = "0.0.0.0", log_level = Logger.INFO):
-        self.logger = Logger(log_level)
+    def __init__(self, source_ip = "0.0.0.0", logger = None):
+        self.logger = logger if logger else Logger()
         self.source_ip = source_ip
-        self.stun_ip_tcp = []
-        self.stun_ip_udp = []
         if not self.check_reuse_ability():
             raise OSError("This OS or Python does not support reusing ports!")
-        self.logger.info("Getting STUN server IP...")
-        for hostname in self.stun_server_tcp:
-            self.stun_ip_tcp.extend(self.resolve_hostname(hostname))
-        for hostname in self.stun_server_udp:
-            self.stun_ip_udp.extend(self.resolve_hostname(hostname))
-        if not self.stun_ip_tcp or not self.stun_ip_udp:
+        if not self._stun_ip_tcp or not self._stun_ip_udp:
+            self.logger.info("Getting STUN server IP...")
+            for hostname in self.stun_server_tcp:
+                self._stun_ip_tcp.extend(
+                    ip for ip in self.resolve_hostname(hostname) if ip not in self._stun_ip_tcp
+                )
+            for hostname in self.stun_server_udp:
+                self._stun_ip_udp.extend(
+                    ip for ip in self.resolve_hostname(hostname) if ip not in self._stun_ip_udp
+                )
+        if not self._stun_ip_tcp or not self._stun_ip_udp:
             raise Exception("No public STUN server is avaliable. Please check your Internet connection.")
 
-    def get_free_port(self, udp=False):
-        if udp:
-            sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-        else:
-            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-        sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-        if 'SO_REUSEPORT' in dir(socket):
-            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
-        sock.bind(('', 0))
-        ret = sock.getsockname()[1]
-        sock.close()
-        return ret
-
     def check_reuse_ability(self):
         try:
-            test_port = self.get_free_port()
+            # A simple test: listen on the same port
+            test_port = get_free_port()
             s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
             s1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-            if 'SO_REUSEPORT' in dir(socket):
+            if "SO_REUSEPORT" in dir(socket):
                 s1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
-            s1.bind(("0.0.0.0", test_port))
-            s1.listen(1)
+            s1.bind(("", test_port))
+            s1.listen(5)
             s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
             s2.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-            if 'SO_REUSEPORT' in dir(socket):
+            if "SO_REUSEPORT" in dir(socket):
                 s2.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
-            s2.bind(("0.0.0.0", test_port))
-            s2.listen(1)
+            s2.bind(("", test_port))
+            s2.listen(5)
             s1.close()
             s2.close()
             return True
         except OSError as e:
-            self.logger.debug("%s: %s" % (e.__class__.__name__, e))
+            self.logger.debug("Cannot reuse: %s: %s" % (e.__class__.__name__, e))
             return False
 
     def resolve_hostname(self, hostname):
@@ -148,7 +174,7 @@ class StunClient(object):
             host, alias, ip_addresses = socket.gethostbyname_ex(hostname)
             return ip_addresses
         except Exception as e:
-            self.logger.debug("%s: %s" % (e.__class__.__name__, e))
+            self.logger.debug("Cannot resolve: %s: %s" % (e.__class__.__name__, e))
             return []
 
     def random_tran_id(self, use_magic_cookie = False):
@@ -168,7 +194,7 @@ class StunClient(object):
         payload = data[20:20 + msg_length]
         return msg_type, tran_id, payload
 
-    def get_mapped_addr(self, payload):
+    def extract_mapped_addr(self, payload):
         while payload:
             attrib_type, attrib_length = struct.unpack("!HH", payload[:4])
             attrib_value = payload[4:4 + attrib_length]
@@ -196,7 +222,7 @@ class StunClient(object):
         try:
             sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
             sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-            if 'SO_REUSEPORT' in dir(socket):
+            if "SO_REUSEPORT" in dir(socket):
                 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
             sock.settimeout(timeout)
             sock.bind((self.source_ip, source_port))
@@ -207,7 +233,7 @@ class StunClient(object):
             msg_type, msg_id, payload = self.unpack_stun_message(buf)
             if tran_id == msg_id and msg_type == self.BIND_RESPONSE:
                 source_addr  = sock.getsockname()
-                mapped_addr = self.get_mapped_addr(payload)
+                mapped_addr = self.extract_mapped_addr(payload)
                 ret = source_addr, mapped_addr
                 self.logger.debug("(TCP) %s says: %s" % (stun_host, mapped_addr))
             else:
@@ -215,25 +241,30 @@ class StunClient(object):
             sock.shutdown(socket.SHUT_RDWR)
             sock.close()
         except Exception as e:
-            self.logger.debug("%s: %s" % (e.__class__.__name__, e))
+            self.logger.debug("Cannot do TCP STUN test: %s: %s" % (e.__class__.__name__, e))
             sock.close()
             ret = None
         return ret
 
-    def udp_test(self, stun_host, source_port, change_ip = False, change_port = False, timeout = 1, repeat = 3):
+    def udp_test(self, stun_host, source_port, change_ip = False, change_port = False, timeout = 1, repeat = 3, custom_sock = None):
         # Note:
-        #     Assuming STUN is being multiplexed with other protocols,
-        #     the packet must be inspected to check if it is a STUN packet.
+        #   Assuming STUN is being multiplexed with other protocols,
+        #   the packet must be inspected to check if it is a STUN packet.
+        #   Parameter source_port has no effect when custom_sock is set
         self.logger.debug("Trying UDP STUN: %s (change ip:%d/port:%d)" % (stun_host, change_ip, change_port))
         time_start = time.time()
         tran_id = self.random_tran_id()
-        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        if custom_sock is None:
+            sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        else:
+            sock = custom_sock
+        origin_timeout = sock.gettimeout()
         try:
-            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-            if 'SO_REUSEPORT' in dir(socket):
-                sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
-            sock.settimeout(timeout)
-            sock.bind((self.source_ip, source_port))
+            if sock is not custom_sock:
+                sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+                if "SO_REUSEPORT" in dir(socket):
+                    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
+                sock.bind((self.source_ip, source_port))
             flags = 0
             if change_ip:
                 flags |= self.CHANGE_IP
@@ -254,46 +285,48 @@ class StunClient(object):
                 sock.settimeout(time_left)
                 buf, recv_addr = sock.recvfrom(self.MTU)
                 recv_host, recv_port = recv_addr
-                # check STUN packet
+                # Check the STUN packet.
                 if len(buf) < 20:
                     continue
                 msg_type, msg_id, payload = self.unpack_stun_message(buf)
                 if tran_id != msg_id or msg_type != self.BIND_RESPONSE:
                     continue
                 source_addr  = sock.getsockname()
-                mapped_addr  = self.get_mapped_addr(payload)
+                mapped_addr  = self.extract_mapped_addr(payload)
                 ip_changed   = (recv_host != self.STUN_PORT)
                 port_changed = (recv_port != self.STUN_PORT)
                 self.logger.debug("(UDP) %s says: %s" % (recv_addr, mapped_addr))
                 return source_addr, mapped_addr, ip_changed, port_changed
         except Exception as e:
-            self.logger.debug("%s: %s" % (e.__class__.__name__, e))
+            self.logger.debug("Cannot do UDP STUN test: %s: %s" % (e.__class__.__name__, e))
             return None
         finally:
-            sock.close()
+            sock.settimeout(origin_timeout)
+            if sock is not custom_sock:
+                sock.close()
 
     def get_tcp_mapping(self, source_port):
-        server_ip = first = self.stun_ip_tcp[0]
+        server_ip = first = self._stun_ip_tcp[0]
         while True:
             ret = self.tcp_test(server_ip, source_port)
             if ret is None:
-                # server unavailable, put it at the end of the list
-                self.stun_ip_tcp.append(self.stun_ip_tcp.pop(0))
-                server_ip = self.stun_ip_tcp[0]
+                # Server unavailable, put it at the end of the list.
+                self._stun_ip_tcp.append(self._stun_ip_tcp.pop(0))
+                server_ip = self._stun_ip_tcp[0]
                 if server_ip == first:
                     raise Exception("No public STUN server is avaliable. Please check your Internet connection.")
             else:
                 source_addr, mapped_addr = ret
                 return source_addr, mapped_addr
 
-    def get_udp_mapping(self, source_port):
-        server_ip = first = self.stun_ip_udp[0]
+    def get_udp_mapping(self, source_port, custom_sock = None):
+        server_ip = first = self._stun_ip_udp[0]
         while True:
-            ret = self.udp_test(server_ip, source_port)
+            ret = self.udp_test(server_ip, source_port, custom_sock = custom_sock)
             if ret is None:
-                # server unavailable, put it at the end of the list
-                self.stun_ip_udp.append(self.stun_ip_udp.pop(0))
-                server_ip = self.stun_ip_udp[0]
+                # Server unavailable, put it at the end of the list.
+                self._stun_ip_udp.append(self._stun_ip_udp.pop(0))
+                server_ip = self._stun_ip_udp[0]
                 if server_ip == first:
                     raise Exception("No public STUN server is avaliable. Please check your Internet connection.")
             else:
@@ -308,9 +341,9 @@ class StunClient(object):
         ret_test2 = None
         ret_test3 = None
         if source_port == 0:
-            source_port = self.get_free_port(udp=True)
+            source_port = get_free_port(udp=True)
 
-        for server_ip in self.stun_ip_udp:
+        for server_ip in self._stun_ip_udp:
             ret = self.udp_test(server_ip, source_port, change_ip=False, change_port=False)
             if ret is None:
                 self.logger.debug("No response. Trying another STUN server...")
@@ -352,10 +385,10 @@ class StunClient(object):
     def is_tcp_cone(self, source_port = 0):
         # Detect NAT behavior for TCP. Requires at least three STUN servers for accuracy.
         if source_port == 0:
-            source_port = self.get_free_port()
+            source_port = get_free_port()
         mapped_addr_first = None
         count = 0
-        for server_ip in self.stun_ip_tcp:
+        for server_ip in self._stun_ip_tcp:
             if count >= 3:
                 return True
             ret = self.tcp_test(server_ip, source_port)
@@ -372,21 +405,23 @@ class HttpTestServer(object):
     # HTTP Server for testing purpose
     # On success, you can see the text "It works!".
 
-    def __init__(self, listen_addr):
+    def __init__(self, listen_addr, logger = None):
+        self.logger = logger if logger else Logger()
         self.running = False
         self.listen_addr = listen_addr
         self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
         self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-        if 'SO_REUSEPORT' in dir(socket):
+        if "SO_REUSEPORT" in dir(socket):
             self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
 
     def run(self):
         self.running = True
         self.sock.bind(self.listen_addr)
-        self.sock.listen(1)
+        self.sock.listen(5)
         while self.running:
             try:
                 conn, addr = self.sock.accept()
+                self.logger.debug("HttpTestServer got client %s" % (addr,))
             except Exception:
                 return
             try:
@@ -402,182 +437,532 @@ class HttpTestServer(object):
                 conn.close()
 
     def start(self):
+        self.logger.info("HttpTestServer starting...")
         threading.Thread(target=self.run).start()
 
     def stop(self):
+        self.logger.info("HttpTestServer stopping...")
         self.running = False
         sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
         sock.settimeout(0.1)
-        result = sock.connect_ex(self.listen_addr)
+        sock.connect_ex(self.listen_addr)
         sock.close()
         self.sock.close()
 
 
-class Natter(object):
-    def __init__(self, source_ip, source_port, test_http = False,
-                 keep_alive_host = "www.qq.com", keep_alive_interval = 10, retry_sec = 3, log_level = Logger.INFO):
-        self.logger = Logger(log_level)
-        self.source_ip = source_ip
-        self.source_port = source_port
-        self.test_http = test_http
-        self.keep_alive_host = keep_alive_host
-        self.keep_alive_interval = keep_alive_interval
-        self.retry_sec = retry_sec
-        self.stun_client = StunClient(source_ip, log_level = log_level)
-        self.keep_alive_sock = self._init_keep_alive_sock()
-        self.http_test_server = HttpTestServer((source_ip, source_port))
-
-    def _init_keep_alive_sock(self):
-        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+class TCPForwarder(object):
+    def __init__(self, listen_addr, forward_addr, logger = None):
+        self.listen_sock = None
+        self.listen_addr = listen_addr
+        self.forward_addr = forward_addr
+        self.logger = logger if logger else Logger()
+        self.stopped = False
+
+    def run(self):
+        self.stopped = False
+        self.listen_sock = s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
         s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
         s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-        if 'SO_REUSEPORT' in dir(socket):
+        if "SO_REUSEPORT" in dir(socket):
             s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
-        s.bind((self.source_ip, self.source_port))
-        s.connect((self.keep_alive_host, 80))
-        s.settimeout(self.keep_alive_interval)
-        return s
+        s.bind(self.listen_addr)
+        s.settimeout(None)
+        s.listen(5)
+        while not self.stopped:
+            try:
+                client_sock, client_addr = s.accept()
+                self.logger.debug("Got client: %s" % (client_addr,))
+            except Exception as e:
+                self.logger.debug("Cannot accept client: %s: %s" % (e.__class__.__name__, e))
+                continue
+            server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+            server_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+            try:
+                server_sock.settimeout(3)
+                server_sock.connect(self.forward_addr)
+                server_sock.settimeout(None)
+            except Exception as e:
+                self.logger.debug("Cannot connect to forward_addr: %s: %s" % (e.__class__.__name__, e))
+                client_sock.close()
+                server_sock.close()
+            threading.Thread(target=self._forward, args=(client_sock, server_sock)).start()
+            threading.Thread(target=self._forward, args=(server_sock, client_sock)).start()
+
+    @staticmethod
+    def _forward(s1, s2):
+        data = "..."
+        try:
+            while data:
+                data = s1.recv(1024)
+                if data:
+                    s2.sendall(data)
+                else:
+                    s1.shutdown(socket.SHUT_RD)
+                    s2.shutdown(socket.SHUT_WR)
+        except Exception:
+            s1.close()
+            s2.close()
+
+    def start(self):
+        threading.Thread(target=self.run).start()
+
+    def stop(self):
+        if self.stopped:
+            return
+        self.stopped = True
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        sock.settimeout(0.1)
+        sock.connect_ex(self.listen_addr)
+        sock.close()
+        self.listen_sock.close()
+        self.listen_sock = None
+
+
+class UDPForwarder(object):
+    def __init__(self, listen_sock, listen_addr, forward_addr, logger):
+        self.listen_sock = listen_sock
+        self.listen_addr = listen_addr
+        self.forward_addr = forward_addr
+        self.logger = logger if logger else Logger()
+        self.stopped = False
+        self.client_last = {}
+        self.srv_socks = {}
+        self.udp_timeout = 90
+
+    def run(self):
+        self.stopped = False
+        while not self.stopped:
+            try:
+                data, client_addr = self.listen_sock.recvfrom(2048)
+            except socket.timeout:
+                continue
+            self.client_last[client_addr] = time.time()
+            server_sock = self.srv_socks.get(client_addr)
+            if data and server_sock is None:
+                self.logger.debug("Got client: %s" % (client_addr,))
+                self.srv_socks[client_addr] = server_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+                threading.Thread(target=self._udp_forward, args=(client_addr,)).start()
+            if server_sock:
+                server_sock.sendto(data, self.forward_addr)
+
+    def _udp_forward(self, client_addr):
+        server_sock = self.srv_socks[client_addr]
+        server_sock.settimeout(self.udp_timeout)
+        data = "..."
+        try:
+            while data:
+                time_diff = time.time() - self.client_last[client_addr]
+                if time_diff > self.udp_timeout:
+                    server_sock.sendto("", self.forward_addr)
+                    raise socket.timeout("client timeout")
+                data, server_addr = server_sock.recvfrom(2048)
+                self.listen_sock.sendto(data, client_addr)
+        except Exception:
+            pass
+        finally:
+            server_sock.close()
+            del self.client_last[client_addr]
+            del self.srv_socks[client_addr]
+
+    def start(self):
+        threading.Thread(target=self.run).start()
+
+    def stop(self):
+        if self.stopped:
+            return
+        self.stopped = True
+        sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        sock.sendto(b"", self.listen_addr)
+        sock.close()
+
 
-    def _keep_alive(self):
+class NatterTCP(object):
+    def __init__(self, source_addr, forward_addr, keep_alive_host, logger = None):
+        self.logger = logger if logger else Logger()
+        self.stun_client = StunClient(source_addr[0], logger = self.logger)
+        self.forwarder = TCPForwarder(source_addr, forward_addr, logger = self.logger)
+        self.source_addr = source_addr
+        self.forward_addr = forward_addr
+        self.keep_alive_host = keep_alive_host
+        self.keep_alive_sock = None
+        self.forward_running = False
+
+    def keep_alive(self, timeout = 1):
+        # Note:
+        #   The only purpose of this method is to keep the outgoing TCP connection from being closed.
+        #   Natter will send a HEAD HTTP request with keep-alive header to the target host.
+        #   We don't want to disturb the host too much, and meanwhile we will get minimal return data this way.
         s = self.keep_alive_sock
         try:
-            s.sendall(b"GET /~ HTTP/1.1\r\n")
+            if s is None:
+                self.keep_alive_sock = s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+                s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+                s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+                if "SO_REUSEPORT" in dir(socket):
+                    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
+                s.bind(self.source_addr)
+                s.settimeout(timeout)
+                s.connect((self.keep_alive_host, 80))
+            s.sendall(b"HEAD / HTTP/1.1\r\n")
             s.sendall(b"Host: %s\r\n" % self.keep_alive_host.encode())
+            s.sendall(b"User-Agent: Mozilla/5.0 (%s; %s) Natter\r\n" % (sys.platform.encode(), os.name.encode()))
+            s.sendall(b"Accept: */*\r\n")
             s.sendall(b"Connection: keep-alive\r\n")
             s.sendall(b"\r\n")
+            received = b""
+            conn_closed = False
+            while b"\r\n\r\n" not in received and not conn_closed:
+                received = received[-4:] + s.recv(4096)
+                conn_closed = (len(received) == 0)
+            if not conn_closed:
+                self.logger.debug("[%s] Keep-Alive OK!" % time.asctime())
+                return True
+            else:
+                raise socket.error("Server closed connection")
         except Exception as e:
-            self.logger.debug("%s: %s" % (e.__class__.__name__, e))
+            self.logger.debug("Cannot TCP keep-alive: %s: %s" % (e.__class__.__name__, e))
+            if self.keep_alive_sock is None:
+                return False
+            try:
+                # Explicitly shut down the socket
+                self.keep_alive_sock.shutdown(socket.SHUT_RDWR)
+            except Exception:
+                pass
+            self.keep_alive_sock.close()
+            # Set self.keep_alive_sock to None so the keep-alive connection will be re-established the
+            # next time keep_alive() is called.
+            self.keep_alive_sock = None
             return False
+
+    def get_mapping(self):
         try:
-            while s.recv(4096):
-                self.logger.debug("[%s] Keep-Alive OK!" % time.asctime())
-            self.logger.debug("Server closed connection")
-            return False
-        except socket.timeout:
+            return self.stun_client.get_tcp_mapping(self.source_addr[1])
+        except Exception as e:
+            self.logger.debug("Cannot get TCP mapping: %s: %s" % (e.__class__.__name__, e))
+            return None
+
+    def start_forward(self):
+        if not self.forward_running:
+            self.forwarder.start()
+            self.forward_running = True
+
+    def stop_forward(self):
+        if self.forward_running:
+            self.forwarder.stop()
+            self.forward_running = False
+
+
+class NatterUDP(object):
+    def __init__(self, source_addr, forward_addr, keep_alive_host, logger = None):
+        self.base_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
+        self.base_sock.bind(source_addr)
+        self.logger = logger if logger else Logger()
+        self.stun_client = StunClient(source_addr[0], logger = self.logger)
+        self.forwarder = UDPForwarder(self.base_sock, source_addr, forward_addr, logger = self.logger)
+        self.source_addr = source_addr
+        self.forward_addr = forward_addr
+        self.keep_alive_host = keep_alive_host
+        self.forward_running = False
+
+    def keep_alive(self):
+        # Note:
+        #   Natter will send a message to port 30003 of the target's UDP, regardless of
+        #   whether the target replies to it.
+        try:
+            self.base_sock.sendto(b"hello", (self.keep_alive_host, 30003))
+            self.logger.debug("[%s] Keep-Alive OK!" % time.asctime())
             return True
         except Exception as e:
-            self.logger.debug("%s: %s" % (e.__class__.__name__, e))
+            self.logger.debug("Cannot UDP keep-alive: %s: %s" % (e.__class__.__name__, e))
             return False
 
-    def test_port_open(self, dst_addr, timeout = 3):
-        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-        sock.settimeout(timeout)
-        result = sock.connect_ex(dst_addr)
-        sock.close()
-        return result == 0
-
-    def tcp_punch(self):
-        self.logger.info("Checking NAT Type for UDP...")
-        nat_type = self.stun_client.check_nat_type()
-        if nat_type == StunClient.NAT_OPEN_INTERNET:
-            nat_type_txt = "Open Internet"
-        elif nat_type == StunClient.NAT_SYM_UDP_FIREWALL:
-            nat_type_txt = "Symmetric UDP firewall"
-        elif nat_type == StunClient.NAT_FULL_CONE:
-            nat_type_txt = "Full cone (NAT 1)"
-        elif nat_type == StunClient.NAT_RESTRICTED:
-            nat_type_txt = "Restricted (NAT 2)"
-        elif nat_type == StunClient.NAT_PORT_RESTRICTED:
-            nat_type_txt = "Port restricted (NAT 3)"
-        elif nat_type == StunClient.NAT_SYMMETRIC:
-            nat_type_txt = "Symmetric (NAT 4)"
-        else:
-            nat_type_txt = "Unknown"
-        self.logger.info("NAT Type for UDP: [ %s ]" % nat_type_txt)
-        if nat_type == StunClient.NAT_OPEN_INTERNET:
-            self.logger.warning("It looks like you are not in a NAT network, so there is no need to use this tool.")
-        elif nat_type != StunClient.NAT_FULL_CONE:
-            self.logger.warning("The NAT type of your network is not full cone (NAT 1). TCP hole punching may fail.")
-
-        self.logger.info("Checking NAT Type for TCP...")
-        if self.stun_client.is_tcp_cone():
-            self.logger.info("NAT Type for TCP: [ Cone NAT ]")
-        else:
-            self.logger.info("NAT Type for TCP: [ Symmetric ]")
-            self.logger.error("You cannot perform TCP hole punching in a symmetric NAT network.")
-            return
+    def get_mapping(self):
+        fwd = self.forward_running
+        if fwd:
+            # Temporarily stop port forwarding to avoid interference.
+            self.stop_forward()
+        try:
+            return self.stun_client.get_udp_mapping(self.source_addr[1], custom_sock = self.base_sock)
+        except Exception as e:
+            self.logger.debug("Cannot get UDP mapping: %s: %s" % (e.__class__.__name__, e))
+            return None
+        finally:
+            if fwd:
+                self.start_forward()
+
+    def start_forward(self):
+        if not self.forward_running:
+            self.forwarder.start()
+            self.forward_running = True
 
-        self.logger.info("Start punching...")
-        self.http_test_server.start()
-        source_addr, mapped_addr = self.stun_client.get_tcp_mapping(self.source_port)
-        if not self.test_port_open(source_addr):
-            self.logger.error("Local address %s is not available. Check your firewall settings." % source_addr)
+    def stop_forward(self):
+        if self.forward_running:
+            self.forwarder.stop()
+            self.forward_running = False
+
+
+class Natter(object):
+    def __init__(self, keep_alive_host, interval = 10, logger = None):
+        self.logger = logger if logger else Logger()
+        self.nr_list = []
+        self.keep_alive_host = keep_alive_host
+        self.interval = interval
+        self.hook_command = None
+        self.status_file = None
+        self.maps = {"tcp": {}, "udp": {}}
+
+    def __del__(self):
+        self.close()
+
+    def add_tcp_open_port(self, source_addr):
+        self.nr_list.append(NatterTCP(source_addr, None, self.keep_alive_host, logger = self.logger))
+
+    def add_udp_open_port(self, source_addr):
+        self.nr_list.append(NatterUDP(source_addr, None, self.keep_alive_host, logger = self.logger))
+
+    def add_tcp_forward_port(self, forward_addr):
+        source_addr = ("0.0.0.0", get_free_port())
+        self.nr_list.append(NatterTCP(source_addr, forward_addr, self.keep_alive_host, logger = self.logger))
+
+    def add_udp_forward_port(self, forward_addr):
+        source_addr = ("0.0.0.0", get_free_port(udp=True))
+        self.nr_list.append(NatterUDP(source_addr, forward_addr, self.keep_alive_host, logger = self.logger))
+
+    def set_hook(self, hook_command):
+        self.hook_command = hook_command
+
+    def set_status_file(self, status_file_path):
+        self.status_file = open(status_file_path, "w+")
+
+    def execute_hook(self, inner_addr, outer_addr, protocol, command):
+        inner_ip, inner_port = inner_addr
+        outer_ip, outer_port = outer_addr
+        command = command.replace("{inner_ip}", str(inner_ip))
+        command = command.replace("{inner_port}", str(inner_port))
+        command = command.replace("{outer_ip}", str(outer_ip))
+        command = command.replace("{outer_port}", str(outer_port))
+        command = command.replace("{protocol}", str(protocol))
+        os.system(command)
+
+    def update_status_file(self):
+        status = {"tcp": [], "udp": []}
+        for protocol in status:
+            for inner_ip, inner_port in self.maps[protocol]:
+                outer_ip, outer_port = self.maps[protocol][inner_ip, inner_port]
+                record = {
+                    "inner": "%s:%d" % (inner_ip, inner_port),
+                    "outer": "%s:%d" % (outer_ip, outer_port),
+                }
+                status[protocol].append(record)
+        self.status_file.seek(0)
+        self.status_file.truncate(0)
+        json.dump(status, self.status_file, indent = 4)
+        self.status_file.flush()
+
+    def _update_status(self, nr):
+        mapping = nr.get_mapping()
+        if not mapping:
             return
-        if self.test_port_open(mapped_addr):
-            self.logger.info(
-                "The TCP hole punching appears to be successful. "
-                "Please test this address from another network: %s" % str(mapped_addr)
-            )
-            print("\n================================\n    %s\n================================\n"% str(mapped_addr))
-            if self.test_http:
-                print("HTTP test server is enabled. Please check [ http://%s:%d/ ]\n" % mapped_addr)
-        else:
-            self.logger.warning(
-                "TCP hole punching seems to fail. Maybe you are behind a firewall. "
-                "However, you may check this address from another network: %s" % str(mapped_addr)
-            )
-        if not self.test_http:
-            self.http_test_server.stop()
-        # Keep alive
-        self.logger.info("TCP keep-alive...")
+        # update mapping dict
+        protocol = "tcp" if type(nr) is NatterTCP else "udp"
+        inner_addr, outer_addr = mapping
+        if nr.forward_addr:
+            inner_addr = nr.forward_addr
+        self.maps[protocol][inner_addr] = outer_addr
+        self.logger.info(">>> [%s] %s -> %s <<<" % (protocol.upper(), inner_addr, outer_addr))
+        # update status file
+        if self.status_file:
+            self.update_status_file()
+        # excute hook command
+        if self.hook_command:
+            threading.Thread(
+                target = self.execute_hook,
+                args = (inner_addr, outer_addr, protocol, self.hook_command)
+            ).start()
+
+    def run(self):
+        last_ok = {}
+        for nr in self.nr_list:
+            last_ok[nr] = False
+            nr.keep_alive()
+            if nr.forward_addr:
+                nr.start_forward()
         while True:
-            ok = self._keep_alive()
-            if not ok:
-                self.keep_alive_sock.close()
-                time.sleep(self.retry_sec)
-                self.keep_alive_sock = self._init_keep_alive_sock()
-                self._keep_alive()
-                source_addr, mapped_addr = self.stun_client.get_tcp_mapping(self.source_port)
-                self.logger.info("Mapped address: %s" % str(mapped_addr))
+            for nr in self.nr_list:
+                if not last_ok[nr]:
+                    self._update_status(nr)
+                last_ok[nr] = nr.keep_alive()
+            time.sleep(self.interval)
+            self.logger.debug("Current threads: %s" % threading.active_count())
+
+    @staticmethod
+    def from_config(config_path):
+        fo = open(config_path)
+        config = json.load(fo)
+        fo.close()
+
+        log_level = {
+            "debug": Logger.DEBUG,
+            "info": Logger.INFO,
+            "warning": Logger.WARNING,
+            "error": Logger.ERROR
+        }[config["logging"]["level"]]
+        log_file = config["logging"]["log_file"]
+        if log_file:
+            logger = Logger(log_level, files=(sys.stderr, open(log_file, "a")))
+        else:
+            logger = Logger(log_level)
+
+        StunClient.stun_server_tcp = config["stun_server"]["tcp"]
+        StunClient.stun_server_udp = config["stun_server"]["udp"]
+
+        keep_alive_host = config["keep_alive"]
+
+        natter = Natter(keep_alive_host, interval=10, logger=logger)
+        hook = config["status_report"]["hook"]
+        if hook:
+            natter.set_hook(hook)
+        statfile = config["status_report"]["status_file"]
+        if statfile:
+            natter.set_status_file(statfile)
+
+        for addr_str in config["open_port"]["tcp"]:
+            ip, port_str = addr_str.split(":")
+            port = int(port_str)
+            natter.add_tcp_open_port((ip, port))
+
+        for addr_str in config["open_port"]["udp"]:
+            ip, port_str = addr_str.split(":")
+            port = int(port_str)
+            natter.add_udp_open_port((ip, port))
+
+        for addr_str in config["forward_port"]["tcp"]:
+            ip, port_str = addr_str.split(":")
+            port = int(port_str)
+            natter.add_tcp_forward_port((ip, port))
+
+        for addr_str in config["forward_port"]["udp"]:
+            ip, port_str = addr_str.split(":")
+            port = int(port_str)
+            natter.add_udp_forward_port((ip, port))
+
+        return natter
     
     def close(self):
-        try:
-            self.keep_alive_sock.shutdown(socket.SHUT_RDWR)
-        except Exception:
-            pass
-        self.keep_alive_sock.close()
-        self.http_test_server.stop()
+        for nr in self.nr_list:
+            nr.stop_forward()
+        if self.status_file:
+            self.status_file.close()
+
+def print_nat(source_ip = "0.0.0.0", source_port = 0):
+    logger = Logger()
+    stun_client = StunClient(source_ip, logger = logger)
+    nat_type = stun_client.check_nat_type(source_port)
+    if nat_type == StunClient.NAT_OPEN_INTERNET:
+        nat_type_txt = "Open Internet"
+    elif nat_type == StunClient.NAT_SYM_UDP_FIREWALL:
+        nat_type_txt = "Symmetric UDP firewall"
+    elif nat_type == StunClient.NAT_FULL_CONE:
+        nat_type_txt = "Full cone (NAT 1)"
+    elif nat_type == StunClient.NAT_RESTRICTED:
+        nat_type_txt = "Restricted (NAT 2)"
+    elif nat_type == StunClient.NAT_PORT_RESTRICTED:
+        nat_type_txt = "Port restricted (NAT 3)"
+    elif nat_type == StunClient.NAT_SYMMETRIC:
+        nat_type_txt = "Symmetric (NAT 4)"
+    else:
+        nat_type_txt = "Unknown"
+    logger.info("NAT Type for UDP: [ %s ]" % nat_type_txt)
+    if nat_type == StunClient.NAT_OPEN_INTERNET:
+        logger.warning("It looks like you are not in a NAT network, so there is no need to use this tool.")
+    elif nat_type != StunClient.NAT_FULL_CONE:
+        logger.warning("The NAT type of your network is not full cone (NAT 1). TCP hole punching may fail.")
+
+    logger.info("Checking NAT Type for TCP...")
+    if stun_client.is_tcp_cone():
+        logger.info("NAT Type for TCP: [ Cone NAT ]")
+    else:
+        logger.info("NAT Type for TCP: [ Symmetric ]")
+        logger.warning("You cannot perform TCP hole punching in a symmetric NAT network.")
+        return
 
 
 def main():
     try:
+        config_path = ""
         src_host = "0.0.0.0"
         src_port = -1
         verbose = False
         test_http = False
+        use_config = False
+        check_nat = False
         l = []
         for arg in sys.argv[1:]:
             if arg[0] == "-":
-                if arg == "-v":
+                if arg == "-c":
+                    use_config = True
+                elif arg == "-v":
                     verbose = True
                 elif arg == "-t":
                     test_http = True
+                elif arg == "--check-nat":
+                    check_nat = True
                 else:
                     raise ValueError
             else:
                 l.append(arg)
-        if len(l) == 1:
-            src_port = int(l[0])
-        elif len(l) == 2:
-            src_host = l[0]
-            src_port = int(l[1])
+        if not use_config:
+            if len(l) == 0 and check_nat:
+                src_port = 0
+            elif len(l) == 1:
+                src_port = int(l[0])
+            elif len(l) == 2:
+                src_host = l[0]
+                src_port = int(l[1])
+            else:
+                raise ValueError
         else:
-            raise ValueError
+            if len(l) == 1:
+                config_path = l[0]
+                if not os.path.exists(config_path):
+                    print("Config file not found.")
+                    raise ValueError
+            else:
+                raise ValueError
     except ValueError:
-        print("Usage: python natter.py [-v] [-t] [SRC_HOST] SRC_PORT\n")
+        print(
+            "Usage: \n"
+            "    python natter.py [-v] [-t] [SRC_IP] SRC_PORT\n"
+            "    python natter.py --check-nat [SRC_IP] SRC_PORT\n"
+            "    python natter.py --check-nat\n"
+            "    python natter.py -c config_file\n"
+        )
         return
     
-    if verbose:
-        log_level=Logger.DEBUG
+    if check_nat:
+        print_nat(src_host, src_port)
+        return
+    
+    http_test_server = None
+    if not use_config:
+        # TCP single port punching
+        log_level = Logger.DEBUG if verbose else Logger.INFO
+        natter = Natter("www.qq.com", interval=10, logger=Logger(log_level))
+        natter.add_tcp_open_port((src_host, src_port))
+        if test_http:
+            http_test_server = HttpTestServer((src_host, src_port), logger=natter.logger)
+            http_test_server.start()
     else:
-        log_level=Logger.INFO
-    natter = Natter(src_host, src_port, test_http=test_http, log_level=log_level)
+        natter = Natter.from_config(config_path)
+
     try:
-        natter.tcp_punch()
+        natter.run()
     except KeyboardInterrupt:
-        print("\nExiting...\n")
+        if http_test_server:
+            http_test_server.stop()
+        natter.logger.info("Exiting...")
         natter.close()
-
+        os._exit(0)
 
 if __name__ == '__main__':
     main()