├── .gitignore ├── README.MD └── tunnel ├── dial-tone.wav ├── link_cable.py ├── modemClass.py ├── netlink.py ├── tunnel.py └── xband.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *.bat 5 | dreampi.py 6 | releases/ 7 | versions/ 8 | tunnel/errors_dumps/ 9 | connector_send_rcv.py 10 | sip_ring.py 11 | xband_config.py 12 | ring_test.bat 13 | functionality_tests/logs/ 14 | tunnel/femtosip/ 15 | tunnel/linkCable.py 16 | tunnel/linkCable.bat 17 | functionality_tests/ -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | An internet tunneler for connecting Netlink and Xband games 2 | 3 | The tunneler is included in the latest Dreamcast Live Dreampi releases. I do not directly maintain those releases so cannot guarantee functionality. 4 | 5 | Now included is link_cable.py which can be used to tunnel Dreamcast link cable games online using a Dreamcast "Coder's cable." This is still in early development and feedback is appreciated. 6 | 7 | modemClass.py is borrowed/lifted and slightly modified from the Dreampi script. Thanks to Kazade et al for creating that invaluable tool. 8 | 9 | [Original Dreampi Script](https://github.com/Kazade/dreampi) 10 | 11 | You should be able to use this for other generic modem to modem communications over the net. You will need to modify how the modem waiting for a call initializes. If you can modify the init string, appending "dt0" should cause that modem to dial out and wait for a "RING". See the README in the releases for how to call the waiting modem. 12 | -------------------------------------------------------------------------------- /tunnel/dial-tone.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/eaudunord/Netlink/909d6f49687008f91cae80ff30c891e49e91c4c3/tunnel/dial-tone.wav -------------------------------------------------------------------------------- /tunnel/link_cable.py: -------------------------------------------------------------------------------- 1 | import socket 2 | import time 3 | import serial 4 | import select 5 | import sys 6 | import threading 7 | from modemClass import Modem 8 | import logging 9 | import os 10 | 11 | osName = os.name 12 | logging.basicConfig(level=logging.INFO) 13 | logger = logging.getLogger('Tunnel') 14 | logger.setLevel(logging.INFO) 15 | pinging = True 16 | printout = False 17 | packetSplit = b"" 18 | dataSplit = b"" 19 | com_port = None 20 | speed = None 21 | ms = None 22 | dial_string = None 23 | ping = time.time() 24 | 25 | if sys.version_info < (3,0,0): 26 | input = raw_input 27 | 28 | def setup(com_port = com_port, speed = speed, ms = ms, dial_string = dial_string): 29 | for arg in sys.argv[1:]: 30 | if len(arg.split("=")[1].strip()) == 0: 31 | continue 32 | elif arg.split("=")[0] == "com": 33 | com_port = arg.split("=")[1].strip() 34 | elif arg.split("=")[0] == "speed": 35 | speed = int(arg.split("=")[1].strip()) 36 | elif arg.split("=")[0] == "state": 37 | ms = arg.split("=")[1].strip() 38 | elif arg.split("=")[0] == "address": 39 | dial_string = arg.split("=")[1].strip() 40 | 41 | while True: 42 | if not com_port: 43 | com_port = input("\nCOM port: ") 44 | try: 45 | testCon = serial.Serial(com_port,9600) 46 | testCon.close() 47 | break 48 | except serial.SerialException: 49 | print("Invalid COM port") 50 | com_port = None 51 | continue 52 | print("\nUsing %s" % com_port) 53 | 54 | while True: 55 | if speed: 56 | break 57 | speed = input("\nGame:\r\n[1] Aero Dancing F\r\n[2] Aero Dancing I\r\n[3] F355\r\n[4] Sega Tetris\r\n[5] Virtual On\r\n[6] Hell Gate\r\n[7] custom\r\n[8] calculated\r\n" 58 | ) 59 | if speed == '1': 60 | speed = 28800 61 | elif speed == '2': 62 | speed = 38400 63 | elif speed == '3': 64 | speed = 230400 65 | elif speed == '4': 66 | speed = 14400 67 | elif speed == '5': 68 | speed = 260416 69 | elif speed == '6': 70 | speed = 57600 71 | elif speed == '7': 72 | try: 73 | int(speed) 74 | speed = int(input("\ncustom baud: ")) 75 | except ValueError: 76 | print("Invalid selection") 77 | speed = None 78 | continue 79 | elif speed == '8': 80 | try: 81 | int(speed) 82 | multiplier = int(input("\nSCBRR2 multiplier: ")) 83 | speed = int(round((50*1000000)/(multiplier+1)/32,0)) 84 | print(speed) 85 | except ValueError: 86 | print("Invalid selection") 87 | speed = None 88 | continue 89 | else: 90 | print("invalid selection") 91 | speed = None 92 | continue 93 | 94 | while True: 95 | if ms: 96 | break 97 | side = input('\nWait or connect:\n[1] Wait\n[2] Connect\n') 98 | if side == '1' or side =='2': 99 | if side == '1': 100 | ms = "waiting" 101 | else: 102 | ms = "calling" 103 | else: 104 | print('Invalid selection') 105 | ms = None 106 | continue 107 | 108 | if not dial_string: 109 | dial_string = input("Opponent IP address: ") 110 | 111 | modem = Modem(com_port, speed,send_dial_tone=False) 112 | modem.connect_netlink(speed=speed,rtscts=True) 113 | print("setting serial rate to: %s" % speed) 114 | ser = modem._serial 115 | ser.reset_output_buffer() #flush the serial output buffer. It should be empty, but doesn't hurt. 116 | ser.reset_input_buffer() 117 | ser.timeout = None 118 | variables = ( 119 | com_port, 120 | speed, 121 | ms, 122 | dial_string, 123 | modem, 124 | ser 125 | ) 126 | return variables 127 | 128 | def initConnection(ms,dial_string): 129 | if dial_string: 130 | opponent = dial_string.replace('*','.') 131 | ip_set = opponent.split('.') 132 | for i,set in enumerate(ip_set): #socket connect doesn't like leading zeroes now 133 | fixed = str(int(set)) 134 | ip_set[i] = fixed 135 | opponent = ('.').join(ip_set) 136 | 137 | return ("connecting",opponent) 138 | 139 | 140 | 141 | def serial_exchange(side,state,opponent): 142 | 143 | def listener(): 144 | global ser 145 | global ping 146 | global state 147 | first_run = True 148 | lastPing = 0 149 | pong = time.time() 150 | startup = time.time() 151 | jitterStore = [] 152 | pingStore = [] 153 | currentSequence = 0 154 | maxPing = 0 155 | maxJitter = 0 156 | recoveredCount = 0 157 | if side == "waiting": 158 | oppPort = 21002 159 | 160 | if side == "calling": 161 | oppPort = 21001 162 | while(state != "netlink_disconnected"): 163 | ready = select.select([udp],[],[],0) #polling select 164 | if ready[0]: 165 | try: 166 | packetSet = udp.recv(1024) 167 | while time.time() - startup < 3: 168 | # Discard packets for 3 seconds in case there are any in the OS buffer. 169 | continue 170 | #start pinging code block 171 | if pinging == True: 172 | if packetSet == b'PING_SHIRO': 173 | udp.sendto(b'PONG_SHIRO', (opponent,oppPort)) 174 | continue 175 | elif packetSet == b'RESET_COUNT_SHIRO': 176 | # If peer reset their tunnel, we need to reset our sequence counter. 177 | print("Packet sequence reset") 178 | currentSequence = 0 179 | continue 180 | elif packetSet == b'PONG_SHIRO': 181 | if first_run: 182 | print("Connection established. Begin link play\r\n") 183 | udp.sendto(b'RESET_COUNT_SHIRO', (opponent,oppPort)) 184 | # we know there's a peer because it responded to our ping 185 | # tell it to reset its sequence counter 186 | first_run = False 187 | pong = time.time() 188 | pingResult = round((pong-ping)*1000,2) 189 | if pingResult > 500: 190 | continue 191 | if pingResult > maxPing: 192 | maxPing = pingResult 193 | pingStore.insert(0,pingResult) 194 | if len(pingStore) > 20: 195 | pingStore.pop() 196 | jitter = round(abs(pingResult-lastPing),2) 197 | if jitter > maxJitter: 198 | maxJitter = jitter 199 | jitterStore.insert(0,jitter) 200 | if len(jitterStore) >20: 201 | jitterStore.pop() 202 | jitterAvg = round(sum(jitterStore)/len(jitterStore),2) 203 | pingAvg = round(sum(pingStore)/len(pingStore),2) 204 | if osName != 'posix': 205 | sys.stdout.write('Ping: %s Max: %s | Jitter: %s Max: %s | Avg Ping: %s | Avg Jitter: %s | Recovered Packets: %s \r' % (pingResult,maxPing,jitter, maxJitter,pingAvg,jitterAvg,recoveredCount)) 206 | lastPing = pingResult 207 | continue 208 | #end pinging code block 209 | 210 | packets= packetSet.split(packetSplit) 211 | try: 212 | while True: 213 | packetNum = 0 214 | 215 | #go through all packets 216 | for p in packets: 217 | if int(p.split(dataSplit)[1]) == currentSequence: 218 | break 219 | packetNum += 1 220 | 221 | #if the packet needed is not here, grab the latest in the set 222 | if packetNum == len(packets): 223 | packetNum = 0 224 | if packetNum > 0 : 225 | recoveredCount += 1 226 | message = packets[packetNum] 227 | payload = message.split(dataSplit)[0] 228 | sequence = message.split(dataSplit)[1] 229 | if int(sequence) < currentSequence: 230 | break #All packets are old data, so drop it entirely 231 | currentSequence = int(sequence) + 1 232 | toSend = payload 233 | # logger.info(binascii.hexlify(payload)) 234 | ser.write(toSend) 235 | if packetNum == 0: # if the first packet was the processed packet, no need to go through the rest 236 | break 237 | 238 | except IndexError: 239 | continue 240 | except ConnectionResetError: 241 | continue 242 | 243 | logger.info("listener stopped") 244 | 245 | def sender(side,opponent): 246 | global ser 247 | global ping 248 | global state 249 | if side == "waiting": 250 | oppPort = 21002 251 | if side == "calling": 252 | oppPort = 21001 253 | sequence = 0 254 | packets = [] 255 | first_run = True 256 | 257 | while(state != "netlink_disconnected"): 258 | if time.time() - ping >= 5: 259 | try: 260 | udp.sendto(b'PING_SHIRO', (opponent,oppPort)) 261 | except ConnectionResetError: 262 | pass 263 | ping = time.time() 264 | raw_input = b'' 265 | if ser.in_waiting > 0: 266 | raw_input += ser.read(ser.in_waiting) 267 | if len(raw_input) > 0 and printout: 268 | print(raw_input) 269 | try: 270 | payload = raw_input 271 | seq = str(sequence) 272 | if len(payload) > 0: 273 | 274 | packets.insert(0,(payload+dataSplit+seq.encode())) 275 | if(len(packets) > 5): 276 | packets.pop() 277 | 278 | for i in range(1): #send the data twice. May help with drops or latency 279 | ready = select.select([],[udp],[]) #blocking select 280 | if ready[1]: 281 | udp.sendto(packetSplit.join(packets), (opponent,oppPort)) 282 | 283 | sequence+=1 284 | except Exception as e: 285 | print(e) 286 | 287 | continue 288 | try: 289 | udp.close() 290 | logger.info("sender stopped") 291 | except Exception as e: 292 | print(e) 293 | 294 | if state == "connecting": 295 | t1 = threading.Thread(target=listener) 296 | t2 = threading.Thread(target=sender,args=(side,opponent)) 297 | if side == "waiting": #we're going to bind to a port. Some users may want to run two instances on one machine, so use different ports for waiting, calling 298 | Port = 21001 299 | if side == "calling": 300 | Port = 21002 301 | udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 302 | udp.setsockopt(socket.IPPROTO_IP, socket.IP_TOS, 184) 303 | udp.setblocking(0) 304 | udp.bind(('', Port)) 305 | 306 | t1.start() 307 | t2.start() 308 | while t1.is_alive: 309 | t1.join(2) 310 | while t2.is_alive: 311 | t2.join(2) 312 | 313 | if __name__ == '__main__': 314 | try: 315 | com_port, speed, ms, dial_string, modem, ser = setup() 316 | state, opponent = initConnection(ms,dial_string) 317 | print(state,opponent) 318 | serial_exchange(ms,state,opponent) 319 | except KeyboardInterrupt: 320 | state = "netlink_disconnected" 321 | print('Interrupted') 322 | time.sleep(4) 323 | try: 324 | sys.exit(130) 325 | except SystemExit: 326 | os._exit(130) -------------------------------------------------------------------------------- /tunnel/modemClass.py: -------------------------------------------------------------------------------- 1 | #modemClass_version=202307212009 2 | import os 3 | import serial 4 | from datetime import datetime 5 | from datetime import timedelta 6 | import time 7 | 8 | 9 | 10 | class Modem(object): 11 | def __init__(self, device, speed, send_dial_tone=True): 12 | self._device, self._speed = device, speed 13 | self._serial = None 14 | self._sending_tone = False 15 | 16 | if send_dial_tone: 17 | self._dial_tone_wav = self._read_dial_tone() 18 | else: 19 | self._dial_tone_wav = None 20 | 21 | self._time_since_last_dial_tone = None 22 | self._dial_tone_counter = 0 23 | 24 | @property 25 | def device_speed(self): 26 | return self._speed 27 | 28 | @property 29 | def device_name(self): 30 | return self._device 31 | 32 | def _read_dial_tone(self): 33 | this_dir = os.path.dirname(os.path.abspath(os.path.realpath(__file__))) 34 | dial_tone_wav = os.path.join(this_dir, "dial-tone.wav") 35 | 36 | with open(dial_tone_wav, "rb") as f: 37 | dial_tone = f.read() # Read the entire wav file 38 | dial_tone = dial_tone[44:] # Strip the header (44 bytes) 39 | 40 | return dial_tone 41 | 42 | def connect(self): #blocking 43 | if self._serial: 44 | self.disconnect() 45 | 46 | print("Opening serial interface to {}".format(self._device)) 47 | self._serial = serial.Serial( 48 | self._device, self._speed, timeout=0 49 | ) 50 | def connect_netlink(self,speed = 115200, timeout = 0.01, rtscts = False): #non-blocking 51 | if self._serial: 52 | self.disconnect() 53 | print("Opening serial interface to {}".format(self._device)) 54 | self._serial = serial.Serial( 55 | self._device, speed, timeout=timeout, rtscts = rtscts 56 | ) 57 | 58 | def disconnect(self): 59 | if self._serial and self._serial.isOpen(): 60 | self._serial.flush() #added a flush, is data hanging on in the buffer? 61 | self._serial.close() 62 | self._serial = None 63 | # print("Serial interface terminated") 64 | 65 | def reset(self): 66 | while True: 67 | try: 68 | self.send_command("ATZ0",timeout=3) # Send reset command 69 | time.sleep(1) 70 | self.send_command("AT&F0") 71 | self.send_command("ATE0W2") # Don't echo our responses 72 | return 73 | except IOError: 74 | self.shake_it_off() # modem isn't responding. Try a harder reset 75 | 76 | def start_dial_tone(self): 77 | if not self._dial_tone_wav: 78 | return 79 | 80 | i = 0 81 | while i < 3: 82 | try: 83 | self.reset() 84 | self.send_command(b"AT+FCLASS=8") # Enter voice mode 85 | self.send_command(b"AT+VLS=1") # Go off-hook 86 | self.send_command(b"AT+VSM=1,8000") # 8 bit unsigned PCM 87 | self.send_command(b"AT+VTX") # Voice transmission mode 88 | print("") 89 | break 90 | except IOError: 91 | time.sleep(0.5) 92 | i+=1 93 | pass 94 | 95 | self._sending_tone = True 96 | 97 | self._time_since_last_dial_tone = ( 98 | datetime.now() - timedelta(seconds=100) 99 | ) 100 | 101 | self._dial_tone_counter = 0 102 | 103 | def stop_dial_tone(self): 104 | if not self._sending_tone: 105 | return 106 | 107 | self._serial.write(("\0{}{}\r\n".format(chr(0x10), chr(0x03))).encode()) 108 | self.send_escape() 109 | self.send_command("ATH0") # Go on-hook 110 | self.reset() # Reset the modem 111 | self._sending_tone = False 112 | 113 | def answer(self): 114 | self.reset() 115 | # When we send ATA we only want to look for CONNECT. Some modems respond OK then CONNECT 116 | # and that messes everything up 117 | self.send_command("ATA", ignore_responses=["OK"]) 118 | #time.sleep(5) 119 | print("Call answered!") 120 | # logger.info(subprocess.check_output(["pon", "dreamcast"])) 121 | print("Connected") 122 | 123 | def query_modem(self, command, timeout=3, response = "OK"): #this function assumes we're being passed a non-blocking modem 124 | 125 | if isinstance(command, bytes): 126 | final_command = command + b'\r\n' 127 | else: 128 | final_command = ("%s\r\n" % command).encode() 129 | self._serial.write(final_command) 130 | print('Command: %s' % final_command.decode()) 131 | 132 | start = time.time() 133 | 134 | line = b"" 135 | while True: 136 | new_data = self._serial.readline().strip() 137 | 138 | if not new_data: #non-blocking modem will end up here when timeout reached, try until this function's timeout is reached. 139 | if time.time() - start < timeout: 140 | continue 141 | raise IOError("There was a timeout while waiting for a response from the modem") 142 | 143 | line = line + new_data 144 | 145 | if response.encode() in line: 146 | if response != "OK": 147 | print('Response: %s' % line.decode()) 148 | return # Valid response 149 | 150 | 151 | def send_command(self, command, timeout=60, ignore_responses=None): 152 | 153 | ignore_responses = ignore_responses or [] # Things to completely ignore 154 | 155 | VALID_RESPONSES = [b"OK", b"ERROR", b"CONNECT", b"VCON"] 156 | 157 | for ignore in ignore_responses: 158 | VALID_RESPONSES.remove(ignore.encode()) 159 | 160 | if isinstance(command, bytes): 161 | final_command = command + b'\r\n' 162 | else: 163 | final_command = ("%s\r\n" % command).encode() 164 | 165 | self._serial.write(final_command) 166 | print('Command: %s' % final_command.decode()) 167 | 168 | start = time.time() 169 | line = b"" 170 | while True: 171 | new_data = self._serial.readline().strip() 172 | 173 | if not new_data: 174 | if time.time() - start < timeout: 175 | continue 176 | raise IOError("There was a timeout while waiting for a response from the modem") 177 | 178 | line = line + new_data 179 | for resp in VALID_RESPONSES: 180 | if resp in line: 181 | if resp != b"OK": 182 | print('Response: %s' % line.decode()) 183 | if resp == b"ERROR": 184 | raise IOError("Command returned an error") 185 | 186 | # logger.info(line[line.find(resp):]) 187 | return # We are done 188 | 189 | 190 | def send_escape(self): 191 | time.sleep(1.0) 192 | self._serial.write(b"+++") 193 | time.sleep(1.0) 194 | 195 | def shake_it_off(self): #sometimes the modem gets stuck in data mode 196 | for i in range(3): 197 | self._serial.write(b'+') 198 | time.sleep(0.2) 199 | time.sleep(4) 200 | self.send_command('ATH0') #make sure we're on hook 201 | print("Shook it off") 202 | 203 | def update(self): 204 | now = datetime.now() 205 | if self._sending_tone: 206 | # Keep sending dial tone 207 | BUFFER_LENGTH = 1000 208 | TIME_BETWEEN_UPLOADS_MS = (1000.0 / 8000.0) * BUFFER_LENGTH 209 | 210 | milliseconds = (now - self._time_since_last_dial_tone).microseconds * 1000 211 | if not self._time_since_last_dial_tone or milliseconds >= TIME_BETWEEN_UPLOADS_MS: 212 | byte = self._dial_tone_wav[self._dial_tone_counter:self._dial_tone_counter+BUFFER_LENGTH] 213 | self._dial_tone_counter += BUFFER_LENGTH 214 | if self._dial_tone_counter >= len(self._dial_tone_wav): 215 | self._dial_tone_counter = 0 216 | self._serial.write(byte) 217 | self._time_since_last_dial_tone = now -------------------------------------------------------------------------------- /tunnel/netlink.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Thu May 19 08:01:31 2022 4 | 5 | @author: joe 6 | """ 7 | #netlink_version=202306041400 8 | import sys 9 | 10 | if __name__ == "__main__": 11 | print("This script should not be run on its own") 12 | sys.exit() 13 | 14 | import socket 15 | import time 16 | import serial 17 | from datetime import datetime 18 | import logging 19 | import threading 20 | import binascii 21 | import select 22 | import os 23 | import platform 24 | 25 | pythonVer = platform.python_version_tuple()[0] 26 | osName = os.name 27 | if osName == 'posix': 28 | logger = logging.getLogger('dreampi') 29 | else: 30 | logger = logging.getLogger('Netlink') 31 | logger.setLevel(logging.INFO) 32 | 33 | pinging = True 34 | 35 | packetSplit = b"" 36 | dataSplit = b"" 37 | printout = False 38 | if 'printout' in sys.argv: 39 | printout = True 40 | timeout = 0.003 41 | data = [] 42 | state = "starting" 43 | poll_rate = 0.01 44 | ser = "" 45 | 46 | def digit_parser(modem): 47 | char = modem._serial.read(1).decode() #first character was , what's next? 48 | tel_digits = ['0','1', '2', '3', '4', '5', '6', '7', '8', '9'] 49 | ip_digits = ['0','1', '2', '3', '4', '5', '6', '7', '8', '9','*'] 50 | if char in tel_digits: 51 | dial_string = char 52 | last_heard = time.time() 53 | while(True): 54 | if time.time() - last_heard > 3: #if more than 3 seconds of silence, assume done dialing. 55 | break 56 | char = modem._serial.read(1).decode() 57 | if not char: 58 | continue 59 | if ord(char) == 16: 60 | try: 61 | char = modem._serial.read(1).decode() 62 | digit = int(char) #will raise exception if anything but a digit 63 | dial_string+= str(digit) 64 | last_heard = time.time() 65 | except (TypeError, ValueError): 66 | pass 67 | #at this point we have the full dialed string. We can insert an IP address lookup here. For now, assume PPP 68 | if dial_string == "0": 69 | return {'client':'direct_dial','dial_string':dial_string,'side':'waiting'} 70 | if dial_string == "70": 71 | logger.info("Call waiting disabled") 72 | return "nada" 73 | else: 74 | return {'client':'ppp_internet','dial_string':dial_string,'side':'na'} 75 | 76 | elif char == '#': 77 | dial_string = "" 78 | last_heard = time.time() 79 | while (True): 80 | if time.time() - last_heard > 3: #if more than 3 seconds of silence, assume done dialing. 81 | break 82 | char = modem._serial.read(1).decode() #modem sends s at regular intervals to indicate silence 83 | if not char: 84 | continue 85 | if ord(char) == 16: #16 is 86 | try: 87 | char = modem._serial.read(1).decode() 88 | if char == '#': 89 | if '*' in dial_string: #if the ip address was dialed with * no need for further formatting 90 | break 91 | elif len(dial_string) >= 12: #if we have a full 12 digit string add in '.' every three characters 92 | dial_string = '.'.join(dial_string[i:i+3] for i in range(0, len(dial_string), 3)) 93 | break 94 | if char in ip_digits: 95 | dial_string += char 96 | last_heard = time.time() 97 | except (TypeError, ValueError):#Dreampi originally tried to convert characters to int and passed on the exception raised for other characters. This shouldn't be needed anymore. 98 | pass 99 | return {'client':'direct_dial','dial_string':dial_string,'side':'calling'} 100 | else: 101 | return "nada" 102 | 103 | def initConnection(ms,dial_string): 104 | opponent = dial_string.replace('*','.') 105 | ip_set = opponent.split('.') 106 | for i,set in enumerate(ip_set): #socket connect doesn't like leading zeroes now 107 | fixed = str(int(set)) 108 | ip_set[i] = fixed 109 | opponent = ('.').join(ip_set) 110 | 111 | if ms == "waiting": 112 | logger.info("I'm waiting") 113 | timerStart = time.time() 114 | PORT = 65432 115 | tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 116 | tcp.settimeout(120) 117 | tcp.bind(('', PORT)) 118 | tcp.listen(5) 119 | while True: 120 | if time.time() - timerStart > 120: 121 | return ["failed",""] 122 | ready = select.select([tcp], [], [],0) 123 | if ready[0]: 124 | conn, addr = tcp.accept() 125 | opponent = addr[0] 126 | logger.info('connection from %s' % opponent) 127 | while True: 128 | try: 129 | data = conn.recv(1024) 130 | except socket.error: #first try can return no payload 131 | continue 132 | if data == b'readyip': 133 | conn.sendall(b'g2gip') 134 | logger.info("Sending Ring") 135 | ser.write(("RING\r\n").encode()) 136 | ser.write(("CONNECT\r\n").encode()) 137 | logger.info("Ready for Data Exchange!") 138 | #tcp.shutdown(socket.SHUT_RDWR) #best practice is to close your socket, but it gives me issues. 139 | #tcp.close() 140 | return ["connected",opponent] 141 | if not data: 142 | logger.info("failed to init") 143 | #tcp.shutdown(socket.SHUT_RDWR) 144 | #tcp.close() 145 | break 146 | if ms == "calling": 147 | logger.info("I'm calling") 148 | PORT = 65432 149 | tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 150 | tcp.settimeout(120) 151 | try: 152 | tcp.connect((opponent, PORT)) 153 | tcp.sendall(b"readyip") 154 | ready = select.select([tcp], [], []) 155 | if ready[0]: 156 | data = tcp.recv(1024) 157 | if data == b'g2gip': 158 | logger.info("Ready for Data Exchange!") 159 | #tcp.shutdown(socket.SHUT_RDWR) 160 | #tcp.close() 161 | return ["connected",opponent] 162 | except socket.error: 163 | return ["failed", ""] 164 | 165 | else: 166 | return ["error","error"] 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | def netlink_setup(side,dial_string,modem): 177 | global ser 178 | ser = modem._serial 179 | state = initConnection(side,dial_string) 180 | #time.sleep(0.2) 181 | return state 182 | 183 | def netlink_exchange(side,net_state,opponent,ser=ser): 184 | def listener(): 185 | logger.info(state) 186 | pingCount = 0 187 | lastPing = 0 188 | ping = time.time() 189 | pong = time.time() 190 | jitterStore = [] 191 | pingStore = [] 192 | currentSequence = 0 193 | maxPing = 0 194 | maxJitter = 0 195 | recoveredCount = 0 196 | if side == "waiting": 197 | oppPort = 20002 198 | if side == "calling": 199 | oppPort = 20001 200 | while(state != "netlink_disconnected"): 201 | ready = select.select([udp],[],[],0) #polling select 202 | if ready[0]: 203 | packetSet = udp.recv(1024) 204 | 205 | #start pinging code block 206 | if pinging == True: 207 | pingCount +=1 208 | if pingCount >= 30: 209 | pingCount = 0 210 | ping = time.time() 211 | udp.sendto(b'PING_SHIRO', (opponent,oppPort)) 212 | if packetSet == b'PING_SHIRO': 213 | udp.sendto(b'PONG_SHIRO', (opponent,oppPort)) 214 | continue 215 | elif packetSet == b'PONG_SHIRO': 216 | pong = time.time() 217 | pingResult = round((pong-ping)*1000,2) 218 | if pingResult > 500: 219 | continue 220 | if pingResult > maxPing: 221 | maxPing = pingResult 222 | pingStore.insert(0,pingResult) 223 | if len(pingStore) > 20: 224 | pingStore.pop() 225 | jitter = round(abs(pingResult-lastPing),2) 226 | if jitter > maxJitter: 227 | maxJitter = jitter 228 | jitterStore.insert(0,jitter) 229 | if len(jitterStore) >20: 230 | jitterStore.pop() 231 | jitterAvg = round(sum(jitterStore)/len(jitterStore),2) 232 | pingAvg = round(sum(pingStore)/len(pingStore),2) 233 | if osName != 'posix': 234 | sys.stdout.write('Ping: %s Max: %s | Jitter: %s Max: %s | Avg Ping: %s | Avg Jitter: %s | Recovered Packets: %s \r' % (pingResult,maxPing,jitter, maxJitter,pingAvg,jitterAvg,recoveredCount)) 235 | lastPing = pingResult 236 | continue 237 | #end pinging code block 238 | 239 | packets= packetSet.split(packetSplit) 240 | try: 241 | while True: 242 | packetNum = 0 243 | 244 | #go through all packets 245 | for p in packets: 246 | if int(p.split(dataSplit)[1]) == currentSequence: 247 | break 248 | packetNum += 1 249 | 250 | #if the packet needed is not here, grab the latest in the set 251 | if packetNum == len(packets): 252 | packetNum = 0 253 | if packetNum > 0 : 254 | recoveredCount += 1 255 | message = packets[packetNum] 256 | payload = message.split(dataSplit)[0] 257 | sequence = message.split(dataSplit)[1] 258 | if int(sequence) < currentSequence: 259 | break #All packets are old data, so drop it entirely 260 | 261 | currentSequence = int(sequence) + 1 262 | 263 | toSend = payload 264 | 265 | ser.write(toSend) 266 | if len(payload) > 0 and printout == True: 267 | logger.info(binascii.hexlify(payload)) 268 | if packetNum == 0: # if the first packet was the processed packet, no need to go through the rest 269 | break 270 | 271 | except IndexError: 272 | continue 273 | 274 | logger.info("listener stopped") 275 | 276 | def sender(side,opponent): 277 | global state 278 | logger.info("sending") 279 | first_run = False 280 | if side == "waiting": 281 | oppPort = 20002 282 | if side == "calling": 283 | oppPort = 20001 284 | last = 0 285 | sequence = 0 286 | packets = [] 287 | ser.timeout = None 288 | 289 | while(state != "netlink_disconnected"): 290 | new = ser.read(1) #should now block until data. Attempt to reduce CPU usage. 291 | raw_input = new + ser.read(ser.in_waiting) 292 | if b"NO CARRIER" in raw_input: 293 | print('') 294 | logger.info("NO CARRIER") 295 | # ser.write(("ATs86?\r\n").encode()) 296 | # response = ser.readline().strip() 297 | # if len(response) == 0: 298 | # response = ser.readline().strip() 299 | # print(response) 300 | state = "netlink_disconnected" 301 | time.sleep(1) 302 | udp.close() 303 | logger.info("sender stopped") 304 | return 305 | 306 | try: 307 | payload = raw_input 308 | seq = str(sequence) 309 | if len(payload)>0: 310 | 311 | packets.insert(0,(payload+dataSplit+seq.encode())) 312 | if(len(packets) > 5): 313 | packets.pop() 314 | 315 | for i in range(2): #send the data twice. May help with drops or latency 316 | ready = select.select([],[udp],[]) #blocking select 317 | if ready[1]: 318 | udp.sendto(packetSplit.join(packets), (opponent,oppPort)) 319 | 320 | sequence+=1 321 | except: 322 | continue 323 | 324 | global state 325 | state = net_state 326 | if state == "connected": 327 | t1 = threading.Thread(target=listener) 328 | t2 = threading.Thread(target=sender,args=(side,opponent)) 329 | if side == "waiting": #we're going to bind to a port. Some users may want to run two instances on one machine, so use different ports for waiting, calling 330 | Port = 20001 331 | if side == "calling": 332 | Port = 20002 333 | udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 334 | udp.setsockopt(socket.IPPROTO_IP, socket.IP_TOS, 184) 335 | udp.setblocking(0) 336 | udp.bind(('', Port)) 337 | 338 | t1.start() 339 | t2.start() 340 | t1.join() 341 | t2.join() 342 | 343 | def kddi_exchange(side,net_state,opponent,ser=ser): 344 | def listener(): 345 | logger.info(state) 346 | pingCount = 0 347 | lastPing = 0 348 | ping = time.time() 349 | pong = time.time() 350 | jitterStore = [] 351 | pingStore = [] 352 | currentSequence = 0 353 | maxPing = 0 354 | maxJitter = 0 355 | recoveredCount = 0 356 | firstRun = True 357 | lastWrite = None 358 | if side == "waiting": 359 | oppPort = 20002 360 | if side == "calling": 361 | oppPort = 20001 362 | while(state != "netlink_disconnected"): 363 | ready = select.select([udp],[],[],0) #polling select 364 | if ready[0]: 365 | packetSet = udp.recv(1024) 366 | 367 | #start pinging code block 368 | if pinging == True: 369 | pingCount +=1 370 | if pingCount >= 30: 371 | pingCount = 0 372 | ping = time.time() 373 | udp.sendto(b'PING_SHIRO', (opponent,oppPort)) 374 | if packetSet == b'PING_SHIRO': 375 | udp.sendto(b'PONG_SHIRO', (opponent,oppPort)) 376 | continue 377 | elif packetSet == b'PONG_SHIRO': 378 | pong = time.time() 379 | pingResult = round((pong-ping)*1000,2) 380 | if pingResult > 500: 381 | continue 382 | if pingResult > maxPing: 383 | maxPing = pingResult 384 | pingStore.insert(0,pingResult) 385 | if len(pingStore) > 20: 386 | pingStore.pop() 387 | jitter = round(abs(pingResult-lastPing),2) 388 | if jitter > maxJitter: 389 | maxJitter = jitter 390 | jitterStore.insert(0,jitter) 391 | if len(jitterStore) >20: 392 | jitterStore.pop() 393 | jitterAvg = round(sum(jitterStore)/len(jitterStore),2) 394 | pingAvg = round(sum(pingStore)/len(pingStore),2) 395 | if osName != 'posix': 396 | sys.stdout.write('Ping: %s Max: %s | Jitter: %s Max: %s | Avg Ping: %s | Avg Jitter: %s | Recovered Packets: %s \r' % (pingResult,maxPing,jitter, maxJitter,pingAvg,jitterAvg,recoveredCount)) 397 | lastPing = pingResult 398 | continue 399 | #end pinging code block 400 | 401 | packets= packetSet.split(packetSplit) 402 | try: 403 | while True: 404 | packetNum = 0 405 | 406 | #go through all packets 407 | for p in packets: 408 | if int(p.split(dataSplit)[1]) == currentSequence: 409 | break 410 | packetNum += 1 411 | 412 | #if the packet needed is not here, grab the latest in the set 413 | if packetNum == len(packets): 414 | packetNum = 0 415 | if packetNum > 0 : 416 | recoveredCount += 1 417 | message = packets[packetNum] 418 | payload = message.split(dataSplit)[0] 419 | sequence = message.split(dataSplit)[1] 420 | if int(sequence) < currentSequence: 421 | break #All packets are old data, so drop it entirely 422 | 423 | currentSequence = int(sequence) + 1 424 | 425 | toSend = payload 426 | 427 | if firstRun == True: 428 | if pythonVer == '2': 429 | lastWrite = time.clock() 430 | firstRun = False 431 | else: 432 | lastWrite = time.perf_counter() 433 | firstRun = False 434 | elif firstRun == False: 435 | if pythonVer == '2': 436 | if time.clock() - lastWrite > 0.026: 437 | logger.info('Late KDDI Packet') 438 | lastWrite = time.clock() 439 | else: 440 | if time.perf_counter() - lastWrite > 0.026: 441 | logger.info('Late KDDI Packet') 442 | lastWrite = time.perf_counter() 443 | 444 | ser.write(toSend) 445 | if len(payload) > 0 and printout == True: 446 | logger.info(binascii.hexlify(payload)) 447 | if packetNum == 0: # if the first packet was the processed packet, no need to go through the rest 448 | break 449 | 450 | except IndexError: 451 | continue 452 | 453 | logger.info("listener stopped") 454 | 455 | def sender(side,opponent): 456 | global state 457 | logger.info("sending") 458 | first_run = False 459 | if side == "waiting": 460 | oppPort = 20002 461 | if side == "calling": 462 | oppPort = 20001 463 | last = 0 464 | sequence = 0 465 | packets = [] 466 | ser.timeout = 0.5 467 | 468 | while(state != "netlink_disconnected"): 469 | if not ser.cd: 470 | print('') 471 | logger.info("NO CARRIER") 472 | ser.read(ser.in_waiting) 473 | state = "netlink_disconnected" 474 | time.sleep(1) 475 | udp.close() 476 | logger.info("sender stopped") 477 | return 478 | raw_input = b'' 479 | new = ser.read(1) 480 | if len(new) == 0: 481 | continue 482 | else: 483 | raw_input += new 484 | while len(raw_input) < 6: 485 | new = ser.read(1) 486 | if new == b'\x0a' or len(new) == 0: 487 | raw_input += new 488 | break 489 | raw_input += new 490 | 491 | 492 | try: 493 | payload = raw_input 494 | seq = str(sequence) 495 | if len(payload)>0: 496 | 497 | packets.insert(0,(payload+dataSplit+seq.encode())) 498 | if(len(packets) > 5): 499 | packets.pop() 500 | 501 | for i in range(2): #send the data twice. May help with drops or latency 502 | ready = select.select([],[udp],[]) #blocking select 503 | if ready[1]: 504 | udp.sendto(packetSplit.join(packets), (opponent,oppPort)) 505 | 506 | sequence+=1 507 | except: 508 | continue 509 | 510 | global state 511 | state = net_state 512 | if state == "connected": 513 | t1 = threading.Thread(target=listener) 514 | t2 = threading.Thread(target=sender,args=(side,opponent)) 515 | if side == "waiting": #we're going to bind to a port. Some users may want to run two instances on one machine, so use different ports for waiting, calling 516 | Port = 20001 517 | if side == "calling": 518 | Port = 20002 519 | udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 520 | udp.setsockopt(socket.IPPROTO_IP, socket.IP_TOS, 184) 521 | udp.setblocking(0) 522 | udp.bind(('', Port)) 523 | 524 | t1.start() 525 | t2.start() 526 | t1.join() 527 | t2.join() 528 | 529 | 530 | 531 | -------------------------------------------------------------------------------- /tunnel/tunnel.py: -------------------------------------------------------------------------------- 1 | #tunnel_version=202401151537 2 | import sys 3 | import os 4 | from datetime import datetime 5 | import logging 6 | import time 7 | logging.basicConfig(level=logging.INFO) 8 | import serial 9 | import requests 10 | import platform 11 | com_port = None 12 | logger = logging.getLogger('Netlink') 13 | 14 | 15 | 16 | def updater(): 17 | base_script_url = "https://raw.githubusercontent.com/eaudunord/Netlink/latest/tunnel/" 18 | checkScripts = ['modemClass.py','tunnel.py','netlink.py','xband.py'] 19 | restartFlag = False 20 | for script in checkScripts: 21 | url = base_script_url+script 22 | try: 23 | r=requests.get(url, stream = True) 24 | r.raise_for_status() 25 | for line in r.iter_lines(): 26 | if b'_version' in line: 27 | upstream_version = str(line.decode().split('version=')[1]).strip() 28 | break 29 | local_script = os.path.realpath('./') + "/" +script 30 | if os.path.isfile(local_script) == False: 31 | local_version = None 32 | else: 33 | with open(script,'rb') as f: 34 | for line in f: 35 | if b'_version' in line: 36 | local_version = str(line.decode().split('version=')[1]).strip() 37 | break 38 | if upstream_version == local_version: 39 | print('%s Up To Date' % script) 40 | else: 41 | optIn = "no" 42 | pythonVer = platform.python_version_tuple()[0] 43 | if pythonVer == '2': 44 | optIn = raw_input('Update for %s available. Press enter to download or type no to skip >>' % script) 45 | else: 46 | optIn = input('Update for %s available. Press enter to download or type no to skip >>' % script) 47 | if "no" in optIn.lower(): 48 | continue 49 | #make a handler for a bad request so bad data doesn't overwrite our local file 50 | r = requests.get(url) 51 | r.raise_for_status() 52 | with open(script,'wb') as f: 53 | f.write(r.content) 54 | print('%s Updated' % script) 55 | if script == "tunnel.py": 56 | restartFlag = True 57 | 58 | except requests.exceptions.HTTPError: 59 | logger.info("Couldn't check updates for: %s" % script) 60 | continue 61 | if restartFlag: 62 | print('Main script updated. Please restart the tunnel') 63 | sys.exit() 64 | 65 | if 'noUpdate' in sys.argv: 66 | print("updates disabled") 67 | else: 68 | updater() 69 | 70 | import netlink 71 | from modemClass import Modem 72 | import xband 73 | 74 | 75 | def com_scanner(): 76 | global com_port 77 | speed = 115200 78 | for i in range(1,25): # this should be a big enough range. USB com ports usually end up in the teens. 79 | osName = os.name 80 | if osName == 'posix': # should work on linux and Mac for USB modem, but untested. 81 | com_port = "/dev/ttyACM%s" % (i-1) 82 | else: 83 | com_port = "COM%s" % i 84 | try: 85 | modem = Modem(com_port, speed,send_dial_tone=False) 86 | modem.connect_netlink() 87 | #print("potential modem found at %s" % com) 88 | modem.query_modem("AT",timeout = 1) # potential modem. Other devices respond to AT, so not definitive. 89 | modem.query_modem("AT+FCLASS=8",timeout = 1) # if potential modem, find out if it's our voice modem 90 | modem.reset() 91 | return com_port 92 | except serial.SerialException as e: 93 | message = str(e) 94 | # print(message) 95 | if "could not open port" in message: 96 | com_port = None 97 | except IOError as e: 98 | com_port = None 99 | finally: 100 | modem.disconnect() 101 | 102 | try: 103 | com_port = sys.argv[1] #script can be started with com port as an argument. If it isn't, we can scan for the modem. 104 | if not ('com' in com_port.lower() or '/dev' in com_port.lower()): 105 | raise IndexError 106 | except IndexError: 107 | com_scanner() 108 | if com_port: 109 | print("Modem found at %s" % com_port) 110 | else: 111 | print("No modem found") 112 | sys.exit() 113 | 114 | 115 | device_and_speed = [com_port,115200] 116 | modem = Modem(device_and_speed[0], device_and_speed[1]) 117 | 118 | 119 | def do_netlink(side,dial_string,modem,saturn=True): 120 | # ser = serial.Serial(device_and_speed[0], device_and_speed[1], timeout=0.02) 121 | state, opponent = netlink.netlink_setup(side,dial_string,modem) 122 | if state == "failed": 123 | for i in range(3): 124 | modem._serial.write(b'+') 125 | time.sleep(0.2) 126 | time.sleep(4) 127 | modem.send_command('ATH0') 128 | return 129 | if saturn == False: 130 | netlink.kddi_exchange(side,state,opponent,ser=modem._serial) 131 | else: 132 | netlink.netlink_exchange(side,state,opponent,ser=modem._serial) 133 | 134 | 135 | def process(): 136 | xbandnums = ["18002071194","19209492263","0120717360","0355703001"] 137 | 138 | xbandMatching = False 139 | xbandTimer = None 140 | xbandInit = False 141 | openXband = False 142 | 143 | mode = "LISTENING" 144 | 145 | modem.connect() 146 | modem.start_dial_tone() 147 | 148 | time_digit_heard = None 149 | saturn = True 150 | while True: 151 | 152 | now = datetime.now() 153 | 154 | if mode == "LISTENING": 155 | 156 | if xbandMatching == True: 157 | if xbandInit == False: 158 | xband.xbandInit() 159 | xbandInit = True 160 | if time.time() - xbandTimer > 900: 161 | xbandMatching = False 162 | xband.closeXband() 163 | openXband = False 164 | continue 165 | if openXband == False: 166 | xband.openXband() 167 | openXband = True 168 | xbandResult,opponent = xband.xbandListen(modem) 169 | if xbandResult == "connected": 170 | xband.netlink_exchange("waiting","connected",opponent,ser=modem._serial) 171 | logger.info("Xband Disconnected") 172 | mode = "LISTENING" 173 | modem.connect() 174 | modem.start_dial_tone() 175 | xbandMatching = False 176 | xband.closeXband() 177 | openXband = False 178 | 179 | modem.update() 180 | char = modem._serial.read(1).strip().decode() 181 | if not char: 182 | continue 183 | 184 | if ord(char) == 16: 185 | # DLE character 186 | try: 187 | parsed = netlink.digit_parser(modem) 188 | if parsed == "nada": 189 | pass 190 | elif isinstance(parsed,dict): 191 | client = parsed['client'] 192 | dial_string = parsed['dial_string'] 193 | side = parsed['side'] 194 | logger.info("Heard: %s" % dial_string) 195 | 196 | if dial_string in xbandnums: 197 | logger.info("Incoming call from Xband") 198 | client = "xband" 199 | mode = "XBAND ANSWERING" 200 | 201 | elif dial_string == "00": 202 | side = "waiting" 203 | client = "direct_dial" 204 | 205 | elif dial_string[0:3] == "859": 206 | try: 207 | kddi_opponent = dial_string 208 | kddi_lookup = "https://dial.redreamcast.net/?phoneNumber=%s" % kddi_opponent 209 | response = requests.get(kddi_lookup) 210 | response.raise_for_status() 211 | ip = response.text 212 | if len(ip) == 0: 213 | pass 214 | else: 215 | dial_string = ip 216 | logger.info(dial_string) 217 | saturn = False 218 | side = "calling" 219 | client = "direct_dial" 220 | time.sleep(7) 221 | except requests.exceptions.HTTPError: 222 | pass 223 | 224 | elif len(dial_string.split('*')) == 5 and dial_string.split('*')[-1] == "1": 225 | oppIP = '.'.join(dial_string.split('*')[0:4]) 226 | client = "xband" 227 | mode = "NETLINK ANSWERING" 228 | side = "calling" 229 | 230 | if client == "direct_dial": 231 | mode = "NETLINK ANSWERING" 232 | elif client == "xband": 233 | pass 234 | else: 235 | mode = "ANSWERING" 236 | modem.stop_dial_tone() 237 | time_digit_heard = now 238 | except (TypeError, ValueError): 239 | pass 240 | 241 | elif mode == "XBAND ANSWERING": 242 | # print("xband answering") 243 | if (now - time_digit_heard).total_seconds() > 8.0: 244 | time_digit_heard = None 245 | modem.query_modem("ATA", timeout=120, response = "CONNECT") 246 | xband.xbandServer(modem) 247 | mode = "LISTENING" 248 | modem.connect() 249 | modem.start_dial_tone() 250 | xbandMatching = True 251 | xbandTimer = time.time() 252 | 253 | elif mode == "ANSWERING": 254 | if (now - time_digit_heard).total_seconds() > 8.0: 255 | time_digit_heard = None 256 | modem.answer() 257 | modem.disconnect() 258 | mode = "CONNECTED" 259 | 260 | elif mode == "NETLINK ANSWERING": 261 | if (now - time_digit_heard).total_seconds() > 8.0: 262 | time_digit_heard = None 263 | 264 | try: 265 | if client == "xband": 266 | xband.init_xband(modem) 267 | result = xband.ringPhone(oppIP,modem) 268 | if result == "hangup": 269 | mode = "LISTENING" 270 | modem.connect() 271 | modem.start_dial_tone() 272 | else: 273 | mode = "NETLINK_CONNECTED" 274 | else: 275 | modem.connect_netlink(speed=57600,timeout=0.01,rtscts = True) #non-blocking version 276 | modem.query_modem(b"AT%E0\V1") 277 | if saturn: 278 | modem.query_modem(b'AT%C0\N3') 279 | modem.query_modem(b'AT+MS=V32b,1,14400,14400,14400,14400') 280 | modem.query_modem(b"ATA", timeout=120, response = "CONNECT") 281 | mode = "NETLINK_CONNECTED" 282 | except IOError: 283 | modem.connect() 284 | mode = "LISTENING" 285 | modem.start_dial_tone() 286 | 287 | 288 | elif mode == "CONNECTED": 289 | modem.connect() 290 | modem.send_escape() 291 | modem.start_dial_tone() 292 | mode = "LISTENING" 293 | 294 | 295 | elif mode == "NETLINK_CONNECTED": 296 | if client == "xband": 297 | xband.netlink_exchange("calling","connected",oppIP,ser=modem._serial) 298 | else: 299 | do_netlink(side,dial_string,modem,saturn=saturn) 300 | logger.info("Netlink Disconnected") 301 | # time.sleep(5) 302 | mode = "LISTENING" 303 | modem.connect() 304 | modem.start_dial_tone() 305 | 306 | if __name__ == "__main__": 307 | process() 308 | 309 | -------------------------------------------------------------------------------- /tunnel/xband.py: -------------------------------------------------------------------------------- 1 | #xband_version=202306132113 2 | import sys 3 | 4 | if __name__ == "__main__": 5 | print("This script should not be run on its own") 6 | sys.exit() 7 | 8 | import socket 9 | import time 10 | from datetime import datetime 11 | import logging 12 | import select 13 | import os 14 | import requests 15 | import subprocess 16 | import errno 17 | import threading 18 | 19 | ser = None 20 | osName = os.name 21 | if osName == 'posix': 22 | logger = logging.getLogger('dreampi') 23 | else: 24 | logger = logging.getLogger('Xband') 25 | logger.setLevel(logging.INFO) 26 | 27 | opponent_port = 4000 28 | opponent_id = "11" 29 | sock_listen = None 30 | my_ip = "127.0.0.1" 31 | 32 | def getWanIP(): 33 | try: 34 | r = requests.get("http://myipv4.p1.opendns.com/get_my_ip") 35 | r.raise_for_status() 36 | my_ip = r.json()['ip'] 37 | except requests.exceptions.HTTPError: 38 | logger.info("Couldn't get WAN IP") 39 | my_ip = "127.0.0.1" 40 | except requests.exceptions.SSLError: 41 | logger.info("Couldn't get WAN IP") 42 | my_ip = "127.0.0.1" 43 | return my_ip 44 | 45 | 46 | if osName == 'posix': # should work on linux and Mac for USB modem, but untested. 47 | femtoSipPath = "/home/pi/dreampi/femtosip" 48 | else: 49 | femtoSipPath = os.path.realpath('./')+"/femtosip" 50 | 51 | def openXband(): 52 | PORT = 65433 53 | global sock_listen 54 | sock_listen = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 55 | sock_listen.setblocking(0) 56 | sock_listen.bind(('', PORT)) 57 | sock_listen.listen(5) 58 | logger.info("listening for xband call") 59 | 60 | def closeXband(): 61 | global sock_listen 62 | try: 63 | sock_listen.close() 64 | except: 65 | pass 66 | 67 | 68 | 69 | def xbandInit(): 70 | if os.path.exists(femtoSipPath) == False: 71 | try: 72 | os.makedirs(femtoSipPath) 73 | r = requests.get("https://raw.githubusercontent.com/eaudunord/femtosip/master/femtosip.py") 74 | r.raise_for_status() 75 | with open(femtoSipPath+"/femtosip.py",'wb') as f: 76 | text = r.content.decode('ascii','ignore').encode() 77 | f.write(text) 78 | logger.info('fetched femtosip') 79 | r = requests.get("https://github.com/astoeckel/femtosip/raw/master/LICENSE") 80 | r.raise_for_status() 81 | with open(femtoSipPath+"/LICENSE",'wb') as f: 82 | f.write(r.content) 83 | logger.info('fetched LICENSE') 84 | with open(femtoSipPath+"/__init__.py",'wb') as f: 85 | pass 86 | except requests.exceptions.HTTPError: 87 | logger.info("unable to fetch femtosip") 88 | return "dropped" 89 | except OSError: 90 | logger.info("error creating femtosip directory") 91 | else: 92 | global sip_ring 93 | import femtosip.femtosip as sip_ring 94 | 95 | def xbandListen(modem): 96 | global sock_listen 97 | ready = select.select([sock_listen], [], [],0) 98 | if ready[0]: 99 | logger.info("incoming xband call") 100 | conn, addr = sock_listen.accept() 101 | opponent = addr[0] 102 | callTime = time.time() 103 | while True: 104 | ready = select.select([conn], [], [],0) 105 | if ready[0]: 106 | data = conn.recv(1024) 107 | if data == b"RESET": 108 | modem.stop_dial_tone() 109 | init_xband(modem) 110 | # modem.connect_netlink(speed=57600,timeout=0.05,rtscts=True) 111 | # modem.query_modem(b'AT%E0') 112 | # modem.query_modem(b"AT\V1%C0") 113 | # modem.query_modem(b'AT+MS=V22b') 114 | conn.sendall(b'ACK RESET') 115 | # time.sleep(2) 116 | elif data == b"RING": 117 | logger.info("RING") 118 | # time.sleep(4) 119 | conn.sendall(b'ANSWERING') 120 | time.sleep(6) 121 | logger.info('Answering') 122 | modem.query_modem("ATX1D", timeout=120, response = "CONNECT") 123 | logger.info("CONNECTED") 124 | elif data == b"PING": 125 | conn.sendall(b'ACK PING') 126 | modem._serial.timeout=None 127 | modem._serial.write(b'\xff') 128 | while True: 129 | char = modem._serial.read(1) #read through the buffer and skip all 0xff 130 | if char == b'\xff': 131 | continue 132 | elif char == b'\x01': 133 | # modem._serial.write(b'\x01') 134 | conn.sendall(b'RESPONSE') 135 | logger.info('got a response') 136 | break 137 | if modem._serial.cd: #if we stayed connected 138 | continue 139 | 140 | elif not modem._serial.cd: #if we dropped the call 141 | logger.info("Xband Disconnected") 142 | # mode = "LISTENING" 143 | modem.connect() 144 | modem.start_dial_tone() 145 | return ("dropped","") 146 | 147 | elif data == b'RESPONSE': 148 | modem._serial.write(b'\x01') 149 | if modem._serial.cd: 150 | return ("connected",opponent) 151 | if time.time() - callTime > 120: 152 | break 153 | return ("nothing","") 154 | 155 | def ringPhone(oppIP,modem): 156 | import femtosip.femtosip as sip_ring 157 | opponent = oppIP 158 | PORT = 65433 159 | sock_send = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 160 | sock_send.settimeout(15) 161 | logger.info("Calling opponent") 162 | # time.sleep(8) 163 | 164 | # sip = femtosip.SIP(user, password, gateway, port, display_name) 165 | # sip.call(call, delay) 166 | 167 | try: 168 | sock_send.connect((opponent, PORT)) 169 | sock_send.sendall(b"RESET") 170 | sentCall = time.time() 171 | while True: 172 | ready = select.select([sock_send], [], [],0) 173 | if ready[0]: 174 | data = sock_send.recv(1024) 175 | if data == b'ACK RESET': 176 | my_ip = getWanIP() 177 | sip = sip_ring.SIP('user','',opponent,opponent_port,local_ip = my_ip,protocol="udp") 178 | sip.call(opponent_id,3) 179 | sock_send.sendall(b'RING') 180 | elif data == b'ANSWERING': 181 | logger.info("Answering") 182 | modem.query_modem("ATA", timeout=120, response = "CONNECT") 183 | logger.info("CONNECTED") 184 | sock_send.sendall(b'PING') 185 | 186 | elif data == b"ACK PING": 187 | modem._serial.timeout=None 188 | modem._serial.write(b'\xff') 189 | while True: 190 | char = modem._serial.read(1) #read through the buffer and skip all 0xff 191 | if char == b'\xff': 192 | continue 193 | elif char == b'\x01': 194 | # modem._serial.write(b'\x01') 195 | logger.info("got a response") 196 | sock_send.sendall(b'RESPONSE') 197 | break 198 | if modem._serial.cd: #if we stayed connected 199 | continue 200 | 201 | elif not modem._serial.cd: #if we dropped the call 202 | return "hangup" 203 | 204 | elif data == b'RESPONSE': 205 | modem._serial.write(b'\x01') 206 | return opponent 207 | if time.time() - sentCall > 90: 208 | logger.info("opponent tunnel not responding") 209 | return "hangup" 210 | 211 | 212 | except socket.error: 213 | logger.info("couldn't connect to opponent") 214 | return "hangup" 215 | 216 | def getserial(): 217 | cpuserial = b"0000000000000000" 218 | if osName == 'posix': 219 | try: 220 | f = open('/proc/cpuinfo','r') 221 | for line in f: 222 | if line[0:6]=='Serial': 223 | cpuserial = line[10:26].encode() 224 | f.close() 225 | logger.info("Found valid CPU ID") 226 | except: 227 | cpuserial = b"ERROR000000000" 228 | logger.info("Couldn't find valid CPU ID, using error ID") 229 | else: 230 | cpuserial = subprocess.check_output(["wmic","cpu","get","ProcessorId","/format:csv"]).strip().split(b",")[-1] 231 | logger.info("Found valid CPU ID") 232 | return cpuserial 233 | 234 | def xbandServer(modem): 235 | modem._serial.timeout = 1 236 | logger.info("connecting to retrocomputing.network") 237 | s = socket.socket() 238 | s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) 239 | s.setblocking(False) 240 | s.settimeout(15) 241 | s.connect(("xbserver.retrocomputing.network", 56969)) 242 | # cpu = subprocess.check_output(["wmic","cpu","get","ProcessorId","/format:csv"]).strip().split(b",")[-1] 243 | hwid = getserial() 244 | sdata = b"///////PI-" + hwid + b"\x0a" 245 | sentid = 0 246 | logger.info("connected") 247 | while True: 248 | try: 249 | ready = select.select([s], [], [],0.3) 250 | if ready[0]: 251 | data = s.recv(1024) 252 | # print(data) 253 | modem._serial.write(data) 254 | if sentid == 0: 255 | s.send(sdata) 256 | sentid = 1 257 | except socket.error as e: 258 | err = e.args[0] 259 | if err == errno.EAGAIN or err == errno.EWOULDBLOCK: 260 | time.sleep(0.1) 261 | else: 262 | logger.warn("tcp connection dropped") 263 | break 264 | if not modem._serial.cd: 265 | logger.info("1: CD is not asserted") 266 | time.sleep(2.0) 267 | if not modem._serial.cd: 268 | logger.info("CD still not asserted after 2 sec - xband hung up") 269 | break 270 | if sentid == 1: 271 | if modem._serial.in_waiting: 272 | line = b"" 273 | while True: 274 | data2 = modem._serial.read(1) 275 | line += data2 276 | if b"\x10\x03" in line: 277 | # print(line) 278 | s.send(line) 279 | break 280 | if not modem._serial.cd: 281 | logger.info("2: CD is not asserted") 282 | time.sleep(2.0) 283 | if not modem._serial.cd: 284 | logger.info("CD still not asserted after 2 sec - xband hung up") 285 | break 286 | s.close() 287 | logger.info("Xband disconnected. Back to listening") 288 | return 289 | 290 | def netlink_exchange(side,net_state,opponent,ser=ser): 291 | packetSplit = b"" 292 | dataSplit = b"" 293 | def listener(): 294 | logger.info(state) 295 | pingCount = 0 296 | lastPing = 0 297 | ping = time.time() 298 | pong = time.time() 299 | jitterStore = [] 300 | pingStore = [] 301 | currentSequence = 0 302 | maxPing = 0 303 | maxJitter = 0 304 | recoveredCount = 0 305 | first = True 306 | if side == "waiting": 307 | oppPort = 20002 308 | if side == "calling": 309 | oppPort = 20001 310 | while(state != "netlink_disconnected"): 311 | ready = select.select([udp],[],[],0) #polling select 312 | if ready[0]: 313 | # if first == True: 314 | # time.sleep(0.01) 315 | # first = False 316 | packetSet = udp.recv(1024) 317 | 318 | #start pinging code block 319 | # if pinging == True: 320 | # pingCount +=1 321 | # if pingCount >= 30: 322 | # pingCount = 0 323 | # ping = time.time() 324 | # udp.sendto(b'PING_SHIRO', (opponent,oppPort)) 325 | # if packetSet == b'PING_SHIRO': 326 | # udp.sendto(b'PONG_SHIRO', (opponent,oppPort)) 327 | # continue 328 | # elif packetSet == b'PONG_SHIRO': 329 | # pong = time.time() 330 | # pingResult = round((pong-ping)*1000,2) 331 | # if pingResult > 500: 332 | # continue 333 | # if pingResult > maxPing: 334 | # maxPing = pingResult 335 | # pingStore.insert(0,pingResult) 336 | # if len(pingStore) > 20: 337 | # pingStore.pop() 338 | # jitter = round(abs(pingResult-lastPing),2) 339 | # if jitter > maxJitter: 340 | # maxJitter = jitter 341 | # jitterStore.insert(0,jitter) 342 | # if len(jitterStore) >20: 343 | # jitterStore.pop() 344 | # jitterAvg = round(sum(jitterStore)/len(jitterStore),2) 345 | # pingAvg = round(sum(pingStore)/len(pingStore),2) 346 | # if osName != 'posix': 347 | # sys.stdout.write('Ping: %s Max: %s | Jitter: %s Max: %s | Avg Ping: %s | Avg Jitter: %s | Recovered Packets: %s \r' % (pingResult,maxPing,jitter, maxJitter,pingAvg,jitterAvg,recoveredCount)) 348 | # lastPing = pingResult 349 | # continue 350 | #end pinging code block 351 | 352 | packets= packetSet.split(packetSplit) 353 | try: 354 | while True: 355 | packetNum = 0 356 | 357 | #go through all packets 358 | for p in packets: 359 | if int(p.split(dataSplit)[1]) == currentSequence: 360 | break 361 | packetNum += 1 362 | 363 | #if the packet needed is not here, grab the latest in the set 364 | if packetNum == len(packets): 365 | packetNum = 0 366 | if packetNum > 0 : 367 | recoveredCount += 1 368 | message = packets[packetNum] 369 | payload = message.split(dataSplit)[0] 370 | sequence = message.split(dataSplit)[1] 371 | if int(sequence) < currentSequence: 372 | break #All packets are old data, so drop it entirely 373 | 374 | currentSequence = int(sequence) + 1 375 | 376 | toSend = payload 377 | if len(toSend) > 0: 378 | ser.write(toSend) 379 | # time.sleep(0.016) 380 | if packetNum == 0: # if the first packet was the processed packet, no need to go through the rest 381 | break 382 | 383 | except IndexError: 384 | continue 385 | 386 | logger.info("listener stopped") 387 | 388 | def sender(side,opponent): 389 | global state 390 | logger.info("sending") 391 | first_run = False 392 | if side == "waiting": 393 | oppPort = 20002 394 | if side == "calling": 395 | oppPort = 20001 396 | last = 0 397 | sequence = 0 398 | packets = [] 399 | ser.timeout = None #Option 1 400 | # ser.timeout = 0.01 #Option 2 401 | 402 | while(state != "netlink_disconnected"): 403 | new = ser.read(1) #Option 1 404 | # if len(new) == 0: 405 | # continue 406 | # if ser.in_waiting > 0: #pings are single bytes. If there are no more bytes, let's assume it's a ping 407 | # raw_input = new + ser.read(3) #packets should be 4 bytes. Let's form a full packet. 408 | # else: 409 | # raw_input = new 410 | raw_input = new + ser.read(ser.in_waiting) #Option1 411 | # raw_input = ser.read(4) #Option 2 412 | # if len(raw_input) >1 and len(raw_input) < 4: 413 | # print(raw_input) 414 | if not ser.cd: 415 | print('') 416 | logger.info("NO CARRIER") 417 | ser.read(ser.in_waiting) 418 | ser.read(ser.in_waiting) 419 | state = "netlink_disconnected" 420 | time.sleep(1) 421 | udp.close() 422 | logger.info("sender stopped") 423 | return 424 | 425 | try: 426 | payload = raw_input 427 | seq = str(sequence) 428 | if len(payload)>0: 429 | 430 | packets.insert(0,(payload+dataSplit+seq.encode())) 431 | if(len(packets) > 5): 432 | packets.pop() 433 | 434 | for i in range(2): #send the data twice. May help with drops or latency 435 | ready = select.select([],[udp],[]) #blocking select 436 | if ready[1]: 437 | udp.sendto(packetSplit.join(packets), (opponent,oppPort)) 438 | 439 | sequence+=1 440 | except: 441 | continue 442 | 443 | global state 444 | state = net_state 445 | if state == "connected": 446 | t1 = threading.Thread(target=listener) 447 | t2 = threading.Thread(target=sender,args=(side,opponent)) 448 | if side == "waiting": #we're going to bind to a port. Some users may want to run two instances on one machine, so use different ports for waiting, calling 449 | Port = 20001 450 | if side == "calling": 451 | Port = 20002 452 | udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 453 | udp.setsockopt(socket.IPPROTO_IP, socket.IP_TOS, 184) 454 | udp.setblocking(0) 455 | udp.bind(('', Port)) 456 | 457 | t1.start() 458 | t2.start() 459 | t1.join() 460 | t2.join() 461 | 462 | 463 | def init_xband(modem): 464 | modem.connect_netlink(speed=57600,timeout=0.05,rtscts=True) 465 | modem.query_modem(b'AT%E0') 466 | modem.query_modem(b"AT\V1%C0") 467 | modem.query_modem(b'AT+MS=V22b') --------------------------------------------------------------------------------