forked from adamfast/DMRlink
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdmrlink.py
executable file
·1292 lines (1084 loc) · 56 KB
/
dmrlink.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python
#
# This work is licensed under the Creative Commons Attribution-ShareAlike
# 3.0 Unported License.To view a copy of this license, visit
# http://creativecommons.org/licenses/by-sa/3.0/ or send a letter to
# Creative Commons, 444 Castro Street, Suite 900, Mountain View,
# California, 94041, USA.
#NOTE: This program uses a configuration file specified on the command line
# if none is specified, then dmrlink.cfg in the same directory as this
# file will be tried. Finally, if that does not exist, this process
# will terminate
from __future__ import print_function
import ConfigParser
import argparse
import sys
import binascii
import csv
import os
import logging
import time
import signal
import cPickle as pickle
from logging.config import dictConfig
from hmac import new as hmac_new
from binascii import b2a_hex as h
from hashlib import sha1
from socket import inet_ntoa as IPAddr
from socket import inet_aton as IPHexStr
from twisted.internet.protocol import DatagramProtocol
from twisted.internet import reactor
from twisted.internet import task
__author__ = 'Cortney T. Buffington, N0MJS'
__copyright__ = 'Copyright (c) 2013, 2014 Cortney T. Buffington, N0MJS and the K0USY Group'
__credits__ = 'Adam Fast, KC0YLK, Dave K, and he who wishes not to be named'
__license__ = 'Creative Commons Attribution-ShareAlike 3.0 Unported'
__version__ = '0.27b'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = '[email protected]'
__status__ = 'beta'
parser = argparse.ArgumentParser()
parser.add_argument('-c', '--config', action='store', dest='CFG_FILE', help='/full/path/to/config.file (usually dmrlink.cfg)')
cli_args = parser.parse_args()
#************************************************
# PARSE THE CONFIG FILE AND BUILD STRUCTURE
#************************************************
NETWORK = {}
networks = {}
config = ConfigParser.ConfigParser()
if not cli_args.CFG_FILE:
cli_args.CFG_FILE = os.path.dirname(os.path.abspath(__file__))+'/dmrlink.cfg'
try:
if not config.read(cli_args.CFG_FILE):
sys.exit('Configuration file \''+cli_args.CFG_FILE+'\' is not a valid configuration file! Exiting...')
except:
sys.exit('Configuration file \''+cli_args.CFG_FILE+'\' is not a valid configuration file! Exiting...')
try:
for section in config.sections():
if section == 'GLOBAL':
# Process GLOBAL items in the configuration
PATH = config.get(section, 'PATH')
elif section == 'REPORTS':
# Process REPORTS items in the configuration
REPORTS = {
'REPORT_PEERS': config.getboolean(section, 'REPORT_PEERS'),
'REPORT_INTERVAL': config.getint(section, 'REPORT_INTERVAL'),
'PEER_REPORT_INC_MODE': config.getboolean(section, 'PEER_REPORT_INC_MODE'),
'PEER_REPORT_INC_FLAGS': config.getboolean(section, 'PEER_REPORT_INC_FLAGS')
}
elif section == 'LOGGER':
# Process LOGGER items in the configuration
LOGGER = {
'LOG_FILE': config.get(section, 'LOG_FILE'),
'LOG_HANDLERS': config.get(section, 'LOG_HANDLERS'),
'LOG_LEVEL': config.get(section, 'LOG_LEVEL'),
'LOG_NAME': config.get(section, 'LOG_NAME')
}
elif config.getboolean(section, 'ENABLED'):
# All other sections define indiviual IPSC Networks we connect to
# Each IPSC network config will contain the following three sections
NETWORK.update({section: {'LOCAL': {}, 'MASTER': {}, 'PEERS': {}}})
# LOCAL means we need to know this stuff to be a peer in the network
NETWORK[section]['LOCAL'].update({
# In case we want to keep config, but not actually connect to the network
'ENABLED': config.getboolean(section, 'ENABLED'),
# These items are used to create the MODE byte
'PEER_OPER': config.getboolean(section, 'PEER_OPER'),
'IPSC_MODE': config.get(section, 'IPSC_MODE'),
'TS1_LINK': config.getboolean(section, 'TS1_LINK'),
'TS2_LINK': config.getboolean(section, 'TS2_LINK'),
'MODE': '',
# These items are used to create the multi-byte FLAGS field
'AUTH_ENABLED': config.getboolean(section, 'AUTH_ENABLED'),
'CSBK_CALL': config.getboolean(section, 'CSBK_CALL'),
'RCM': config.getboolean(section, 'RCM'),
'CON_APP': config.getboolean(section, 'CON_APP'),
'XNL_CALL': config.getboolean(section, 'XNL_CALL'),
'XNL_MASTER': config.getboolean(section, 'XNL_MASTER'),
'DATA_CALL': config.getboolean(section, 'DATA_CALL'),
'VOICE_CALL': config.getboolean(section, 'VOICE_CALL'),
'MASTER_PEER': config.getboolean(section, 'MASTER_PEER'),
'FLAGS': '',
# Things we need to know to connect and be a peer in this IPSC
'RADIO_ID': hex(int(config.get(section, 'RADIO_ID')))[2:].rjust(8,'0').decode('hex'),
'IP': config.get(section, 'IP'),
'PORT': config.getint(section, 'PORT'),
'ALIVE_TIMER': config.getint(section, 'ALIVE_TIMER'),
'MAX_MISSED': config.getint(section, 'MAX_MISSED'),
'AUTH_KEY': (config.get(section, 'AUTH_KEY').rjust(40,'0')).decode('hex'),
'NUM_PEERS': 0,
})
# Master means things we need to know about the master peer of the network
NETWORK[section]['MASTER'].update({
'RADIO_ID': '\x00\x00\x00\x00',
'MODE': '\x00',
'MODE_DECODE': '',
'FLAGS': '\x00\x00\x00\x00',
'FLAGS_DECODE': '',
'STATUS': {
'CONNECTED': False,
'PEER_LIST': False,
'KEEP_ALIVES_SENT': 0,
'KEEP_ALIVES_MISSED': 0,
'KEEP_ALIVES_OUTSTANDING': 0,
'KEEP_ALIVES_RECEIVED': 0,
'KEEP_ALIVE_RX_TIME': 0
},
'IP': '',
'PORT': ''
})
if not NETWORK[section]['LOCAL']['MASTER_PEER']:
NETWORK[section]['MASTER'].update({
'IP': config.get(section, 'MASTER_IP'),
'PORT': config.getint(section, 'MASTER_PORT')
})
# Temporary locations for building MODE and FLAG data
MODE_BYTE = 0
FLAG_1 = 0
FLAG_2 = 0
# Construct and store the MODE field
if NETWORK[section]['LOCAL']['PEER_OPER']:
MODE_BYTE |= 1 << 6
if NETWORK[section]['LOCAL']['IPSC_MODE'] == 'ANALOG':
MODE_BYTE |= 1 << 4
elif NETWORK[section]['LOCAL']['IPSC_MODE'] == 'DIGITAL':
MODE_BYTE |= 1 << 5
if NETWORK[section]['LOCAL']['TS1_LINK']:
MODE_BYTE |= 1 << 3
else:
MODE_BYTE |= 1 << 2
if NETWORK[section]['LOCAL']['TS2_LINK']:
MODE_BYTE |= 1 << 1
else:
MODE_BYTE |= 1 << 0
NETWORK[section]['LOCAL']['MODE'] = chr(MODE_BYTE)
# Construct and store the FLAGS field
if NETWORK[section]['LOCAL']['CSBK_CALL']:
FLAG_1 |= 1 << 7
if NETWORK[section]['LOCAL']['RCM']:
FLAG_1 |= 1 << 6
if NETWORK[section]['LOCAL']['CON_APP']:
FLAG_1 |= 1 << 5
if NETWORK[section]['LOCAL']['XNL_CALL']:
FLAG_2 |= 1 << 7
if NETWORK[section]['LOCAL']['XNL_CALL'] and NETWORK[section]['LOCAL']['XNL_MASTER']:
FLAG_2 |= 1 << 6
elif NETWORK[section]['LOCAL']['XNL_CALL'] and not NETWORK[section]['LOCAL']['XNL_MASTER']:
FLAG_2 |= 1 << 5
if NETWORK[section]['LOCAL']['AUTH_ENABLED']:
FLAG_2 |= 1 << 4
if NETWORK[section]['LOCAL']['DATA_CALL']:
FLAG_2 |= 1 << 3
if NETWORK[section]['LOCAL']['VOICE_CALL']:
FLAG_2 |= 1 << 2
if NETWORK[section]['LOCAL']['MASTER_PEER']:
FLAG_2 |= 1 << 0
NETWORK[section]['LOCAL']['FLAGS'] = '\x00\x00'+chr(FLAG_1)+chr(FLAG_2)
except:
sys.exit('Could not parse configuration file, exiting...')
#************************************************
# CONFIGURE THE SYSTEM LOGGER
#************************************************
dictConfig({
'version': 1,
'disable_existing_loggers': False,
'filters': {
},
'formatters': {
'verbose': {
'format': '%(levelname)s %(asctime)s %(module)s %(process)d %(thread)d %(message)s'
},
'timed': {
'format': '%(levelname)s %(asctime)s %(message)s'
},
'simple': {
'format': '%(levelname)s %(message)s'
},
'syslog': {
'format': '%(name)s (%(process)d): %(levelname)s %(message)s'
}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'simple'
},
'console-timed': {
'class': 'logging.StreamHandler',
'formatter': 'timed'
},
'file': {
'class': 'logging.FileHandler',
'formatter': 'simple',
'filename': LOGGER['LOG_FILE'],
},
'file-timed': {
'class': 'logging.FileHandler',
'formatter': 'timed',
'filename': LOGGER['LOG_FILE'],
},
'syslog': {
'class': 'logging.handlers.SysLogHandler',
'formatter': 'syslog',
}
},
'loggers': {
LOGGER['LOG_NAME']: {
'handlers': LOGGER['LOG_HANDLERS'].split(','),
'level': LOGGER['LOG_LEVEL'],
'propagate': True,
}
}
})
logger = logging.getLogger(LOGGER['LOG_NAME'])
#************************************************
# IMPORTING OTHER FILES - '#include'
#************************************************
# Import IPSC message types and version information
#
try:
from ipsc.ipsc_message_types import *
except ImportError:
sys.exit('IPSC message types file not found or invalid')
# Import IPSC flag mask values
#
try:
from ipsc.ipsc_mask import *
except ImportError:
sys.exit('IPSC mask values file not found or invalid')
# Import the Alias files for numeric ids. This is split to save
# time making lookups in one huge dictionary
#
curdir= os.path.dirname(__file__)
subscriber_ids = {}
peer_ids = {}
talkgroup_ids = {}
try:
with open(PATH+'subscriber_ids.csv', 'rU') as subscriber_ids_csv:
subscribers = csv.reader(subscriber_ids_csv, dialect='excel', delimiter=',')
for row in subscribers:
subscriber_ids[int(row[1])] = (row[0])
except ImportError:
logger.warning('subscriber_ids.csv not found: Subscriber aliases will not be available')
try:
with open(PATH+'peer_ids.csv', 'rU') as peer_ids_csv:
peers = csv.reader(peer_ids_csv, dialect='excel', delimiter=',')
for row in peers:
peer_ids[int(row[1])] = (row[0])
except ImportError:
logger.warning('peer_ids.csv not found: Peer aliases will not be available')
try:
with open(PATH+'talkgroup_ids.csv', 'rU') as talkgroup_ids_csv:
talkgroups = csv.reader(talkgroup_ids_csv, dialect='excel', delimiter=',')
for row in talkgroups:
talkgroup_ids[int(row[1])] = (row[0])
except ImportError:
logger.warning('talkgroup_ids.csv not found: Talkgroup aliases will not be available')
#************************************************
# UTILITY FUNCTIONS FOR INTERNAL USE
#************************************************
# Create a 2 byte hex string from an integer
#
def hex_str_2(_int_id):
try:
return hex(_int_id)[2:].rjust(4,'0').decode('hex')
except TypeError:
logger.error('hex_str_2: invalid integer length')
# Create a 3 byte hex string from an integer
#
def hex_str_3(_int_id):
try:
return hex(_int_id)[2:].rjust(6,'0').decode('hex')
except TypeError:
logger.error('hex_str_3: invalid integer length')
# Create a 4 byte hex string from an integer
#
def hex_str_4(_int_id):
try:
return hex(_int_id)[2:].rjust(8,'0').decode('hex')
except TypeError:
logger.error('hex_str_4: invalid integer length')
# Convert a hex string to an int (radio ID, etc.)
#
def int_id(_hex_string):
return int(h(_hex_string), 16)
# Re-Write Source Radio-ID (DMR NAT)
#
def dmr_nat(_data, _src_id, _nat_id):
_data = _data.replace(_src_id, _nat_id)
return _data
# Lookup text data for numeric IDs
#
def get_info(_id, _dict):
if _id in _dict:
return _dict[_id]
return _id
# Determine if the provided peer ID is valid for the provided network
#
def valid_peer(_peer_list, _peerid):
if _peerid in _peer_list:
return True
return False
# Determine if the provided master ID is valid for the provided network
#
def valid_master(_network, _peerid):
if NETWORK[_network]['MASTER']['RADIO_ID'] == _peerid:
return True
else:
return False
# Accept a complete packet, ready to be sent, and send it to all active peers + master in an IPSC
#
def send_to_ipsc(_target, _packet):
_network = NETWORK[_target]
_network_instance = networks[_target]
_peers = _network['PEERS']
# Send to the Master
if _network['MASTER']['STATUS']['CONNECTED']:
_network_instance.transport.write(_packet, (_network['MASTER']['IP'], _network['MASTER']['PORT']))
# Send to each connected Peer
for peer in _peers.keys():
if _peers[peer]['STATUS']['CONNECTED']:
_network_instance.transport.write(_packet, (_peers[peer]['IP'], _peers[peer]['PORT']))
# De-register a peer from an IPSC by removing it's information
#
def de_register_peer(_network, _peerid):
# Iterate for the peer in our data
if _peerid in NETWORK[_network]['PEERS'].keys():
del NETWORK[_network]['PEERS'][_peerid]
logger.info('(%s) Peer De-Registration Requested for: %s', _network, int_id(_peerid))
return
else:
logger.warning('(%s) Peer De-Registration Requested for: %s, but we don\'t have a listing for this peer', _network, int_id(_peerid))
pass
# Process the MODE byte in registration/peer list packets for determining master and peer capabilities
#
def process_mode_byte(_hex_mode):
_mode = int(h(_hex_mode), 16)
# Determine whether or not the peer is operational
_peer_op = bool(_mode & PEER_OP_MSK)
# Determine whether or not timeslot 1 is linked
_ts1 = bool(_mode & IPSC_TS1_MSK)
# Determine whether or not timeslot 2 is linked
_ts2 = bool(_mode & IPSC_TS2_MSK)
# Determine the operational mode of the peer
if _mode & PEER_MODE_MSK == PEER_MODE_MSK:
_peer_mode = 'UNKNOWN'
elif not _mode & PEER_MODE_MSK:
_peer_mode = 'NO_RADIO'
elif _mode & PEER_MODE_ANALOG:
_peer_mode = 'ANALOG'
elif _mode & PEER_MODE_DIGITAL:
_peer_mode = 'DIGITAL'
return {
'PEER_OP': _peer_op,
'PEER_MODE': _peer_mode,
'TS_1': _ts1,
'TS_2': _ts2
}
# Process the FLAGS bytes in registration replies for determining what services are available
#
def process_flags_bytes(_hex_flags):
_byte3 = int(h(_hex_flags[2]), 16)
_byte4 = int(h(_hex_flags[3]), 16)
_csbk = bool(_byte3 & CSBK_MSK)
_rpt_mon = bool(_byte3 & RPT_MON_MSK)
_con_app = bool(_byte3 & CON_APP_MSK)
_xnl_con = bool(_byte4 & XNL_STAT_MSK)
_xnl_master = bool(_byte4 & XNL_MSTR_MSK)
_xnl_slave = bool(_byte4 & XNL_SLAVE_MSK)
_auth = bool(_byte4 & PKT_AUTH_MSK)
_data = bool(_byte4 & DATA_CALL_MSK)
_voice = bool(_byte4 & VOICE_CALL_MSK)
_master = bool(_byte4 & MSTR_PEER_MSK)
return {
'CSBK': _csbk,
'RCM': _rpt_mon,
'CON_APP': _con_app,
'XNL_CON': _xnl_con,
'XNL_MASTER': _xnl_master,
'XNL_SLAVE': _xnl_slave,
'AUTH': _auth,
'DATA': _data,
'VOICE': _voice,
'MASTER': _master
}
# Take a received peer list and the network it belongs to, process and populate the
# data structure in my_ipsc_config with the results, and return a simple list of peers.
#
def process_peer_list(_data, _network):
# Create a temporary peer list to track who we should have in our list -- used to find old peers we should remove.
_temp_peers = []
# Determine the length of the peer list for the parsing iterator
_peer_list_length = int(h(_data[5:7]), 16)
# Record the number of peers in the data structure... we'll use it later (11 bytes per peer entry)
NETWORK[_network]['LOCAL']['NUM_PEERS'] = _peer_list_length/11
logger.info('(%s) Peer List Received from Master: %s peers in this IPSC', _network, NETWORK[_network]['LOCAL']['NUM_PEERS'])
# Iterate each peer entry in the peer list. Skip the header, then pull the next peer, the next, etc.
for i in range(7, _peer_list_length +7, 11):
# Extract various elements from each entry...
_hex_radio_id = (_data[i:i+4])
_hex_address = (_data[i+4:i+8])
_ip_address = IPAddr(_hex_address)
_hex_port = (_data[i+8:i+10])
_port = int(h(_hex_port), 16)
_hex_mode = (_data[i+10:i+11])
# Add this peer to a temporary PeerID list - used to remove any old peers no longer with us
_temp_peers.append(_hex_radio_id)
# This is done elsewhere for the master too, so we use a separate function
_decoded_mode = process_mode_byte(_hex_mode)
# If this entry WAS already in our list, update everything except the stats
# in case this was a re-registration with a different mode, flags, etc.
if _hex_radio_id in NETWORK[_network]['PEERS'].keys():
NETWORK[_network]['PEERS'][_hex_radio_id]['IP'] = _ip_address
NETWORK[_network]['PEERS'][_hex_radio_id]['PORT'] = _port
NETWORK[_network]['PEERS'][_hex_radio_id]['MODE'] = _hex_mode
NETWORK[_network]['PEERS'][_hex_radio_id]['MODE_DECODE'] = _decoded_mode
NETWORK[_network]['PEERS'][_hex_radio_id]['FLAGS'] = ''
NETWORK[_network]['PEERS'][_hex_radio_id]['FLAGS_DECODE'] = ''
logger.debug('(%s) Peer Updated: %s', _network, NETWORK[_network]['PEERS'][_hex_radio_id])
# If this entry was NOT already in our list, add it.
if _hex_radio_id not in NETWORK[_network]['PEERS'].keys():
NETWORK[_network]['PEERS'][_hex_radio_id] = {
'IP': _ip_address,
'PORT': _port,
'MODE': _hex_mode,
'MODE_DECODE': _decoded_mode,
'FLAGS': '',
'FLAGS_DECODE': '',
'STATUS': {
'CONNECTED': False,
'KEEP_ALIVES_SENT': 0,
'KEEP_ALIVES_MISSED': 0,
'KEEP_ALIVES_OUTSTANDING': 0,
'KEEP_ALIVES_RECEIVED': 0,
'KEEP_ALIVE_RX_TIME': 0
}
}
logger.debug('(%s) Peer Added: %s', _network, NETWORK[_network]['PEERS'][_hex_radio_id])
# Finally, check to see if there's a peer already in our list that was not in this peer list
# and if so, delete it.
for peer in NETWORK[_network]['PEERS'].keys():
if peer not in _temp_peers:
de_register_peer(_network, peer)
logger.warning('(%s) Peer Deleted (not in new peer list): %s', _network, int_id(peer))
# Build a peer list - used when a peer registers, re-regiseters or times out
#
def build_peer_list(_peers):
concatenated_peers = ''
for peer in _peers:
hex_ip = IPHexStr(_peers[peer]['IP'])
hex_port = hex_str_2(_peers[peer]['PORT'])
mode = _peers[peer]['MODE']
concatenated_peers += peer + hex_ip + hex_port + mode
peer_list = hex_str_2(len(concatenated_peers)) + concatenated_peers
return peer_list
# Gratuitous print-out of the peer list.. Pretty much debug stuff.
#
def print_peer_list(_network):
_peers = NETWORK[_network]['PEERS']
_status = NETWORK[_network]['MASTER']['STATUS']['PEER_LIST']
#print('Peer List Status for {}: {}' .format(_network, _status))
if _status and not NETWORK[_network]['PEERS']:
print('We are the only peer for: %s' % _network)
print('')
return
print('Peer List for: %s' % _network)
for peer in _peers.keys():
_this_peer = _peers[peer]
_this_peer_stat = _this_peer['STATUS']
if peer == NETWORK[_network]['LOCAL']['RADIO_ID']:
me = '(self)'
else:
me = ''
print('\tRADIO ID: {} {}' .format(int_id(peer), me))
print('\t\tIP Address: {}:{}' .format(_this_peer['IP'], _this_peer['PORT']))
if _this_peer['MODE_DECODE'] and REPORTS['PEER_REPORT_INC_MODE']:
print('\t\tMode Values:')
for name, value in _this_peer['MODE_DECODE'].items():
print('\t\t\t{}: {}' .format(name, value))
if _this_peer['FLAGS_DECODE'] and REPORTS['PEER_REPORT_INC_FLAGS']:
print('\t\tService Flags:')
for name, value in _this_peer['FLAGS_DECODE'].items():
print('\t\t\t{}: {}' .format(name, value))
print('\t\tStatus: {}, KeepAlives Sent: {}, KeepAlives Outstanding: {}, KeepAlives Missed: {}' .format(_this_peer_stat['CONNECTED'], _this_peer_stat['KEEP_ALIVES_SENT'], _this_peer_stat['KEEP_ALIVES_OUTSTANDING'], _this_peer_stat['KEEP_ALIVES_MISSED']))
print('\t\t KeepAlives Received: {}, Last KeepAlive Received at: {}' .format(_this_peer_stat['KEEP_ALIVES_RECEIVED'], _this_peer_stat['KEEP_ALIVE_RX_TIME']))
print('')
# Gratuitous print-out of Master info.. Pretty much debug stuff.
#
def print_master(_network):
if NETWORK[_network]['LOCAL']['MASTER_PEER']:
print('DMRlink is the Master for %s' % _network)
else:
_master = NETWORK[_network]['MASTER']
print('Master for %s' % _network)
print('\tRADIO ID: {}' .format(int(h(_master['RADIO_ID']), 16)))
if _master['MODE_DECODE'] and REPORTS['PEER_REPORT_INC_MODE']:
print('\t\tMode Values:')
for name, value in _master['MODE_DECODE'].items():
print('\t\t\t{}: {}' .format(name, value))
if _master['FLAGS_DECODE'] and REPORTS['PEER_REPORT_INC_FLAGS']:
print('\t\tService Flags:')
for name, value in _master['FLAGS_DECODE'].items():
print('\t\t\t{}: {}' .format(name, value))
print('\t\tStatus: {}, KeepAlives Sent: {}, KeepAlives Outstanding: {}, KeepAlives Missed: {}' .format(_master['STATUS']['CONNECTED'], _master['STATUS']['KEEP_ALIVES_SENT'], _master['STATUS']['KEEP_ALIVES_OUTSTANDING'], _master['STATUS']['KEEP_ALIVES_MISSED']))
print('\t\t KeepAlives Received: {}, Last KeepAlive Received at: {}' .format(_master['STATUS']['KEEP_ALIVES_RECEIVED'], _master['STATUS']['KEEP_ALIVE_RX_TIME']))
def write_ipsc_stats():
file = open('stats.py', 'w')
pickle.dump(NETWORK, file)
file.close()
# Shut ourselves down gracefully with the IPSC peers.
#
def handler(_signal, _frame):
logger.info('*** DMRLINK IS TERMINATING WITH SIGNAL %s ***', str(_signal))
for network in networks:
this_ipsc = networks[network]
logger.info('De-Registering from IPSC %s', network)
de_reg_req_pkt = this_ipsc.hashed_packet(this_ipsc._local['AUTH_KEY'], this_ipsc.DE_REG_REQ_PKT)
send_to_ipsc(network, de_reg_req_pkt)
reactor.stop()
# Set signal handers so that we can gracefully exit if need be
for sig in [signal.SIGTERM, signal.SIGINT, signal.SIGQUIT]:
signal.signal(sig, handler)
#************************************************
#******** ***********
#******** IPSC Network 'Engine' ***********
#******** ***********
#************************************************
#************************************************
# Base Class (used nearly all of the time)
#************************************************
class IPSC(DatagramProtocol):
#************************************************
# IPSC INSTANCE INSTANTIATION
#************************************************
# Modify the initializer to set up our environment and build the packets
# we need to maintain connections
#
def __init__(self, *args, **kwargs):
if len(args) == 1:
# Housekeeping: create references to the configuration and status data for this IPSC instance.
# Some configuration objects that are used frequently and have lengthy names are shortened
# such as (self._master_sock) expands to (self._config['MASTER']['IP'], self._config['MASTER']['PORT']).
# Note that many of them reference each other... this is the Pythonic way.
#
self._network = args[0]
self._config = NETWORK[self._network]
#
self._local = self._config['LOCAL']
self._local_id = self._local['RADIO_ID']
#
self._master = self._config['MASTER']
self._master_stat = self._master['STATUS']
self._master_sock = self._master['IP'], self._master['PORT']
#
self._peers = self._config['PEERS']
#
# This is a regular list to store peers for the IPSC. At times, parsing a simple list is much less
# Spendy than iterating a list of dictionaries... Maybe I'll find a better way in the future. Also
# We have to know when we have a new peer list, so a variable to indicate we do (or don't)
#
args = ()
# Packet 'constructors' - builds the necessary control packets for this IPSC instance.
# This isn't really necessary for anything other than readability (reduction of code golf)
#
# General Items
self.TS_FLAGS = (self._local['MODE'] + self._local['FLAGS'])
#
# Peer Link Maintenance Packets
self.MASTER_REG_REQ_PKT = (MASTER_REG_REQ + self._local_id + self.TS_FLAGS + IPSC_VER)
self.MASTER_ALIVE_PKT = (MASTER_ALIVE_REQ + self._local_id + self.TS_FLAGS + IPSC_VER)
self.PEER_LIST_REQ_PKT = (PEER_LIST_REQ + self._local_id)
self.PEER_REG_REQ_PKT = (PEER_REG_REQ + self._local_id + IPSC_VER)
self.PEER_REG_REPLY_PKT = (PEER_REG_REPLY + self._local_id + IPSC_VER)
self.PEER_ALIVE_REQ_PKT = (PEER_ALIVE_REQ + self._local_id + self.TS_FLAGS)
self.PEER_ALIVE_REPLY_PKT = (PEER_ALIVE_REPLY + self._local_id + self.TS_FLAGS)
#
# Master Link Maintenance Packets
# self.MASTER_REG_REPLY_PKT is not static and must be generated when it is sent
self.MASTER_ALIVE_REPLY_PKT = (MASTER_ALIVE_REPLY + self._local_id + self.TS_FLAGS + IPSC_VER)
self.PEER_LIST_REPLY_PKT = (PEER_LIST_REPLY + self._local_id)
#
# General Link Maintenance Packets
self.DE_REG_REQ_PKT = (DE_REG_REQ + self._local_id)
self.DE_REG_REPLY_PKT = (DE_REG_REPLY + self._local_id)
#
logger.info('(%s) IPSC Instance Created', self._network)
else:
# If we didn't get called correctly, log it!
#
logger.error('(%s) IPSC Instance Could Not be Created... Exiting', self._network)
sys.exit()
# Choose which set of fucntions to use - authenticated or not
if self._local['AUTH_ENABLED']:
self.hashed_packet = self.auth_hashed_packet
self.strip_hash = self.auth_strip_hash
self.validate_auth = self.auth_validate_auth
else:
self.hashed_packet = self.unauth_hashed_packet
self.strip_hash = self.unauth_strip_hash
self.validate_auth = self.unauth_validate_auth
#************************************************
# CALLBACK FUNCTIONS FOR USER PACKET TYPES
#************************************************
def call_mon_status(self, _network, _data):
logger.debug('(%s) Repeater Call Monitor Origin Packet Received: %s',_network, h(_data))
def call_mon_rpt(self, _network, _data):
logger.debug('(%s) Repeater Call Monitor Repeating Packet Received: %s', _network, h(_data))
def call_mon_nack(self, _network, _data):
logger.debug('(%s) Repeater Call Monitor NACK Packet Received: %s', _network, h(_data))
def xcmp_xnl(self, _network, _data):
logger.debug('(%s) XCMP/XNL Packet Received: %s', _network, h(_data))
def repeater_wake_up(self, _network, _data):
logger.debug('(%s) Repeater Wake-Up Packet Received: %s', _network, h(_data))
def group_voice(self, _network, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
logger.debug('(%s) Group Voice Packet Received From: %s, IPSC Peer %s, Destination %s', _network, int_id(_src_sub), int_id(_peerid), int_id(_dst_sub))
def private_voice(self, _network, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
logger.debug('(%s) Private Voice Packet Received From: %s, IPSC Peer %s, Destination %s', _network, int_id(_src_sub), int_id(_peerid), int_id(_dst_sub))
def group_data(self, _network, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
logger.debug('(%s) Group Data Packet Received From: %s, IPSC Peer %s, Destination %s', _network, int_id(_src_sub), int_id(_peerid), int_id(_dst_sub))
def private_data(self, _network, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
logger.debug('(%s) Private Data Packet Received From: %s, IPSC Peer %s, Destination %s', _network, int_id(_src_sub), int_id(_peerid), int_id(_dst_sub))
def unknown_message(self, _network, _packettype, _peerid, _data):
_packettype = h(_packettype)
logger.error('(%s) Unknown Message - Type: %s From: %s Packet: %s', _network, h(_packettype), int_id(_peerid), h(_data))
#************************************************
# IPSC SPECIFIC MAINTENANCE FUNCTIONS
#************************************************
# FUNTIONS FOR IPSC MAINTENANCE ACTIVITIES WE RESPOND TO
# SOMEONE HAS SENT US A KEEP ALIVE - WE MUST ANSWER IT
def peer_alive_req(self, _data, _peerid, _host, _port):
_hex_mode = (_data[5])
_hex_flags = (_data[6:10])
_decoded_mode = process_mode_byte(_hex_mode)
_decoded_flags = process_flags_bytes(_hex_flags)
self._peers[_peerid]['MODE'] = _hex_mode
self._peers[_peerid]['MODE_DECODE'] = _decoded_mode
self._peers[_peerid]['FLAGS'] = _hex_flags
self._peers[_peerid]['FLAGS_DECODE'] = _decoded_flags
# Generate a hashed packet from our template and send it.
peer_alive_reply_packet = self.hashed_packet(self._local['AUTH_KEY'], self.PEER_ALIVE_REPLY_PKT)
self.transport.write(peer_alive_reply_packet, (_host, _port))
self.reset_keep_alive(_peerid) # Might as well reset our own counter, we know it's out there...
logger.debug('(%s) Keep-Alive reply sent to Peer %s', self._network, int_id(_peerid))
# SOMEONE WANTS TO REGISTER WITH US - WE'RE COOL WITH THAT
def peer_reg_req(self, _peerid, _host, _port):
peer_reg_reply_packet = self.hashed_packet(self._local['AUTH_KEY'], self.PEER_REG_REPLY_PKT)
self.transport.write(peer_reg_reply_packet, (_host, _port))
logger.info('(%s) Peer Registration Request From: %s', self._network, int_id(_peerid))
# SOMEONE HAS ANSWERED OUR KEEP-ALIVE REQUEST - KEEP TRACK OF IT
def peer_alive_reply(self, _peerid):
self.reset_keep_alive(_peerid)
self._peers[_peerid]['STATUS']['KEEP_ALIVES_RECEIVED'] += 1
self._peers[_peerid]['STATUS']['KEEP_ALIVE_RX_TIME'] = int(time.time())
logger.debug('(%s) Keep-Alive Reply (we sent the request) Received from Peer %s', self._network, int_id(_peerid))
# SOMEONE HAS ANSWERED OUR REQEST TO REGISTER WITH THEM - KEEP TRACK OF IT
def peer_reg_reply(self, _peerid):
if _peerid in self._peers.keys():
self._peers[_peerid]['STATUS']['CONNECTED'] = True
logger.info('(%s) Registration Reply From: %s', self._network, int_id(_peerid))
# OUR MASTER HAS ANSWERED OUR KEEP-ALIVE REQUEST - KEEP TRACK OF IT
def master_alive_reply(self, _peerid):
self.reset_keep_alive(_peerid)
self._master['STATUS']['KEEP_ALIVES_RECEIVED'] += 1
self._master['STATUS']['KEEP_ALIVE_RX_TIME'] = int(time.time())
logger.debug('(%s) Keep-Alive Reply (we sent the request) Received from the Master %s', self._network, int_id(_peerid))
# OUR MASTER HAS SENT US A PEER LIST - PROCESS IT
def peer_list_reply(self, _data, _peerid):
NETWORK[self._network]['MASTER']['STATUS']['PEER_LIST'] = True
if len(_data) > 18:
process_peer_list(_data, self._network)
logger.debug('(%s) Peer List Reply Recieved From Master %s', self._network, int_id(_peerid))
# OUR MASTER HAS ANSWERED OUR REQUEST TO REGISTER - LOTS OF INFORMATION TO TRACK
def master_reg_reply(self, _data, _peerid):
_hex_mode = _data[5]
_hex_flags = _data[6:10]
_num_peers = _data[10:12]
_decoded_mode = process_mode_byte(_hex_mode)
_decoded_flags = process_flags_bytes(_hex_flags)
self._local['NUM_PEERS'] = int(h(_num_peers), 16)
self._master['RADIO_ID'] = _peerid
self._master['MODE'] = _hex_mode
self._master['MODE_DECODE'] = _decoded_mode
self._master['FLAGS'] = _hex_flags
self._master['FLAGS_DECODE'] = _decoded_flags
self._master_stat['CONNECTED'] = True
self._master_stat['KEEP_ALIVES_OUTSTANDING'] = 0
logger.warning('(%s) Registration response (we requested reg) from the Master %s (%s peers)', self._network, int_id(_peerid), self._local['NUM_PEERS'])
# WE ARE MASTER AND SOMEONE HAS REQUESTED REGISTRATION FROM US - ANSWER IT
def master_reg_req(self, _data, _peerid, _host, _port):
_ip_address = _host
_port = _port
_hex_mode = _data[5]
_hex_flags = _data[6:10]
_decoded_mode = process_mode_byte(_hex_mode)
_decoded_flags = process_flags_bytes(_hex_flags)
self.MASTER_REG_REPLY_PKT = (MASTER_REG_REPLY + self._local_id + self.TS_FLAGS + hex_str_2(self._local['NUM_PEERS']) + IPSC_VER)
master_reg_reply_packet = self.hashed_packet(self._local['AUTH_KEY'], self.MASTER_REG_REPLY_PKT)
self.transport.write(master_reg_reply_packet, (_host, _port))
logger.debug('(%s) Master Registration Packet Received from peer %s', self._network, int_id(_peerid))
# If this entry was NOT already in our list, add it.
if _peerid not in self._peers.keys():
self._peers[_peerid] = {
'IP': _ip_address,
'PORT': _port,
'MODE': _hex_mode,
'MODE_DECODE': _decoded_mode,
'FLAGS': _hex_flags,
'FLAGS_DECODE': _decoded_flags,
'STATUS': {
'CONNECTED': True,
'KEEP_ALIVES_SENT': 0,
'KEEP_ALIVES_MISSED': 0,
'KEEP_ALIVES_OUTSTANDING': 0,
'KEEP_ALIVES_RECEIVED': 0,
'KEEP_ALIVE_RX_TIME': int(time.time())
}
}
self._local['NUM_PEERS'] = len(self._peers)
logger.debug('(%s) Peer Added To Peer List: %s (IPSC now has %s Peers)', self._network, self._peers[_peerid], self._local['NUM_PEERS'])
# WE ARE MASTER AND SOEMONE SENT US A KEEP-ALIVE - ANSWER IT, TRACK IT
def master_alive_req(self, _peerid, _host, _port):
if _peerid in self._peers.keys():
self._peers[_peerid]['STATUS']['KEEP_ALIVES_RECEIVED'] += 1
self._peers[_peerid]['STATUS']['KEEP_ALIVE_RX_TIME'] = int(time.time())
master_alive_reply_packet = self.hashed_packet(self._local['AUTH_KEY'], self.MASTER_ALIVE_REPLY_PKT)
self.transport.write(master_alive_reply_packet, (_host, _port))
logger.debug('(%s) Master Keep-Alive Request Received from peer %s', self._network, int_id(_peerid))
else:
logger.warning('(%s) Master Keep-Alive Request Received from *UNREGISTERED* peer %s', self._network, int_id(_peerid))
# WE ARE MASTER AND A PEER HAS REQUESTED A PEER LIST - SEND THEM ONE
def peer_list_req(self, _peerid):
if _peerid in self._peers.keys():
logger.debug('(%s) Peer List Request from peer %s', self._network, int_id(_peerid))
peer_list_packet = self.PEER_LIST_REPLY_PKT + build_peer_list(self._peers)
peer_list_packet = self.hashed_packet(self._local['AUTH_KEY'], peer_list_packet)
send_to_ipsc(self._network, peer_list_packet)
else:
logger.warning('(%s) Peer List Request Received from *UNREGISTERED* peer %s', self._network, int_id(_peerid))
# Reset the outstanding keep-alive counter for _peerid...
# Used when receiving acks OR when we see traffic from a repeater, since they ignore keep-alives when transmitting
#
def reset_keep_alive(self, _peerid):
if _peerid in self._peers.keys():
self._peers[_peerid]['STATUS']['KEEP_ALIVES_OUTSTANDING'] = 0
self._peers[_peerid]['STATUS']['KEEP_ALIVE_RX_TIME'] = int(time.time())
if _peerid == self._master['RADIO_ID']:
self._master_stat['KEEP_ALIVES_OUTSTANDING'] = 0
# THE NEXT SECTION DEFINES FUNCTIONS THAT MUST BE DIFFERENT FOR HASHED AND UNHASHED PACKETS
# HASHED MEANS AUTHENTICATED IPSC
# UNHASHED MEANS UNAUTHENTICATED IPSC
# NEXT THREE FUNCITONS ARE FOR AUTHENTICATED PACKETS
# Take a packet to be SENT, calculate auth hash and return the whole thing
#
def auth_hashed_packet(self, _key, _data):
_hash = binascii.a2b_hex((hmac_new(_key,_data,sha1)).hexdigest()[:20])
return _data + _hash
# Remove the hash from a packet and return the payload
#
def auth_strip_hash(self, _data):
return _data[:-10]
# Take a RECEIVED packet, calculate the auth hash and verify authenticity
#
def auth_validate_auth(self, _key, _data):
_payload = self.strip_hash(_data)
_hash = _data[-10:]
_chk_hash = binascii.a2b_hex((hmac_new(_key,_payload,sha1)).hexdigest()[:20])
if _chk_hash == _hash:
return True
else:
return False
# NEXT THREE FUNCITONS ARE FOR UN-AUTHENTICATED PACKETS
# There isn't a hash to build, so just return the data
#
def unauth_hashed_packet(self, _key, _data):
return _data
# Remove the hash from a packet and return the payload... except don't
#
def unauth_strip_hash(self, _data):
return _data
# Everything is validated, so just return True
#
def unauth_validate_auth(self, _key, _data):
return True
#************************************************
# TIMED LOOP - CONNECTION MAINTENANCE
#************************************************
# Timed loop initialization (called by the twisted reactor)
#
def startProtocol(self):
# Timed loops for:
# IPSC connection establishment and maintenance
# Reporting/Housekeeping
#
# IF WE'RE NOT THE MASTER...
if not self._local['MASTER_PEER']:
self._peer_maintenance = task.LoopingCall(self.peer_maintenance_loop)
self._peer_maintenance_loop = self._peer_maintenance.start(self._local['ALIVE_TIMER'])
#
# IF WE ARE THE MASTER...
if self._local['MASTER_PEER']:
self._master_maintenance = task.LoopingCall(self.master_maintenance_loop)
self._master_maintenance_loop = self._master_maintenance.start(self._local['ALIVE_TIMER'])
#
# WE ALWAYS DO THIS
self._reporting = task.LoopingCall(self.reporting_loop)
self._reporting_loop = self._reporting.start(REPORTS['REPORT_INTERVAL'])
# Timed loop used for reporting IPSC status
#
def reporting_loop(self):
# Right now, without this, we really don't know anything is happening.
logger.debug('(%s) Periodic Reporting Loop Started', self._network)
if REPORTS['REPORT_PEERS']:
print_master(self._network)
print_peer_list(self._network)
# Timed loop used for IPSC connection Maintenance when we are the MASTER
#
def master_maintenance_loop(self):
logger.debug('(%s) MASTER Connection Maintenance Loop Started', self._network)
update_time = int(time.time())
for peer in self._peers.keys():
keep_alive_delta = update_time - self._peers[peer]['STATUS']['KEEP_ALIVE_RX_TIME']
logger.debug('(%s) Time Since Last KeepAlive Request from Peer %s: %s seconds', self._network, int_id(peer), keep_alive_delta)
if keep_alive_delta > 120:
de_register_peer(self._network, peer)
peer_list_packet = self.PEER_LIST_REPLY_PKT + build_peer_list(self._peers)
peer_list_packet = self.hashed_packet(self._local['AUTH_KEY'], peer_list_packet)
send_to_ipsc(self._network, peer_list_packet)
logger.warning('(%s) Timeout Exceeded for Peer %s, De-registering', self._network, int_id(peer))
# Timed loop used for IPSC connection Maintenance when we are a PEER
#
def peer_maintenance_loop(self):
logger.debug('(%s) PEER Connection Maintenance Loop Started', self._network)
# If the master isn't connected, we have to do that before we can do anything else!
#
if not self._master_stat['CONNECTED']: