diff --git a/bridge.py b/bridge.py index fd749a7..6ac29ea 100755 --- a/bridge.py +++ b/bridge.py @@ -72,6 +72,9 @@ from datetime import datetime import re from socket import gethostbyname +from setproctitle import setproctitle + + # Does anybody read this stuff? There's a PEP somewhere that says I should do this. @@ -285,26 +288,94 @@ def download_config(L_CONFIG_FILE, cli_file): # From hotspot_proxy2, FreeDMR def hotspot_proxy(listen_port, port_start, port_stop): +## Master = "127.0.0.1" +## ListenPort = listen_port +## DestportStart = port_start +## DestPortEnd = port_stop +## Timeout = 30 +## Stats = True +## Debug = False +## BlackList = [1234567] +## +## +## CONNTRACK = {} +## +## for port in range(DestportStart,DestPortEnd+1,1): +## CONNTRACK[port] = False +## +## +## reactor.listenUDP(ListenPort,Proxy(Master,ListenPort,CONNTRACK,BlackList,Timeout,Debug,DestportStart,DestPortEnd)) +## +## def loopingErrHandle(failure): +## logger.error('(GLOBAL) STOPPING REACTOR TO AVOID MEMORY LEAK: Unhandled error innowtimed loop.\n {}'.format(failure)) +## reactor.stop() +## +## def stats(): +## count = 0 +## nowtime = time() +## for port in CONNTRACK: +## if CONNTRACK[port]: +## count = count+1 +## +## totalPorts = DestPortEnd - DestportStart +## freePorts = totalPorts - count +## +## logger.info("{} ports out of {} in use ({} free)".format(count,totalPorts,freePorts)) +## +## +## +## if Stats == True: +## stats_task = task.LoopingCall(stats) +## statsa = stats_task.start(30) +## statsa.addErrback(loopingErrHandle) + Master = "127.0.0.1" ListenPort = listen_port + # '' = all IPv4, '::' = all IPv4 and IPv6 (Dual Stack) + ListenIP = '' DestportStart = port_start DestPortEnd = port_stop Timeout = 30 - Stats = True + Stats = False Debug = False + ClientInfo = True BlackList = [1234567] - +#******************* + + + #Set process title early + setproctitle(__file__) + + #If IPv6 is enabled by enivornment variable... + if ListenIP == '' and 'FDPROXY_IPV6' in os.environ and bool(os.environ['FDPROXY_IPV6']): + ListenIP = '::' + + #Override static config from Environment + if 'FDPROXY_STATS' in os.environ: + Stats = bool(os.environ['FDPROXY_STATS']) + if 'FDPROXY_DEBUG' in os.environ: + Debug = bool(os.environ['FDPROXY_DEBUG']) + if 'FDPROXY_CLIENTINFO' in os.environ: + ClientInfo = bool(os.environ['FDPROXY_CLIENTINFO']) + if 'FDPROXY_LISTENPORT' in os.environ: + ListenPort = os.environ['FDPROXY_LISTENPORT'] + + CONNTRACK = {} for port in range(DestportStart,DestPortEnd+1,1): CONNTRACK[port] = False + #If we are listening IPv6 and Master is an IPv4 IPv4Address + #IPv6ify the address. + if ListenIP == '::' and IsIPv4Address(Master): + Master = '::ffff:' + Master - reactor.listenUDP(ListenPort,Proxy(Master,ListenPort,CONNTRACK,BlackList,Timeout,Debug,DestportStart,DestPortEnd)) + reactor.listenUDP(ListenPort,Proxy(Master,ListenPort,CONNTRACK,BlackList,Timeout,Debug,ClientInfo,DestportStart,DestPortEnd),interface=ListenIP) def loopingErrHandle(failure): - logger.error('(GLOBAL) STOPPING REACTOR TO AVOID MEMORY LEAK: Unhandled error innowtimed loop.\n {}'.format(failure)) + print('(GLOBAL) STOPPING REACTOR TO AVOID MEMORY LEAK: Unhandled error innowtimed loop.\n {}'.format(failure)) reactor.stop() def stats(): @@ -317,7 +388,7 @@ def hotspot_proxy(listen_port, port_start, port_stop): totalPorts = DestPortEnd - DestportStart freePorts = totalPorts - count - logger.info("{} ports out of {} in use ({} free)".format(count,totalPorts,freePorts)) + print("{} ports out of {} in use ({} free)".format(count,totalPorts,freePorts)) @@ -325,6 +396,7 @@ def hotspot_proxy(listen_port, port_start, port_stop): stats_task = task.LoopingCall(stats) statsa = stats_task.start(30) statsa.addErrback(loopingErrHandle) + # Used to track if we have downloaded user custon rules user_rules = {} @@ -745,7 +817,9 @@ class routerOBP(OPENBRIDGE): pkt_time = time() dmrpkt = _data[20:53] _bits = _data[15] - print(int_id(_dst_id)) + + print('Src: ' + str(int_id(_rf_src))) + print('Dst: ' + str(int_id(_dst_id))) # Make/update this unit in the UNIT_MAP cache UNIT_MAP[_rf_src] = (self.name, pkt_time) diff --git a/data_gateway.py b/data_gateway.py index 417fb40..e89868e 100644 --- a/data_gateway.py +++ b/data_gateway.py @@ -1058,7 +1058,7 @@ def mmdvm_encapsulate(dst_id, src_id, peer_id, _seq, _slot, _call_type, _dtype_v middle_guts = slot + call_type + frame_type + dtype_vseq #print(middle_guts) dmr_data = str(_dmr_data)[2:-1] #str(re.sub("b'|'", '', str(_dmr_data))) - complete_packet = signature.encode() + seq + dest_id + source_id + via_id + middle_guts.tobytes() + stream_id + bytes.fromhex((dmr_data))# + bitarray('0000000000101111').tobytes()#bytes.fromhex(dmr_data) + complete_packet = signature.encode() + seq + source_id + dest_id + via_id + middle_guts.tobytes() + stream_id + bytes.fromhex((dmr_data))# + bitarray('0000000000101111').tobytes()#bytes.fromhex(dmr_data) #print('Complete: ' + str(ahex(complete_packet))) return complete_packet @@ -1085,15 +1085,15 @@ def dmr_encode(packet_list, _slot): l_slot = bitarray('0111011100') r_slot = bitarray('1101110001') #Mobile Station - # D5D7F77FD757 - #sync_data = bitarray('110101011101011111110111011111111101011101010111') - if _slot == 0: - # TS1 - F7FDD5DDFD55 - sync_data = bitarray('111101111111110111010101110111011111110101010101') - if _slot == 1: - #TS2 - D7557F5FF7F5 - sync_data = bitarray('110101110101010101111111010111111111011111110101') - + # D5D7F77FD757 + sync_data = bitarray('110101011101011111110111011111111101011101010111') +## if _slot == 0: +## # TS1 - F7FDD5DDFD55 +## sync_data = bitarray('111101111111110111010101110111011111110101010101') +## if _slot == 1: +## #TS2 - D7557F5FF7F5 +## sync_data = bitarray('110101110101010101111111010111111111011111110101') +## # Data sync? 110101011101011111110111011111111101011101010111 - D5D7F77FD757 new_pkt = ahex(stitched_pkt[:98] + l_slot + sync_data + r_slot + stitched_pkt[98:]) send_seq.append(new_pkt) @@ -1130,13 +1130,16 @@ def create_sms_seq(dst_id, src_id, peer_id, _slot, _call_type, dmr_string): mmdvm_send_seq.append(ahex(the_mmdvm_pkt)) cap_in = cap_in + 1 print(ahex(the_mmdvm_pkt)) - if bytes.fromhex(dst_id) in UNIT_MAP: - logger.info('Sending SMS packet to ' + str(UNIT_MAP[bytes.fromhex(dst_id)][0])) - systems[UNIT_MAP[bytes.fromhex(dst_id)][0]].send_system(the_mmdvm_pkt) - else: - for s in CONFIG['SYSTEMS'].items(): + if bytes.fromhex(dst_id) in UNIT_MAP: + logger.info('Sending SMS packet to ' + str(UNIT_MAP[bytes.fromhex(dst_id)][0])) + systems[UNIT_MAP[bytes.fromhex(dst_id)][0]].send_system(the_mmdvm_pkt) + else: + for s in CONFIG['SYSTEMS'].items(): + if 'FREEDMR' in s[1]['OTHER_OPTIONS']: + systems[s[0]].send_system(b'SVRDDATA' + the_mmdvm_pkt) + else: systems[s[0]].send_system(the_mmdvm_pkt) - logger.info('Sending SMS packet to ' + str(s[0])) + logger.info('Sending SMS packet to ' + str(s[0])) if CONFIG['WEB_SERVICE']['REMOTE_CONFIG_ENABLED'] == False: with open('/tmp/.hblink_data_que_' + str(CONFIG['DATA_CONFIG']['APRS_LOGIN_CALL']).upper() + '/' + str(random.randint(1000, 9999)) + '.mmdvm_seq', "w") as packet_write_file: packet_write_file.write(str(mmdvm_send_seq)) @@ -1230,9 +1233,9 @@ def send_sms(csbk, to_id, from_id, peer_id, call_type, msg): call_type = 0 # Send all Group data to TS 2, need to fix later. slot = 1 - if csbk == 'yes': + if csbk == True: use_csbk = True - create_sms_seq(to_id, from_id, peer_id, int(slot), new_call_type, csbk_gen(to_id, from_id) + create_crc16(gen_header(to_id, from_id, new_call_type)) + create_crc32(format_sms(msg, to_id, from_id))) + create_sms_seq(to_id, from_id, peer_id, int(slot), call_type, csbk_gen(to_id, from_id) + create_crc16(gen_header(to_id, from_id, call_type)) + create_crc32(format_sms(msg, to_id, from_id))) else: create_sms_seq(to_id, from_id, peer_id, int(slot), call_type, create_crc16(gen_header(to_id, from_id, call_type)) + create_crc32(format_sms(str(msg), to_id, from_id))) @@ -1476,6 +1479,7 @@ def data_received(self, _peer_id, _rf_src, _dst_id, _seq, _slot, _call_type, _fr # Use block 0 as trigger. $GPRMC must also be in string to indicate NMEA. # This triggers the APRS upload if btf == 0: + final_packet = str(bitarray(re.sub("\)|\(|bitarray|'", '', packet_assembly)).tobytes().decode('utf-8', 'ignore')) sms_hex = str(ba2hx(bitarray(re.sub("\)|\(|bitarray|'", '', packet_assembly)))) sms_hex_string = re.sub("b'|'", '', str(sms_hex)) @@ -1690,8 +1694,10 @@ class OBP(OPENBRIDGE): if _mode == b'UNIT': UNIT_MAP[_data] = (self._system, time()) print(UNIT_MAP) - if _mode == b'DATA' or _mode == b'MDATA': + if _mode == b'DATA' or _mode == b'MDAT': +## print(ahex(_data)) # DMR Data packet, sent via SVRD + _peer_id = _data[11:15] _seq = _data[4] _rf_src = _data[5:8] @@ -1709,6 +1715,14 @@ class OBP(OPENBRIDGE): _dtype_vseq = (_bits & 0xF) # data, 1=voice header, 2=voice terminator; voice, 0=burst A ... 5=burst F _stream_id = _data[16:20] + print(int_id(_peer_id)) + print(int_id(_rf_src)) + print(int_id(_dst_id)) + print((_dtype_vseq)) + print(ahex(bptc_decode(_data))) + + + ## # Record last packet to prevent duplicates, think finger printing. ## PACKET_MATCH[_rf_src] = [_data, time()] @@ -1724,9 +1738,8 @@ class HBP(HBSYSTEM): def dmrd_received(self, _peer_id, _rf_src, _dst_id, _seq, _slot, _call_type, _frame_type, _dtype_vseq, _stream_id, _data): UNIT_MAP[_rf_src] = (self._system, time()) - print(_dtype_vseq) +## print(ahex(_data)) print('MMDVM RCVD') - print(UNIT_MAP) if _rf_src not in PACKET_MATCH: PACKET_MATCH[_rf_src] = [_data, time()] elif _data == PACKET_MATCH[_rf_src][0] and time() - 1 < PACKET_MATCH[_rf_src][1]: diff --git a/hblink.py b/hblink.py index 108b2c7..22556dc 100755 --- a/hblink.py +++ b/hblink.py @@ -191,6 +191,7 @@ class OPENBRIDGE(DatagramProtocol): pass def datagramReceived(self, _packet, _sockaddr): + print(_sockaddr) ## print(_packet[:4]) # Keep This Line Commented Unless HEAVILY Debugging! ## logger.debug('(%s) RX packet from %s -- %s', self._system, _sockaddr, ahex(_packet)) @@ -298,7 +299,8 @@ class OPENBRIDGE(DatagramProtocol): ## self.dmrd_received(_peer_id, _rf_src, _dst_id, _seq, _slot, _call_type, _frame_type, _dtype_vseq, _stream_id, _data) ## else: - self.svrd_received(_d_pkt[4:8], _d_pkt[8:]) + self.svrd_received(_d_pkt[4:8], _d_pkt[8:]) +## print(ahex(_d_pkt[8:])) #************************************************ # HB MASTER CLASS diff --git a/hotspot_proxy_v2.py b/hotspot_proxy_v2.py index fea6edd..a19f932 100644 --- a/hotspot_proxy_v2.py +++ b/hotspot_proxy_v2.py @@ -1,26 +1,63 @@ +############################################################################### +# Copyright (C) 2020 Simon Adlem, G7RZU +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +############################################################################### + from twisted.internet.protocol import DatagramProtocol from twisted.internet import reactor, task from time import time -from resettabletimer import ResettableTimer from dmr_utils3.utils import int_id import random +import ipaddress +import os +from setproctitle import setproctitle +from datetime import datetime # Does anybody read this stuff? There's a PEP somewhere that says I should do this. __author__ = 'Simon Adlem - G7RZU' __copyright__ = 'Copyright (c) Simon Adlem, G7RZU 2020,2021' -__credits__ = 'Jon Lee, G4TSN; Norman Williams, M6NBP' +__credits__ = 'Jon Lee, G4TSN; Norman Williams, M6NBP; Christian, OA4DOA' __license__ = 'GNU GPLv3' __maintainer__ = 'Simon Adlem G7RZU' __email__ = 'simon@gb7fr.org.uk' +def IsIPv4Address(ip): + try: + ipaddress.IPv4Address(ip) + return True + except ValueError as errorCode: + pass + return False + +def IsIPv6Address(ip): + try: + ipaddress.IPv6Address(ip) + return True + except ValueError as errorCode: + pass + class Proxy(DatagramProtocol): - def __init__(self,Master,ListenPort,connTrack,blackList,Timeout,Debug,DestportStart,DestPortEnd): + def __init__(self,Master,ListenPort,connTrack,blackList,Timeout,Debug,ClientInfo,DestportStart,DestPortEnd): self.master = Master self.connTrack = connTrack self.peerTrack = {} self.timeout = Timeout self.debug = Debug + self.clientinfo = ClientInfo self.blackList = blackList self.destPortStart = DestportStart self.destPortEnd = DestPortEnd @@ -30,7 +67,9 @@ class Proxy(DatagramProtocol): def reaper(self,_peer_id): if self.debug: print("dead",_peer_id) - self.transport.write(b'RPTCL'+_peer_id, ('127.0.0.1',self.peerTrack[_peer_id]['dport'])) + if self.clientinfo and _peer_id != b'\xff\xff\xff\xff': + print(f"{datetime.now().replace(microsecond=0)} Client: ID:{str(int_id(_peer_id)).rjust(9)} IP:{self.peerTrack[_peer_id]['shost'].rjust(15)} Port:{self.peerTrack[_peer_id]['sport']} Removed.") + self.transport.write(b'RPTCL'+_peer_id, (self.master,self.peerTrack[_peer_id]['dport'])) self.connTrack[self.peerTrack[_peer_id]['dport']] = False del self.peerTrack[_peer_id] @@ -57,6 +96,8 @@ class Proxy(DatagramProtocol): RPTA = b'RPTA' RPTO = b'RPTO' + _peer_id = False + host,port = addr nowtime = time() @@ -66,62 +107,49 @@ class Proxy(DatagramProtocol): #If the packet comes from the master if host == self.master: _command = data[:4] - _lng_command = data[:6] -#### print(_lng_command) - if _command == DMRD: _peer_id = data[11:15] -## print(self.peerTrack[_peer_id]['timer']) elif _command == RPTA: if data[6:10] in self.peerTrack: _peer_id = data[6:10] else: _peer_id = self.connTrack[port] - elif _lng_command == MSTNAK: - _peer_id = data[6:10] - - elif _command == MSTN and MSTNAK not in _lng_command: + elif _command == MSTN: _peer_id = data[6:10] - - self.peerTrack[_peer_id]['timer'].cancel() - self.reaper(_peer_id) - return elif _command == MSTP: _peer_id = data[7:11] -## print(self.peerTrack) elif _command == MSTC: _peer_id = data[5:9] - self.peerTrack[_peer_id]['timer'].cancel() - self.reaper(_peer_id) - return - # _peer_id = self.connTrack[port] if self.debug: print(data) - if _peer_id and _peer_id in self.peerTrack: + if _peer_id in self.peerTrack: self.transport.write(data,(self.peerTrack[_peer_id]['shost'],self.peerTrack[_peer_id]['sport'])) - #self.peerTrack[_peer_id]['timer'].reset() + # Remove the client after send a MSTN or MSTC packet + if _command in (MSTN,MSTC): + # Give time to the client for a reply to prevent port reassignment + self.peerTrack[_peer_id]['timer'].reset(15) + return - - + else: _command = data[:4] if _command == DMRD: # DMRData -- encapsulated DMR data frame _peer_id = data[11:15] elif _command == DMRA: # DMRAlias -- Talker Alias information - _peer_id = _data[4:8] + _peer_id = data[4:8] elif _command == RPTL: # RPTLogin -- a repeater wants to login _peer_id = data[4:8] elif _command == RPTK: # Repeater has answered our login challenge _peer_id = data[4:8] elif _command == RPTC: # Repeater is sending it's configuraiton OR disconnecting - if data[:5] == RPTCL: # Disconnect command + if data[:5] == RPTCL: # Disconnect command _peer_id = data[5:9] else: - _peer_id = data[4:8] # Configure Command + _peer_id = data[4:8] # Configure Command elif _command == RPTO: # options _peer_id = data[4:8] elif _command == RPTP: # RPTPing -- peer is pinging us @@ -133,33 +161,33 @@ class Proxy(DatagramProtocol): _dport = self.peerTrack[_peer_id]['dport'] self.peerTrack[_peer_id]['sport'] = port self.peerTrack[_peer_id]['shost'] = host - self.transport.write(data, ('127.0.0.1',_dport)) - self.peerTrack[_peer_id]['timer'].reset() + self.transport.write(data, (self.master,_dport)) + self.peerTrack[_peer_id]['timer'].reset(self.timeout) if self.debug: print(data) - print(_dport) return + else: - if int_id(_peer_id) in self.blackList: + return + # Make a list with the available ports + _ports_avail = [port for port in self.connTrack if not self.connTrack[port]] + if _ports_avail: + _dport = random.choice(_ports_avail) + else: return - #for _dport in self.connTrack: - while True: - _dport = random.randint(1,(self.numPorts - 1)) - _dport = _dport + self.destPortStart - if not self.connTrack[_dport]: - break self.connTrack[_dport] = _peer_id self.peerTrack[_peer_id] = {} self.peerTrack[_peer_id]['dport'] = _dport self.peerTrack[_peer_id]['sport'] = port self.peerTrack[_peer_id]['shost'] = host - self.peerTrack[_peer_id]['timer'] = ResettableTimer(self.timeout,self.reaper,[_peer_id]) - self.peerTrack[_peer_id]['timer'].start() + self.peerTrack[_peer_id]['timer'] = reactor.callLater(self.timeout,self.reaper,_peer_id) self.transport.write(data, (self.master,_dport)) + + if self.clientinfo and _peer_id != b'\xff\xff\xff\xff': + print(f'{datetime.now().replace(microsecond=0)} New client: ID:{str(int_id(_peer_id)).rjust(9)} IP:{host.rjust(15)} Port:{port}, assigned to port:{_dport}.') if self.debug: print(data) - return @@ -169,23 +197,48 @@ if __name__ == '__main__': Master = "127.0.0.1" ListenPort = 62031 - DestportStart = 54100 - DestPortEnd = 54102 + # '' = all IPv4, '::' = all IPv4 and IPv6 (Dual Stack) + ListenIP = '' + DestportStart = 54000 + DestPortEnd = 54100 Timeout = 30 - Stats = True - Debug = True + Stats = False + Debug = False + ClientInfo = False BlackList = [1234567] #******************* - + + + #Set process title early + setproctitle(__file__) + + #If IPv6 is enabled by enivornment variable... + if ListenIP == '' and 'FDPROXY_IPV6' in os.environ and bool(os.environ['FDPROXY_IPV6']): + ListenIP = '::' + + #Override static config from Environment + if 'FDPROXY_STATS' in os.environ: + Stats = bool(os.environ['FDPROXY_STATS']) + if 'FDPROXY_DEBUG' in os.environ: + Debug = bool(os.environ['FDPROXY_DEBUG']) + if 'FDPROXY_CLIENTINFO' in os.environ: + ClientInfo = bool(os.environ['FDPROXY_CLIENTINFO']) + if 'FDPROXY_LISTENPORT' in os.environ: + ListenPort = os.environ['FDPROXY_LISTENPORT'] + CONNTRACK = {} for port in range(DestportStart,DestPortEnd+1,1): CONNTRACK[port] = False + #If we are listening IPv6 and Master is an IPv4 IPv4Address + #IPv6ify the address. + if ListenIP == '::' and IsIPv4Address(Master): + Master = '::ffff:' + Master - reactor.listenUDP(ListenPort,Proxy(Master,ListenPort,CONNTRACK,BlackList,Timeout,Debug,DestportStart,DestPortEnd)) + reactor.listenUDP(ListenPort,Proxy(Master,ListenPort,CONNTRACK,BlackList,Timeout,Debug,ClientInfo,DestportStart,DestPortEnd),interface=ListenIP) def loopingErrHandle(failure): print('(GLOBAL) STOPPING REACTOR TO AVOID MEMORY LEAK: Unhandled error innowtimed loop.\n {}'.format(failure)) diff --git a/web/templates/map.html b/web/templates/map.html index e791bd8..b64552f 100644 --- a/web/templates/map.html +++ b/web/templates/map.html @@ -11,7 +11,7 @@ Red = Peer location (no APRS) Blue = GPS location (APRS) -Green = Peer location (APRS) +