├── .gitignore ├── LICENSE ├── README.md ├── server.py ├── client.py └── stun.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | bin/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # Installer logs 26 | pip-log.txt 27 | pip-delete-this-directory.txt 28 | 29 | # Unit test / coverage reports 30 | htmlcov/ 31 | .tox/ 32 | .coverage 33 | .cache 34 | nosetests.xml 35 | coverage.xml 36 | 37 | # Translations 38 | *.mo 39 | 40 | # Mr Developer 41 | .mr.developer.cfg 42 | .project 43 | .pydevproject 44 | .idea 45 | 46 | # Rope 47 | .ropeproject 48 | 49 | # Django stuff: 50 | *.log 51 | *.pot 52 | 53 | # Sphinx documentation 54 | docs/_build/ 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 laike9m 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PyPunchP2P 2 | ========== 3 | 4 | ### THIS PROJECT IS FOR STUDYING AND VERIFICATION, DON'T USE IT IN PRODUCTION. 5 | 6 | Python p2p chat client/server with built-in NAT traversal (UDP hole punching). 7 | I've written [an article][4] about the detailed implementation (in Chinese). 8 | 9 | Based on 10 | [koenbollen's gist][1] 11 | [pystun][2] 12 | [Peer-to-Peer Communication Across Network Address Translators][3] 13 | 14 | Python edition: py2.6+ but no Python 3 support 15 | Platform: Linux/Windows 16 | 17 | Usage 18 | ----- 19 | Suppose you run server.py on a VPS with ip 1.2.3.4, listening on port 5678 20 | ```bash 21 | $ server.py 5678 22 | ``` 23 | 24 | On client A and client B (run this on both clients): 25 | ```bash 26 | $ client.py 1.2.3.4 5678 100 27 | ``` 28 | The number `100` is used to match clients, you can choose any number you like but only clients with the **same** number will be linked by server. If two clients get linked, two people can chat by typing in terminal, and once you hit `` your partner will see your message in his terminal. 29 | Encoding is a known issue since I didn't pay much effort on making this tool perfect, but as long as you type English it will be fine. 30 | 31 | Test Mode 32 | ---- 33 | You could do simulation testing by specifying a fourth parameter of `client.py`, it will assume that your client is behind a specific type of NAT device. 34 | 35 | Here are the corresponding NAT type and number: 36 | 37 | FullCone 0 38 | RestrictNAT 1 39 | RestrictPortNAT 2 40 | SymmetricNAT 3 41 | 42 | So you might run 43 | ```bash 44 | $ client.py 1.2.3.4 5678 100 1 45 | ``` 46 | pretending your client is behind RestrictNAT. 47 | You can test the relay server functionality by making `3` as the forth parameter, since if one client is behind symmetric NAT, there will be no direct connection but server forwaring. 48 | 49 | License 50 | ------- 51 | MIT 52 | 53 | [1]:https://gist.github.com/koenbollen/464613 54 | [2]:https://pypi.python.org/pypi/pystun 55 | [3]:http://www.bford.info/pub/net/p2pnat/index.html 56 | [4]:http://www.laike9m.com/blog/pythonshi-xian-stunturnp2pliao-tian,29/ 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding:utf-8 3 | 4 | import socket 5 | import struct 6 | import sys 7 | from collections import namedtuple 8 | 9 | FullCone = "Full Cone" # 0 10 | RestrictNAT = "Restrict NAT" # 1 11 | RestrictPortNAT = "Restrict Port NAT" # 2 12 | SymmetricNAT = "Symmetric NAT" # 3 13 | UnknownNAT = "Unknown NAT" # 4 14 | NATTYPE = (FullCone, RestrictNAT, RestrictPortNAT, SymmetricNAT, UnknownNAT) 15 | 16 | def addr2bytes(addr, nat_type_id): 17 | """Convert an address pair to a hash.""" 18 | host, port = addr 19 | try: 20 | host = socket.gethostbyname(host) 21 | except (socket.gaierror, socket.error): 22 | raise ValueError("invalid host") 23 | try: 24 | port = int(port) 25 | except ValueError: 26 | raise ValueError("invalid port") 27 | try: 28 | nat_type_id = int(nat_type_id) 29 | except ValueError: 30 | raise ValueError("invalid NAT type") 31 | bytes = socket.inet_aton(host) 32 | bytes += struct.pack("H", port) 33 | bytes += struct.pack("H", nat_type_id) 34 | return bytes 35 | 36 | 37 | def main(): 38 | port = sys.argv[1] 39 | try: 40 | port = int(sys.argv[1]) 41 | except (IndexError, ValueError): 42 | pass 43 | 44 | sockfd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 45 | sockfd.bind(("", port)) 46 | print "listening on *:%d (udp)" % port 47 | 48 | poolqueue = {} 49 | # A,B with addr_A,addr_B,pool=100 50 | # temp state {100:(nat_type_id, addr_A, addr_B)} 51 | # final state {addr_A:addr_B, addr_B:addr_A} 52 | symmetric_chat_clients = {} 53 | ClientInfo = namedtuple("ClientInfo", "addr, nat_type_id") 54 | while True: 55 | data, addr = sockfd.recvfrom(1024) 56 | if data.startswith("msg "): 57 | # forward symmetric chat msg, act as TURN server 58 | try: 59 | sockfd.sendto(data[4:], symmetric_chat_clients[addr]) 60 | print("msg successfully forwarded to {0}".format(symmetric_chat_clients[addr])) 61 | print(data[4:]) 62 | except KeyError: 63 | print("something is wrong with symmetric_chat_clients!") 64 | else: 65 | # help build connection between clients, act as STUN server 66 | print "connection from %s:%d" % addr 67 | pool, nat_type_id = data.strip().split() 68 | sockfd.sendto("ok {0}".format(pool), addr) 69 | print("pool={0}, nat_type={1}, ok sent to client".format(pool, NATTYPE[int(nat_type_id)])) 70 | data, addr = sockfd.recvfrom(2) 71 | if data != "ok": 72 | continue 73 | 74 | print "request received for pool:", pool 75 | 76 | try: 77 | a, b = poolqueue[pool].addr, addr 78 | nat_type_id_a, nat_type_id_b = poolqueue[pool].nat_type_id, nat_type_id 79 | sockfd.sendto(addr2bytes(a, nat_type_id_a), b) 80 | sockfd.sendto(addr2bytes(b, nat_type_id_b), a) 81 | print "linked", pool 82 | del poolqueue[pool] 83 | except KeyError: 84 | poolqueue[pool] = ClientInfo(addr, nat_type_id) 85 | 86 | if pool in symmetric_chat_clients: 87 | if nat_type_id == '3' or symmetric_chat_clients[pool][0] == '3': 88 | # at least one is symmetric NAT 89 | recorded_client_addr = symmetric_chat_clients[pool][1] 90 | symmetric_chat_clients[addr] = recorded_client_addr 91 | symmetric_chat_clients[recorded_client_addr] = addr 92 | print("Hurray! symmetric chat link established.") 93 | del symmetric_chat_clients[pool] 94 | else: 95 | del symmetric_chat_clients[pool] # neither clients are symmetric NAT 96 | else: 97 | symmetric_chat_clients[pool] = (nat_type_id, addr) 98 | 99 | 100 | if __name__ == "__main__": 101 | if len(sys.argv) != 2: 102 | print("usage: server.py port") 103 | exit(0) 104 | else: 105 | assert sys.argv[1].isdigit(), "port should be a number!" 106 | main() 107 | -------------------------------------------------------------------------------- /client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding:utf-8 3 | 4 | import optparse 5 | import socket 6 | import struct 7 | import sys 8 | import time 9 | from threading import Event, Thread 10 | 11 | import stun 12 | 13 | FullCone = "Full Cone" # 0 14 | RestrictNAT = "Restrict NAT" # 1 15 | RestrictPortNAT = "Restrict Port NAT" # 2 16 | SymmetricNAT = "Symmetric NAT" # 3 17 | UnknownNAT = "Unknown NAT" # 4 18 | NATTYPE = (FullCone, RestrictNAT, RestrictPortNAT, SymmetricNAT, UnknownNAT) 19 | 20 | 21 | def bytes2addr(bytes): 22 | """Convert a hash to an address pair.""" 23 | if len(bytes) != 8: 24 | raise ValueError("invalid bytes") 25 | host = socket.inet_ntoa(bytes[:4]) 26 | port = struct.unpack("H", bytes[-4:-2])[ 27 | 0] # unpack returns a tuple even if it contains exactly one item 28 | nat_type_id = struct.unpack("H", bytes[-2:])[0] 29 | target = (host, port) 30 | return target, nat_type_id 31 | 32 | 33 | class Client(): 34 | def __init__(self): 35 | try: 36 | master_ip = '127.0.0.1' if sys.argv[ 37 | 1] == 'localhost' else sys.argv[1] 38 | self.master = (master_ip, int(sys.argv[2])) 39 | self.pool = sys.argv[3].strip() 40 | self.sockfd = self.target = None 41 | self.periodic_running = False 42 | self.peer_nat_type = None 43 | except (IndexError, ValueError): 44 | print(sys.stderr, "usage: %s " % sys.argv[0]) 45 | sys.exit(65) 46 | 47 | def request_for_connection(self, nat_type_id=0): 48 | self.sockfd = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 49 | self.sockfd.sendto(self.pool + ' {0}'.format(nat_type_id), self.master) 50 | data, addr = self.sockfd.recvfrom(len(self.pool) + 3) 51 | if data != "ok " + self.pool: 52 | print(sys.stderr, "unable to request!") 53 | sys.exit(1) 54 | self.sockfd.sendto("ok", self.master) 55 | sys.stderr = sys.stdout 56 | print(sys.stderr, 57 | "request sent, waiting for partner in pool '%s'..." % self.pool) 58 | data, addr = self.sockfd.recvfrom(8) 59 | 60 | self.target, peer_nat_type_id = bytes2addr(data) 61 | print(self.target, peer_nat_type_id) 62 | self.peer_nat_type = NATTYPE[peer_nat_type_id] 63 | print(sys.stderr, "connected to {1}:{2}, its NAT type is {0}".format( 64 | self.peer_nat_type, *self.target)) 65 | 66 | def recv_msg(self, sock, is_restrict=False, event=None): 67 | if is_restrict: 68 | while True: 69 | data, addr = sock.recvfrom(1024) 70 | if self.periodic_running: 71 | print("periodic_send is alive") 72 | self.periodic_running = False 73 | event.set() 74 | print("received msg from target," 75 | "periodic send cancelled, chat start.") 76 | if addr == self.target or addr == self.master: 77 | sys.stdout.write(data) 78 | if data == "punching...\n": 79 | sock.sendto("end punching\n", addr) 80 | else: 81 | while True: 82 | data, addr = sock.recvfrom(1024) 83 | if addr == self.target or addr == self.master: 84 | sys.stdout.write(data) 85 | if data == "punching...\n": # peer是restrict 86 | sock.sendto("end punching", addr) 87 | 88 | def send_msg(self, sock): 89 | while True: 90 | data = sys.stdin.readline() 91 | sock.sendto(data, self.target) 92 | 93 | @staticmethod 94 | def start_working_threads(send, recv, event=None, *args, **kwargs): 95 | ts = Thread(target=send, args=args, kwargs=kwargs) 96 | ts.setDaemon(True) 97 | ts.start() 98 | if event: 99 | event.wait() 100 | tr = Thread(target=recv, args=args, kwargs=kwargs) 101 | tr.setDaemon(True) 102 | tr.start() 103 | 104 | def chat_fullcone(self): 105 | self.start_working_threads(self.send_msg, self.recv_msg, None, 106 | self.sockfd) 107 | 108 | def chat_restrict(self): 109 | from threading import Timer 110 | cancel_event = Event() 111 | 112 | def send(count): 113 | self.sockfd.sendto('punching...\n', self.target) 114 | print("UDP punching package {0} sent".format(count)) 115 | if self.periodic_running: 116 | Timer(0.5, send, args=(count + 1, )).start() 117 | 118 | self.periodic_running = True 119 | send(0) 120 | kwargs = {'is_restrict': True, 'event': cancel_event} 121 | self.start_working_threads(self.send_msg, self.recv_msg, cancel_event, 122 | self.sockfd, **kwargs) 123 | 124 | def chat_symmetric(self): 125 | """ 126 | Completely rely on relay server(TURN) 127 | """ 128 | 129 | def send_msg_symm(sock): 130 | while True: 131 | data = 'msg ' + sys.stdin.readline() 132 | sock.sendto(data, self.master) 133 | 134 | def recv_msg_symm(sock): 135 | while True: 136 | data, addr = sock.recvfrom(1024) 137 | if addr == self.master: 138 | sys.stdout.write(data) 139 | 140 | self.start_working_threads(send_msg_symm, recv_msg_symm, None, 141 | self.sockfd) 142 | 143 | def main(self, test_nat_type=None): 144 | """ 145 | nat_type是自己的nat类型 146 | peer_nat_type是从服务器获取的对方的nat类型 147 | 选择哪种chat模式是根据nat_type来选择, 例如我这边的NAT设备是restrict, 那么我必须得一直向对方发包, 148 | 我的NAT设备才能识别对方为"我已经发过包的地址". 直到收到对方的包, periodic发送停止 149 | """ 150 | if not test_nat_type: 151 | nat_type, _, _ = self.get_nat_type() 152 | else: 153 | nat_type = test_nat_type # 假装正在测试某种类型的NAT 154 | try: 155 | self.request_for_connection(nat_type_id=NATTYPE.index(nat_type)) 156 | except ValueError: 157 | print("NAT type is %s" % nat_type) 158 | self.request_for_connection(nat_type_id=4) # Unknown NAT 159 | 160 | if nat_type == UnknownNAT or self.peer_nat_type == UnknownNAT: 161 | print("Symmetric chat mode") 162 | self.chat_symmetric() 163 | if nat_type == SymmetricNAT or self.peer_nat_type == SymmetricNAT: 164 | print("Symmetric chat mode") 165 | self.chat_symmetric() 166 | elif nat_type == FullCone: 167 | print("FullCone chat mode") 168 | self.chat_fullcone() 169 | elif nat_type in (RestrictNAT, RestrictPortNAT): 170 | print("Restrict chat mode") 171 | self.chat_restrict() 172 | else: 173 | print("NAT type wrong!") 174 | 175 | while True: 176 | try: 177 | time.sleep(0.5) 178 | except KeyboardInterrupt: 179 | print("exit") 180 | sys.exit(0) 181 | 182 | @staticmethod 183 | def get_nat_type(): 184 | parser = optparse.OptionParser(version=stun.__version__) 185 | parser.add_option( 186 | "-d", 187 | "--debug", 188 | dest="DEBUG", 189 | action="store_true", 190 | default=False, 191 | help="Enable debug logging") 192 | parser.add_option( 193 | "-H", 194 | "--host", 195 | dest="stun_host", 196 | default=None, 197 | help="STUN host to use") 198 | parser.add_option( 199 | "-P", 200 | "--host-port", 201 | dest="stun_port", 202 | type="int", 203 | default=3478, 204 | help="STUN host port to use (default: " 205 | "3478)") 206 | parser.add_option( 207 | "-i", 208 | "--interface", 209 | dest="source_ip", 210 | default="0.0.0.0", 211 | help="network interface for client (default: 0.0.0.0)") 212 | parser.add_option( 213 | "-p", 214 | "--port", 215 | dest="source_port", 216 | type="int", 217 | default=54320, 218 | help="port to listen on for client " 219 | "(default: 54320)") 220 | (options, args) = parser.parse_args() 221 | if options.DEBUG: 222 | stun.enable_logging() 223 | kwargs = dict( 224 | source_ip=options.source_ip, 225 | source_port=int(options.source_port), 226 | stun_host=options.stun_host, 227 | stun_port=options.stun_port) 228 | nat_type, external_ip, external_port = stun.get_ip_info(**kwargs) 229 | print("NAT Type:", nat_type) 230 | print("External IP:", external_ip) 231 | print("External Port:", external_port) 232 | return nat_type, external_ip, external_port 233 | 234 | 235 | if __name__ == "__main__": 236 | c = Client() 237 | try: 238 | test_nat_type = NATTYPE[int(sys.argv[4])] # 输入数字0,1,2,3 239 | except IndexError: 240 | test_nat_type = None 241 | 242 | c.main(test_nat_type) 243 | -------------------------------------------------------------------------------- /stun.py: -------------------------------------------------------------------------------- 1 | # coding:utf-8 2 | # this script come from https://pypi.python.org/pypi/pystun 3 | 4 | import random 5 | import socket 6 | import binascii 7 | import logging 8 | 9 | __version__ = "0.0.4" 10 | 11 | log = logging.getLogger("pystun") 12 | 13 | 14 | def enable_logging(): 15 | logging.basicConfig() 16 | log.setLevel(logging.DEBUG) 17 | 18 | stun_servers_list = ( 19 | "stun.ekiga.net", 20 | 'stunserver.org', 21 | 'stun.ideasip.com', 22 | 'stun.softjoys.com', 23 | 'stun.voipbuster.com', 24 | ) 25 | 26 | #stun attributes 27 | MappedAddress = '0001' 28 | ResponseAddress = '0002' 29 | ChangeRequest = '0003' 30 | SourceAddress = '0004' 31 | ChangedAddress = '0005' 32 | Username = '0006' 33 | Password = '0007' 34 | MessageIntegrity = '0008' 35 | ErrorCode = '0009' 36 | UnknownAttribute = '000A' 37 | ReflectedFrom = '000B' 38 | XorOnly = '0021' 39 | XorMappedAddress = '8020' 40 | ServerName = '8022' 41 | SecondaryAddress = '8050' # Non standard extention 42 | 43 | #types for a stun message 44 | BindRequestMsg = '0001' 45 | BindResponseMsg = '0101' 46 | BindErrorResponseMsg = '0111' 47 | SharedSecretRequestMsg = '0002' 48 | SharedSecretResponseMsg = '0102' 49 | SharedSecretErrorResponseMsg = '0112' 50 | 51 | dictAttrToVal = {'MappedAddress': MappedAddress, 52 | 'ResponseAddress': ResponseAddress, 53 | 'ChangeRequest': ChangeRequest, 54 | 'SourceAddress': SourceAddress, 55 | 'ChangedAddress': ChangedAddress, 56 | 'Username': Username, 57 | 'Password': Password, 58 | 'MessageIntegrity': MessageIntegrity, 59 | 'ErrorCode': ErrorCode, 60 | 'UnknownAttribute': UnknownAttribute, 61 | 'ReflectedFrom': ReflectedFrom, 62 | 'XorOnly': XorOnly, 63 | 'XorMappedAddress': XorMappedAddress, 64 | 'ServerName': ServerName, 65 | 'SecondaryAddress': SecondaryAddress} 66 | 67 | dictMsgTypeToVal = { 68 | 'BindRequestMsg': BindRequestMsg, 69 | 'BindResponseMsg': BindResponseMsg, 70 | 'BindErrorResponseMsg': BindErrorResponseMsg, 71 | 'SharedSecretRequestMsg': SharedSecretRequestMsg, 72 | 'SharedSecretResponseMsg': SharedSecretResponseMsg, 73 | 'SharedSecretErrorResponseMsg': SharedSecretErrorResponseMsg} 74 | 75 | dictValToMsgType = {} 76 | 77 | dictValToAttr = {} 78 | 79 | Blocked = "Blocked" 80 | OpenInternet = "Open Internet" 81 | FullCone = "Full Cone" 82 | SymmetricUDPFirewall = "Symmetric UDP Firewall" 83 | RestrictNAT = "Restrict NAT" 84 | RestrictPortNAT = "Restrict Port NAT" 85 | SymmetricNAT = "Symmetric NAT" 86 | ChangedAddressError = "Meet an error, when do Test1 on Changed IP and Port" 87 | 88 | 89 | def _initialize(): 90 | items = dictAttrToVal.items() 91 | for i in xrange(len(items)): 92 | dictValToAttr.update({items[i][1]: items[i][0]}) 93 | items = dictMsgTypeToVal.items() 94 | for i in xrange(len(items)): 95 | dictValToMsgType.update({items[i][1]: items[i][0]}) 96 | 97 | 98 | def gen_tran_id(): 99 | a = '' 100 | for i in xrange(32): 101 | a += random.choice('0123456789ABCDEF') # RFC3489 128bits transaction ID 102 | #return binascii.a2b_hex(a) 103 | return a 104 | 105 | 106 | def stun_test(sock, host, port, source_ip, source_port, send_data=""): 107 | retVal = {'Resp': False, 'ExternalIP': None, 'ExternalPort': None, 108 | 'SourceIP': None, 'SourcePort': None, 'ChangedIP': None, 109 | 'ChangedPort': None} 110 | str_len = "%#04d" % (len(send_data) / 2) 111 | tranid = gen_tran_id() 112 | str_data = ''.join([BindRequestMsg, str_len, tranid, send_data]) 113 | data = binascii.a2b_hex(str_data) 114 | recvCorr = False 115 | while not recvCorr: 116 | recieved = False 117 | count = 3 118 | while not recieved: 119 | log.debug("sendto %s" % str((host, port))) 120 | try: 121 | sock.sendto(data, (host, port)) 122 | except socket.gaierror: 123 | retVal['Resp'] = False 124 | return retVal 125 | try: 126 | buf, addr = sock.recvfrom(2048) 127 | log.debug("recvfrom: %s" % str(addr)) 128 | recieved = True 129 | except Exception: 130 | recieved = False 131 | if count > 0: 132 | count -= 1 133 | else: 134 | retVal['Resp'] = False 135 | return retVal 136 | msgtype = binascii.b2a_hex(buf[0:2]) 137 | bind_resp_msg = dictValToMsgType[msgtype] == "BindResponseMsg" 138 | tranid_match = tranid.upper() == binascii.b2a_hex(buf[4:20]).upper() 139 | if bind_resp_msg and tranid_match: 140 | recvCorr = True 141 | retVal['Resp'] = True 142 | len_message = int(binascii.b2a_hex(buf[2:4]), 16) 143 | len_remain = len_message 144 | base = 20 145 | while len_remain: 146 | attr_type = binascii.b2a_hex(buf[base:(base + 2)]) 147 | attr_len = int(binascii.b2a_hex(buf[(base + 2):(base + 4)]), 148 | 16) 149 | if attr_type == MappedAddress: # first two bytes: 0x0001 150 | port = int(binascii.b2a_hex(buf[base + 6:base + 8]), 16) 151 | ip = ".".join([ 152 | str(int(binascii.b2a_hex(buf[base + 8:base + 9]), 16)), 153 | str(int(binascii.b2a_hex(buf[base + 9:base + 10]), 16)), 154 | str(int(binascii.b2a_hex(buf[base + 10:base + 11]), 16)), 155 | str(int(binascii.b2a_hex(buf[base + 11:base + 12]), 16))]) 156 | retVal['ExternalIP'] = ip 157 | retVal['ExternalPort'] = port 158 | if attr_type == SourceAddress: 159 | port = int(binascii.b2a_hex(buf[base + 6:base + 8]), 16) 160 | ip = ".".join([ 161 | str(int(binascii.b2a_hex(buf[base + 8:base + 9]), 16)), 162 | str(int(binascii.b2a_hex(buf[base + 9:base + 10]), 16)), 163 | str(int(binascii.b2a_hex(buf[base + 10:base + 11]), 16)), 164 | str(int(binascii.b2a_hex(buf[base + 11:base + 12]), 16))]) 165 | retVal['SourceIP'] = ip 166 | retVal['SourcePort'] = port 167 | if attr_type == ChangedAddress: 168 | port = int(binascii.b2a_hex(buf[base + 6:base + 8]), 16) 169 | ip = ".".join([ 170 | str(int(binascii.b2a_hex(buf[base + 8:base + 9]), 16)), 171 | str(int(binascii.b2a_hex(buf[base + 9:base + 10]), 16)), 172 | str(int(binascii.b2a_hex(buf[base + 10:base + 11]), 16)), 173 | str(int(binascii.b2a_hex(buf[base + 11:base + 12]), 16))]) 174 | retVal['ChangedIP'] = ip 175 | retVal['ChangedPort'] = port 176 | #if attr_type == ServerName: 177 | #serverName = buf[(base+4):(base+4+attr_len)] 178 | base = base + 4 + attr_len 179 | len_remain = len_remain - (4 + attr_len) 180 | #s.close() 181 | return retVal 182 | 183 | 184 | def get_nat_type(s, source_ip, source_port, stun_host=None, stun_port=3478): 185 | _initialize() 186 | port = stun_port 187 | log.debug("Do Test1") 188 | resp = False 189 | if stun_host: 190 | ret = stun_test(s, stun_host, port, source_ip, source_port) 191 | resp = ret['Resp'] 192 | else: 193 | for stun_host in stun_servers_list: 194 | log.debug('Trying STUN host: %s' % stun_host) 195 | ret = stun_test(s, stun_host, port, source_ip, source_port) 196 | resp = ret['Resp'] 197 | if resp: 198 | break 199 | if not resp: 200 | return Blocked, ret 201 | log.debug("Result: %s" % ret) 202 | exIP = ret['ExternalIP'] 203 | exPort = ret['ExternalPort'] 204 | changedIP = ret['ChangedIP'] 205 | changedPort = ret['ChangedPort'] 206 | if ret['ExternalIP'] == source_ip: 207 | changeRequest = ''.join([ChangeRequest, '0004', "00000006"]) 208 | ret = stun_test(s, stun_host, port, source_ip, source_port, 209 | changeRequest) 210 | if ret['Resp']: 211 | typ = OpenInternet 212 | else: 213 | typ = SymmetricUDPFirewall 214 | else: 215 | changeRequest = ''.join([ChangeRequest, '0004', "00000006"]) 216 | log.debug("Do Test2") 217 | ret = stun_test(s, stun_host, port, source_ip, source_port, 218 | changeRequest) 219 | log.debug("Result: %s" % ret) 220 | if ret['Resp']: 221 | typ = FullCone 222 | else: 223 | log.debug("Do Test1") 224 | ret = stun_test(s, changedIP, changedPort, source_ip, source_port) 225 | log.debug("Result: %s" % ret) 226 | if not ret['Resp']: 227 | typ = ChangedAddressError 228 | else: 229 | if exIP == ret['ExternalIP'] and exPort == ret['ExternalPort']: 230 | changePortRequest = ''.join([ChangeRequest, '0004', "00000002"]) 231 | log.debug("Do Test3") 232 | ret = stun_test(s, changedIP, port, source_ip, source_port, changePortRequest) 233 | log.debug("Result: %s" % ret) 234 | if ret['Resp'] == True: 235 | typ = RestrictNAT 236 | else: 237 | typ = RestrictPortNAT 238 | else: 239 | typ = SymmetricNAT 240 | return typ, ret 241 | 242 | 243 | def get_ip_info(source_ip="0.0.0.0", source_port=54320, stun_host=None, 244 | stun_port=3478): 245 | socket.setdefaulttimeout(2) 246 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 247 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 248 | s.bind((source_ip, source_port)) 249 | nat_type, nat = get_nat_type(s, source_ip, source_port, 250 | stun_host=stun_host, stun_port=stun_port) 251 | external_ip = nat['ExternalIP'] 252 | external_port = nat['ExternalPort'] 253 | s.close() 254 | socket.setdefaulttimeout(None) 255 | return nat_type, external_ip, external_port 256 | 257 | 258 | def main(): 259 | nat_type, external_ip, external_port = get_ip_info() 260 | print "NAT Type:", nat_type 261 | print "External IP:", external_ip 262 | print "External Port:", external_port 263 | 264 | if __name__ == '__main__': 265 | main() 266 | --------------------------------------------------------------------------------