63 Commits

Author SHA1 Message Date
Cort Buffington 02dc63075c Time Formatting 2014-05-27 10:28:40 -05:00
Cort Buffington b587c92431 Fixed MASTER_REG_REPLY_PKT
was all messed up, but somehow worked too much of the time anyway!
2014-05-24 14:58:49 -05:00
Cort Buffington 0b1d190791 Fixed MASTER_REG_REPLY peer calc
Was previously really messed up, but worked some of the time by some
crazy twist of fate.
2014-05-24 14:56:53 -05:00
Cort Buffington 844339f27b Master + Repeater Firmware Compatibility
Change the reported IPSC version type to deal with some weird 2nd
registration packet we saw from repeaters with newer firmware
2014-05-24 14:24:24 -05:00
Cort Buffington 46b1ad92f7 RPT Status for TS1 and TS2 Added 2014-05-18 16:45:30 -05:00
Cort Buffington 12eab90fa5 Fixed Repeat State Function 2014-05-18 16:18:21 -05:00
Cort Buffington 8f5c636888 Additional Features 2014-05-18 15:39:06 -05:00
Cort Buffington cd823fb07d Fixed RCM Names 2014-05-18 15:31:10 -05:00
Cort Buffington 7340309638 Fixed RCM Names 2014-05-18 15:30:11 -05:00
Cort Buffington 9e7f9961f4 Additional Features 2014-05-18 15:29:01 -05:00
Cort Buffington d6fafd256a RCM Updates 2014-05-18 15:28:27 -05:00
Cort Buffington 0ee36acc52 Additional Features 2014-05-18 15:25:22 -05:00
Cort Buffington 313078039e Additional Features 2014-05-18 15:24:24 -05:00
Cort Buffington 72721d9870 Additional RCM Features 2014-05-18 15:23:19 -05:00
Cort Buffington ba1d108d06 Update README.md 2014-05-17 20:07:51 -05:00
Cort Buffington 56c66dba5a VERSION 0.2 RELEASE
“Official” 0.2 version. Now has Master support, graceful shutdown, and
a number of stability improvements.
2014-05-17 20:01:26 -05:00
Cort Buffington d6c22c4721 Graceful Shutdown & De-Reg. Handling 2014-05-17 13:18:45 -05:00
Cort Buffington ee96c6752e MASTER: Peer List Broadcast Added 2014-05-17 10:53:27 -05:00
Cort Buffington 0be40df13b Master Sup.: Peer List Dist. Added 2014-05-17 10:40:19 -05:00
Cort Buffington 47a2b7db13 Normalized Naming Conventions for peer/peerid 2014-05-16 10:20:21 -05:00
Cort Buffington e223b26a99 Master Support Useable! 2014-05-16 09:02:45 -05:00
Cort Buffington b5ce0edbae Master Support/Normalization 2014-05-15 22:21:54 -05:00
Cort Buffington 68d52ee976 Master Support Cleanup
Also fixed lack of decoding of some hex strings in debug logging
function calls.
2014-05-15 17:17:54 -05:00
Cort Buffington 8cbf6c678f MASTER SUPPORT WORKING
Needs cleaned up, don’t count on it to be perfect, how to configure it
isn’t yet documented (or very clean). BUT – it works!
2014-05-14 21:42:31 -05:00
Cort Buffington bbe299fc60 MASTER SUPPORT WORKING
Master support now works – not very well tested, don’t count on it to
be perfect. Also, configuration for a master isn’t yet documented.
2014-05-14 21:41:20 -05:00
Cort Buffington 8d451abebc Work on Master Support 2014-05-14 20:58:58 -05:00
Cort Buffington f915131a23 Master Support Partially Working 2014-05-14 19:44:26 -05:00
Cort Buffington 77e446c1f1 additional extensions added 2014-05-14 19:14:19 -05:00
Cort Buffington 9835c3f317 Master Support 2014-05-14 14:19:31 -05:00
Cort Buffington 134a34f22e Work on Master Support 2014-05-14 14:18:33 -05:00
Cort Buffington 9accbafe68 Work on Master Support 2014-05-13 11:18:44 -05:00
Cort Buffington 10c5c0c1ac Work on Master Support 2014-05-12 21:18:04 -05:00
Cort Buffington 79ab1adde5 Documentation Added 2014-05-08 08:41:23 -05:00
Cort Buffington bba5d42511 Work on Master support 2014-05-08 08:39:41 -05:00
Cort Buffington 182fe4f93b Work on Master Support 2014-05-08 08:29:57 -05:00
Cort Buffington 71b35032a0 Data Structure Documentation 2014-05-08 08:10:38 -05:00
Cort Buffington a03c6d9789 Internal Data Structure 2014-05-08 07:28:02 -05:00
Cort Buffington 70127c021f Beginning Master Support 2014-05-07 20:03:03 -05:00
Cort Buffington 4cbabc48aa Trim Superfluous Code 2014-04-29 22:00:38 -05:00
Cort Buffington 80114833b1 Update Copyright 2014-04-28 22:07:34 -05:00
Cort Buffington 8cad9ee839 Update Copyright 2014-04-28 22:05:31 -05:00
Cort Buffington 6ab3fa7bc3 Cleaned superfluous "stuff"
Significantly simplified this file.
2014-04-28 22:01:33 -05:00
Cort Buffington a4a339aabf Move RCM Messages to ipsc.ipsc_message_types.py
Consolidate so that only one file with message-type mappings are used,
so that only one place needs updated.
2014-04-28 21:52:53 -05:00
Cort Buffington 43e11ea19a Removed Class & Inheritance for Unauth IPSCs
Previously, an unauthenticated network used a different class that
subclassed IPSC and overrode the the three functions that affect
authentication. Now, during class instantiation ( with __init__ ), the
set of functions are “aliased” depending on whether or not the IPSC’s
auth flag is set in dmrlink.cfg
2014-04-28 21:42:48 -05:00
Cort Buffington 10012548e9 Update README.md 2014-04-28 20:25:27 -05:00
Cort Buffington 851711bb7e Minor Updates 2014-04-28 20:13:07 -05:00
Cort Buffington 59d4058c84 Update README.md 2014-04-28 07:44:00 -05:00
Cort Buffington 21c68c2186 Add Missouri 2014-04-25 15:42:41 -05:00
Cort Buffington ff9469aaea Add configuration file for playback.py 2014-04-25 15:34:53 -05:00
Cort Buffington 3ffa45f5e8 Add configuration for playback 2014-04-25 15:27:17 -05:00
Cort Buffington 45455322ce Easier config, just use integer string, no hex coding 2014-04-24 21:57:38 -05:00
Cort Buffington 39aa714907 Added TGID and TS parsing 2014-04-24 21:46:01 -05:00
Cort Buffington c3a33c7c85 NOW WORKING!
This module records transmissions on an identified TGID and immediately
plays them back on the same IPSC, TS and TGID.
2014-04-24 21:32:23 -05:00
Cort Buffington 3e8177f80b Cleanup 2014-04-24 08:18:57 -05:00
Cort Buffington dc07204f03 NOT YET WORKING
Playback transmissions by repeating a packet stream… not yet working.
2014-04-20 21:37:59 -05:00
Cort Buffington 31ecf3b733 People Can't Convert DEC to HEX
Added a simple function to convert decimal values into the necessary
TGID hexadecimal strings. Seems like everyone contacting me with
trouble using bridge.py has trouble doing this.
2014-04-10 22:15:44 -05:00
Cort Buffington dbe69bb15e keep-alive packet logging set for debug level 2014-01-21 10:49:13 -06:00
Cort Buffington 620d013e92 Added debug logging for keep-alives 2014-01-21 10:40:52 -06:00
Cort Buffington f25f97a045 Update README.md 2014-01-03 15:22:17 -06:00
Cort Buffington 6197240725 Update README.md 2014-01-03 15:21:39 -06:00
Cort Buffington 98300901cc cfg file note added 2014-01-03 15:03:41 -06:00
Cort Buffington 874b11db7b Daemon Support
Shebangs added to all files expected to be executed, command line
argument for configuration file added (otherwise, it looks for
dmrlink.cfg in the same directory as dmrlink.py) - this divorces it
from the last ties to a shell environment… or at least I think.
2014-01-03 15:01:43 -06:00
Cort Buffington cd217b94b5 Bug fix -- extra information in the cfg file not necessary! 2014-01-02 19:01:28 -06:00
15 changed files with 821 additions and 423 deletions
+3
View File
@@ -5,4 +5,7 @@ Icon
dmrlink.cfg
pub*
bridge_rules.py
playback_config.py
*.pyc
*.bak
*.lcl
-46
View File
@@ -1,46 +0,0 @@
Message Types: (1st byte in the payload)
REGISTRATION EXCHANGE
---------------------
PEER:
90 00 c8 32 68 65 00 00 e0 3c 04 03 04 02 0f 9b 2d d4 d3 af 36 da c2 fe
|--SRC ID-| |MODE| | FLAGS | |IPSC VER| |IPSC VER| |-1st 10 bytes of SHA-1 Hash-|
13120104
MASTER:
91 00 04 c2 c0 6a 00 00 80 5d 00 03 04 03 04 00 5c b8 4e e4 7e 44 b6 bb df dd
|--SRC ID-| |MODE| | FLAGS ||PEERS||IPSC VER| |IPSC VER| |-1st 10 bytes of SHA-1 Hash-|
312000
----
PEER:
96 00 c8 32 68 65 00 00 e0 3c 04 03 04 02 45 d0 a9 c9 07 5c 05 ad 50 67
|--SRC ID-| |MODE| | FLAGS | |IPSC VER| |IPSC VER| |-1st 10 bytes of SHA-1 Hash-|
13120104
MASTER:
97 00 04 c2 c0 6a 00 00 80 5d 04 03 04 02 32 c0 f5 d4 02 28 f5 13 48 22
|--SRC ID-| |MODE| | FLAGS | |IPSC VER| |IPSC VER| |-1st 10 bytes of SHA-1 Hash-|
312000
----
PEER:
92 00 c8 32 68 85 26 37 9b 06 93 9a af dd 08
|--SRC ID-| |-1st 10 bytes of SHA-1 Hash-|
MASTER: (This appears to be the peer list)
93 00 04 c2 c0 00 37 00 04 c2 c3 d1 72 71 e9 c3 5a 6a 00 04 c2 c4 6c c8 a4 3d c3 50 6a 00 04 c2 c5 44 67 16 bb c3 5c 6a 00 c8 32 65 a4 71 4c 0c c3 51 6a ...SHA-1 truncated
|TPE| |--SRC ID-| |-LEN-| |-PEER ID-| |-PEER IP-| |PRT||MDE| |-PEER ID-| |-PEER IP-| |PRT||MDE| |-PEER ID-| |-PEER IP-| |PRT||MDE| |-PEER ID-| |-PEER IP-| |PRT||MDE|
13120104 44 (4) 3120103 209.114.113.233 50010 1&2 3120104 108.200.164.61 50000 1&2 3120105 68.103.22.187 50012 1&2 13120101 164.113.26.12 50001 1&2
------------------------------------------------------------
Copyright (c) 2013 Cortney T. Buffington, N0MJS and the K0USY Group. n0mjs@me.com
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.
+1 -1
View File
@@ -1,4 +1,4 @@
Copyright (c) 2013 Cortney T. Buffington, N0MJS and the K0USY Group. n0mjs@me.com
Copyright (c) 2013, 2014 Cortney T. Buffington, N0MJS and the K0USY Group. n0mjs@me.com
This work is licensed under the Creative Commons Attribution-ShareAlike
3.0 Unported License.To view a copy of this license, visit
+26 -10
View File
@@ -1,10 +1,12 @@
***OFFICIAL VERSION V0.1 RELEASE***
***OFFICIAL VERSION V0.2 RELEASE***
##PROJECT: Open Source IPSC Client.
**PURPOSE:** Troubleshooting IPSC performance issues, and create applications such as logging, bridging, etc..
**IMPACT:** Potential concern from Motorla Solutions, as IPSC is a proprietary
**METHOD:** Reverse engineering by pattern matching and process of elimination
**PURPOSE:** Understanding IPSC, building an open-source IPSC "stack", troubleshooting IPSC performance issues, and as a basis to easily write applications such as logging, bridging, etc.
**IMPACT:** Potential concern from Motorla Solutions, as IPSC is a proprietary protocol.
**METHOD:** Reverse engineering by pattern matching and process of elimination.
**PROPERTY:**
This work represents the author's interpretation of the Motorola(tm) MOTOTRBO(tm) IPSC protocol. It is intended for academic purposes and not for commercial gain. It is not guaranteed to work, or be useful in any way, though it is intended to help IPSC users better understand, and thus maintain and operate, IPSC networks. This work is not affiliated with Motorola Solutions(tm), Inc. in any way. Motorola, Motorola Solutions, MOTOTRBO, ISPC and other terms in this document are registered trademarks of Motorola Solutions, Inc. Other registered trademark terms may be used. These are owned and held by their respective owners.
@@ -13,7 +15,24 @@ This work represents the author's interpretation of the Motorola(tm) MOTOTRBO(tm
This document assumes the reader is familiar with the concepts presented in the Motorola Solutions(tm), Inc. MOTOTRBO(tm) Systems Planner.
**CONVENTIONS USED:**
When communications exchanges are described, the symbols "->" and "<-" are used to denote the *direction* of the communcation. For example, "PEER -> MASTER" indicates communcation from the peer to the master. For each exchange outlined, the initiator of the particular communication will be on the left for the duration of the particular item being illustrated.
When communications exchanges are described, the symbols "->" and "<-" are used to denote the *direction* of the communcation. For example, "PEER -> MASTER" indicates communcation from the peer to the master. For each exchange outlined, the initiator of the particular communication will be on the left for the duration of the particular item being illustrated.
**HOW TO USE THIS SOFTWARE:**
The primary objective is the IPSC "stack" itself, and is represended in dmrlink.py. It gets the majority of work, and the applicaitons are examples to show how dmrlink.py can be used. As such, dmrlink.py, dmrlink.cfg and the ipsc directory are pre-requisites for eveyrthing here. dmrlink.py does very little on it's own, but you should ALWAYS make sure it runs directly, as nothing else will work if it does not. Turn on the logging options, set the logger to DEBUG and watch to make sure everything works right **BEFORE** working with the application files.
dmrlink, optionally, uses three additional files to map dmr identifiers to understandable names. These are the .csv files for subscribers, peers (other repeaters, 3rd party console applications, etc.) and one for common talkgroups. These files are really only meaningful for logging and reporting.
The remaining files are sample applicaitons that sub-class dmrlink. Since dmrlink takes a default action on each packet type, overriding the class methods for particular packet types are how the examples are presented. For example, bridge.py only needs access to group voice packets, so the group_voice class method is overridden to perform the bridging function. In this particlar example, several other class methods are also overridden, but only to set them to do nothing.
**FILES:**
+ ***dmrlink.py, dmrlink.cfg, ipsc (directory):*** Core files for dmrlink to work
+ ***talkgroup_ids.csv, subscriber_ids.csv, peer_ids.csv:*** DMR numeric ID to name mapping files (optional)
+ ***bridge.py, log.py, rcm.py, playback.py:*** Sample applications to demonstrate dmrlink's abilities
+ ***files with SAMPLE in the name:*** Configuration files for certain apps - remove "_SAMPLE" and customize to your needs to use. for example, "dmrlink_SAMPLE.cfg" becomes "dmrlink.cfg"
**CONFIGURATION:**
The configuration file for dmrlink is in ".ini" format, and is self-documented. A warning not in the self-documentation: Don't enable features you do not undertand, it can break dmrlink or the target IPSC (nothing turning off dmrlink shouldn't fix). There are options avaialble because the IPSC protocol appears to make them available, but dmrlink doesn't yet understand them. For exmaple, dmrlink does not process XNL/XCMP. If you enable it, and other peers expect interaction with it, the results may be unpredictable. Chances are, you'll confuse applications like RDAC that require it.
###CONNECTION ESTABLISHMENT AND MAINTENANCE
@@ -228,14 +247,11 @@ Number of peers can be derived from PEER_LIST_LENGTH, as each peer entry is 11 b
**NOTE:**
This is important: If you e-mail me asking about dmrlink and don't use the phrase "here I am, rock you like a hurricane" (at least initially), I will probably delete your e-mail without reading it. I need to be very clear. This software is NOT intended to be an out-of-box replacement for c-Bridge, SmartPTT, GenWatch, RDAC, etc. Please do not contact me with the express intent of wanting to know how to configure it to do the same thing as any of these fine products, because it doesn't do what they do, and will likely never be something you can "just run" and peform those functions with. This is free software, shared with the world so that others can learn from or do useful things with it. The price of open source is that I didn't sell you a product, and there is no support or warranty, or even responsiblity on my part for your use of it. If you want something that is a c-Bridge or SmartPTT, then please go buy one of those products -- they work great, I own both. Using dmrlink will require you to get your hands dirty. Using dmrlink requires basic understanding of Python. If you have read this README.md, have looked for comments or other direction within the files themsleves, and understand I owe you nothing, then please e-mail me, and I'll try to help.
Yes, this note probably sounds heavy handed. I have received a great deal of e-mail from a great deal of folks since posting this project. They have ranged from folks helping with development by fixing problems for me or offering the right way to do something where I clearly did it wrong (which I GREATLY APPRECIATE), all the way down to a growing number of people who seem to want to know things such as, "how do you expect me to use this in place of a c-Bridge?". The answer is, I don't. And I need to minimize those e-mails so that I have time to concentrate on making dmrlink a better piece of software.
This is important: If you e-mail me asking about dmrlink and don't use the phrase "here I am, rock you like a hurricane" (at least initially), I will probably delete your e-mail without reading it. I need to be very clear. This software is NOT intended to be an out-of-box replacement for c-Bridge, SmartPTT, GenWatch, RDAC, etc. Please do not contact me with the express intent of wanting to know how to configure it to do the same thing as any of these fine products, because it doesn't do what they do, and will likely never be something you can "just run" and peform those functions with. This is free software, shared with the world so that others can learn from or do useful things with it. The price of open source is that I didn't sell you a product, and there is no support or warranty, or even responsiblity on my part for your use of it. If you want something that is a c-Bridge or SmartPTT, then please go buy one of those products -- they work great, I own both. Using dmrlink will require you to get your hands dirty. Using dmrlink requires basic understanding of Python. If you have read this README.md, have looked for comments or other direction within the files themsleves, and understand I owe you nothing, then please e-mail me, and I'll try to help if I can.
***73 DE N0MJS***
Copyright (c) 2013 Cortney T. Buffington, N0MJS and the K0USY Group. n0mjs@me.com
Copyright (c) 2013, 2014 Cortney T. Buffington, N0MJS and the K0USY Group. n0mjs@me.com
This work is licensed under the Creative Commons Attribution-ShareAlike
3.0 Unported License.To view a copy of this license, visit
+14 -46
View File
@@ -1,4 +1,4 @@
# Copyright (c) 2013 Cortney T. Buffington, N0MJS and the K0USY Group. n0mjs@me.com
#!/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
@@ -15,6 +15,15 @@ from binascii import b2a_hex as h
import sys
from dmrlink import IPSC, NETWORK, networks, send_to_ipsc, dmr_nat, logger
__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.2a'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = 'n0mjs@me.com'
__status__ = 'Production'
NAT = 0
#NAT = '\x2f\x9b\x80'
@@ -40,7 +49,7 @@ class bridgeIPSC(IPSC):
#************************************************
# CALLBACK FUNCTIONS FOR USER PACKET TYPES
#************************************************
#
def group_voice(self, _network, _src_sub, _dst_group, _ts, _end, _peerid, _data):
if _ts not in self.ACTIVE_CALLS:
self.ACTIVE_CALLS.append(_ts)
@@ -70,51 +79,10 @@ class bridgeIPSC(IPSC):
# Send the packet to all peers in the target IPSC
send_to_ipsc(_target, _tmp_data)
def private_voice(self, _network, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
pass
def group_data(self, _network, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
pass
def private_data(self, _network, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
pass
def call_mon_origin(self, _network, _data):
pass
def call_mon_rpt(self, _network, _data):
pass
def call_mon_nack(self, _network, _data):
pass
def xcmp_xnl(self, _network, _data):
pass
class bridgeUnauthIPSC(bridgeIPSC):
# There isn't a hash to build, so just return the data
#
def hashed_packet(self, _key, _data):
return _data
# Remove the hash from a packet and return the payload... except don't
#
def strip_hash(self, _data):
return _data
# Everything is validated, so just return True
#
def validate_auth(self, _key, _data):
return True
if __name__ == '__main__':
logger.info('DMRlink \'bridge.py\' (c) 2013 N0MJS & the K0USY Group - SYSTEM STARTING...')
logger.info('DMRlink \'bridge.py\' (c) 2013, 2014 N0MJS & the K0USY Group - SYSTEM STARTING...')
for ipsc_network in NETWORK:
if NETWORK[ipsc_network]['LOCAL']['ENABLED']:
if NETWORK[ipsc_network]['LOCAL']['AUTH_ENABLED']:
networks[ipsc_network] = bridgeIPSC(ipsc_network)
else:
networks[ipsc_network] = bridgeUnauthIPSC(ipsc_network)
networks[ipsc_network] = bridgeIPSC(ipsc_network)
reactor.listenUDP(NETWORK[ipsc_network]['LOCAL']['PORT'], networks[ipsc_network])
reactor.run()
reactor.run()
+6 -2
View File
@@ -14,10 +14,14 @@ THIS EXAMPLE WILL NOT WORK AS IT IS - YOU MUST SPECIFY NAMES AND GROUP IDS!!!
NOTE: Timeslot transcoding does not yet work (SRC_TS) and (DST_TS) are ignored
'''
def id(_id):
# Create a 3 byte TGID or UID from an integer
return hex(_id)[2:].rjust(6,'0').decode('hex')
RULES = {
'IPSC_FOO': {
'GROUP_VOICE': [
{'SRC_GROUP': b'\x00\x00\x01', 'SRC_TS': 1, 'DST_NET': 'IPSC_BAR', 'DST_GROUP': b'\x00\x00\x02', 'DST_TS': 1},
{'SRC_GROUP': id(1), 'SRC_TS': 1, 'DST_NET': 'IPSC_BAR', 'DST_GROUP': id(2), 'DST_TS': 1},
# Repeat the above line for as many rules for this IPSC network as you want.
],
'PRIVATE_VOICE': [
@@ -29,7 +33,7 @@ RULES = {
},
'IPSC_BAR:' {
'GROUP_VOICE': [
{'SRC_GROUP': b'\x00\x00\x02', 'SRC_TS': 1, 'DST_NET': 'IPSC_FOO', 'DST_GROUP': b'\x00\x00\x01', 'DST_TS': 1},
{'SRC_GROUP': id(2), 'SRC_TS': 1, 'DST_NET': 'IPSC_FOO', 'DST_GROUP': id(1), 'DST_TS': 1},
# Repeat the above line for as many rules for this IPSC network as you want.
],
'PRIVATE_VOICE': [
+399 -178
View File
@@ -1,4 +1,4 @@
# Copyright (c) 2013 Cortney T. Buffington, N0MJS and the K0USY Group. n0mjs@me.com
#!/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
@@ -6,33 +6,49 @@
# 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
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 Cortney T. Buffington, N0MJS and the K0USY Group'
__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.1'
__version__ = '0.3'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = 'n0mjs@me.com'
__status__ = 'Production'
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
#************************************************
@@ -41,10 +57,13 @@ 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:
config.read('./dmrlink.cfg')
except:
sys.exit('Could not open configuration file, exiting...')
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():
@@ -111,14 +130,21 @@ try:
'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
'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': config.get(section, 'MASTER_IP'),
'PORT': config.getint(section, 'MASTER_PORT')
'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
@@ -168,6 +194,7 @@ try:
except:
sys.exit('Could not parse configuration file, exiting...')
#************************************************
# CONFIGURE THE SYSTEM LOGGER
#************************************************
@@ -222,6 +249,7 @@ dictConfig({
})
logger = logging.getLogger('dmrlink')
#************************************************
# IMPORTING OTHER FILES - '#include'
#************************************************
@@ -277,6 +305,30 @@ except ImportError:
# 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):
@@ -292,7 +344,7 @@ def dmr_nat(_data, _src_id, _nat_id):
#
def get_info(_id, _dict):
if _id in _dict:
return _dict[_id]
return _dict[_id]
return _id
# Determine if the provided peer ID is valid for the provided network
@@ -320,7 +372,8 @@ def send_to_ipsc(_target, _packet):
_peers = _network['PEERS']
# Send to the Master
_network_instance.transport.write(_packet, (_network['MASTER']['IP'], _network['MASTER']['PORT']))
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']:
@@ -399,7 +452,7 @@ def process_flags_bytes(_hex_flags):
'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.
@@ -411,7 +464,7 @@ def process_peer_list(_data, _network):
_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, _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):
@@ -442,18 +495,32 @@ def process_peer_list(_data, _network):
'CONNECTED': False,
'KEEP_ALIVES_SENT': 0,
'KEEP_ALIVES_MISSED': 0,
'KEEP_ALIVES_OUTSTANDING': 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 peerid in NETWORK[_network]['PEERS'].keys():
if peerid not in _temp_peers:
de_register_peer(_network, peerid)
logger.warning('(%s) Peer Deleted (not in new peer list): %s', _network, h(peerid))
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, h(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.
#
@@ -489,25 +556,42 @@ def print_peer_list(_network):
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):
_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']))
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']))
# 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()
#************************************************
#******** ***********
@@ -522,6 +606,10 @@ def print_master(_network):
class IPSC(DatagramProtocol):
#************************************************
# IPSC INSTANCE INSTANTIATION
#************************************************
# Modify the initializer to set up our environment and build the packets
# we need to maintain connections
#
@@ -550,47 +638,53 @@ class IPSC(DatagramProtocol):
#
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)
#
self.TS_FLAGS = (self._local['MODE'] + self._local['FLAGS'])
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)
# 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()
# This is called by REACTOR when it starts, We use it to set up the timed
# loop for each instance of the IPSC engine
#
def startProtocol(self):
# Timed loops for:
# IPSC connection establishment and maintenance
# Reporting/Housekeeping
#
#
self._maintenance = task.LoopingCall(self.maintenance_loop)
self._maintenance_loop = self._maintenance.start(self._local['ALIVE_TIMER'])
#
self._reporting = task.LoopingCall(self.reporting_loop)
self._reporting_loop = self._reporting.start(10)
# 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_origin(self, _network, _data):
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):
@@ -606,33 +700,25 @@ class IPSC(DatagramProtocol):
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):
_dst_sub = get_info(int_id(_dst_sub), talkgroup_ids)
_peerid = get_info(int_id(_peerid), peer_ids)
_src_sub = get_info(int_id(_src_sub), subscriber_ids)
logger.debug('(%s) Group Voice Packet Received From: %s, IPSC Peer %s, Destination %s', _network, _src_sub, _peerid, _dst_sub)
logger.debug('(%s) Group Voice Packet Received From: %s, IPSC Peer %s, Destination %s', _network, h(_src_sub), h(_peerid), h(_dst_sub))
def private_voice(self, _network, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
_dst_sub = get_info(int_id(_dst_sub), subscriber_ids)
_peerid = get_info(int_id(_peerid), peer_ids)
_src_sub = get_info(int_id(_src_sub), subscriber_ids)
logger.debug('(%s) Private Voice Packet Received From: %s, IPSC Peer %s, Destination %s', _network, _src_sub, _peerid, _dst_sub)
logger.debug('(%s) Private Voice Packet Received From: %s, IPSC Peer %s, Destination %s', _network, h(_src_sub), h(_peerid), h(_dst_sub))
def group_data(self, _network, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
_dst_sub = get_info(int_id(_dst_sub), talkgroup_ids)
_peerid = get_info(int_id(_peerid), peer_ids)
_src_sub = get_info(int_id(_src_sub), subscriber_ids)
logger.debug('(%s) Group Data Packet Received From: %s, IPSC Peer %s, Destination %s', _network, _src_sub, _peerid, _dst_sub)
logger.debug('(%s) Group Data Packet Received From: %s, IPSC Peer %s, Destination %s', _network, h(_src_sub), h(_peerid), h(_dst_sub))
def private_data(self, _network, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
_dst_sub = get_info(int_id(_dst_sub), subscriber_ids)
_peerid = get_info(int_id(_peerid), peer_ids)
_src_sub = get_info(int_id(_src_sub), subscriber_ids)
logger.debug('(%s) Private Data Packet Received From: %s, IPSC Peer %s, Destination %s', _network, _src_sub, _peerid, _dst_sub)
logger.debug('(%s) Private Data Packet Received From: %s, IPSC Peer %s, Destination %s', _network, h(_src_sub), h(_peerid), h(_dst_sub))
def unknown_message(self, _network, _packettype, _peerid, _data):
_packettype = h(_packettype)
_peerid = get_info(int_id(_peerid), peer_ids)
logger.error('(%s) Unknown message type encountered\n\tPacket Type: %s\n\tFrom: %s\n\tPacket: %s', _network, _packettype, _peerid, h(_data))
logger.error('(%s) Unknown message type encountered\n\tPacket Type: %s\n\tFrom: %s\n\tPacket: %s', _network, h(_packettype), h(_peerid), h(_data))
#************************************************
# IPSC SPECIFIC MAINTENANCE FUNCTIONS
#************************************************
# 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
@@ -640,23 +726,28 @@ class IPSC(DatagramProtocol):
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
#
# NEXT THREE FUNCITONS ARE FOR AUTHENTICATED PACKETS
#
# Take a packet to be SENT, calculate auth hash and return the whole thing
#
def hashed_packet(self, _key, _data):
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 strip_hash(self, _data):
def auth_strip_hash(self, _data):
return _data[:-10]
# Take a RECEIVED packet, calculate the auth hash and verify authenticity
#
def validate_auth(self, _key, _data):
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])
@@ -665,12 +756,52 @@ class IPSC(DatagramProtocol):
return True
else:
return False
#************************************************
# TIMED LOOP - MY CONNECTION MAINTENANCE
#************************************************
#
# 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 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 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'])
#
self._reporting = task.LoopingCall(self.reporting_loop)
self._reporting_loop = self._reporting.start(10)
# 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)
@@ -678,8 +809,28 @@ class IPSC(DatagramProtocol):
print_master(self._network)
print_peer_list(self._network)
def maintenance_loop(self):
logger.debug('(%s) Periodic Connection Maintenance Loop Started', 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, h(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, h(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!
#
@@ -693,6 +844,7 @@ class IPSC(DatagramProtocol):
# Send keep-alive to the master
master_alive_packet = self.hashed_packet(self._local['AUTH_KEY'], self.MASTER_ALIVE_PKT)
self.transport.write(master_alive_packet, self._master_sock)
logger.debug('(%s) Keep Alive Sent to the Master', self._network)
# If we had a keep-alive outstanding by the time we send another, mark it missed.
if (self._master_stat['KEEP_ALIVES_OUTSTANDING']) > 0:
@@ -719,47 +871,51 @@ class IPSC(DatagramProtocol):
#
if (self._master_stat['CONNECTED'] == True) and (self._master_stat['PEER_LIST'] == False):
# Ask the master for a peer-list
peer_list_req_packet = self.hashed_packet(self._local['AUTH_KEY'], self.PEER_LIST_REQ_PKT)
self.transport.write(peer_list_req_packet, self._master_sock)
logger.info('(%s), No Peer List - Requesting One From the Master', self._network)
if self._local['NUM_PEERS']:
peer_list_req_packet = self.hashed_packet(self._local['AUTH_KEY'], self.PEER_LIST_REQ_PKT)
self.transport.write(peer_list_req_packet, self._master_sock)
logger.info('(%s), No Peer List - Requesting One From the Master', self._network)
else:
self._master_stat['PEER_LIST'] = True
logger.debug('(%s), Skip asking for a Peer List, we are the only Peer', self._network)
# If we do have a peer-list, we need to register with the peers and send keep-alives...
#
if self._master_stat['PEER_LIST']:
# Iterate the list of peers... so we do this for each one.
for peer_id in self._peers.keys():
peer = self._peers[peer_id]
for peer in self._peers.keys():
# We will show up in the peer list, but shouldn't try to talk to ourselves.
if peer_id == self._local_id:
if peer == self._local_id:
continue
# If we haven't registered to a peer, send a registration
if not peer['STATUS']['CONNECTED']:
if not self._peers[peer]['STATUS']['CONNECTED']:
peer_reg_packet = self.hashed_packet(self._local['AUTH_KEY'], self.PEER_REG_REQ_PKT)
self.transport.write(peer_reg_packet, (peer['IP'], peer['PORT']))
logger.info('(%s) Registering with Peer %s', self._network, int_id(peer_id))
self.transport.write(peer_reg_packet, (self._peers[peer]['IP'], self._peers[peer]['PORT']))
logger.info('(%s) Registering with Peer %s', self._network, int_id(peer))
# If we have registered with the peer, then send a keep-alive
elif peer['STATUS']['CONNECTED']:
elif self._peers[peer]['STATUS']['CONNECTED']:
peer_alive_req_packet = self.hashed_packet(self._local['AUTH_KEY'], self.PEER_ALIVE_REQ_PKT)
self.transport.write(peer_alive_req_packet, (peer['IP'], peer['PORT']))
self.transport.write(peer_alive_req_packet, (self._peers[peer]['IP'], self._peers[peer]['PORT']))
logger.debug('(%s) Keep-Alive Sent to the Peer %s', self._network, int_id(peer))
# If we have a keep-alive outstanding by the time we send another, mark it missed.
if peer['STATUS']['KEEP_ALIVES_OUTSTANDING'] > 0:
peer['STATUS']['KEEP_ALIVES_MISSED'] += 1
logger.info('(%s) Peer Keep-Alive Missed for %s', self._network, int_id(peer_id))
if self._peers[peer]['STATUS']['KEEP_ALIVES_OUTSTANDING'] > 0:
self._peers[peer]['STATUS']['KEEP_ALIVES_MISSED'] += 1
logger.info('(%s) Peer Keep-Alive Missed for %s', self._network, int_id(peer))
# If we have missed too many keep-alives, de-register the peer and start over.
if peer['STATUS']['KEEP_ALIVES_OUTSTANDING'] >= self._local['MAX_MISSED']:
peer['STATUS']['CONNECTED'] = False
if self._peers[peer]['STATUS']['KEEP_ALIVES_OUTSTANDING'] >= self._local['MAX_MISSED']:
self._peers[peer]['STATUS']['CONNECTED'] = False
#del peer # Becuase once it's out of the dictionary, you can't use it for anything else.
logger.warning('(%s) Maximum Peer Keep-Alives Missed -- De-registering the Peer: %s', self._network, int_id(peer_id))
logger.warning('(%s) Maximum Peer Keep-Alives Missed -- De-registering the Peer: %s', self._network, int_id(peer))
# Update our stats before moving on...
peer['STATUS']['KEEP_ALIVES_SENT'] += 1
peer['STATUS']['KEEP_ALIVES_OUTSTANDING'] += 1
self._peers[peer]['STATUS']['KEEP_ALIVES_SENT'] += 1
self._peers[peer]['STATUS']['KEEP_ALIVES_OUTSTANDING'] += 1
# For public display of information, etc. - anything not part of internal logging/diagnostics
@@ -771,14 +927,15 @@ class IPSC(DatagramProtocol):
network: string, network name to look up in config
event: string, basic description
info: dict, in the interest of accomplishing as much as possible without code changes.
The dict will typically contain a peer_id so the origin of the event is known.
The dict will typically contain the ID of a peer so the origin of the event is known.
"""
pass
#************************************************
# RECEIVED DATAGRAM - ACT IMMEDIATELY!!!
#************************************************
#************************************************
# MESSAGE RECEIVED - TAKE ACTION
#************************************************
# Actions for received packets by type: For every packet received, there are some things that we need to do:
# Decode some of the info
@@ -787,25 +944,27 @@ class IPSC(DatagramProtocol):
#
# Once they're done, we move on to the processing or callbacks for each packet type.
#
# Callbacks are iterated in the order of "more likely" to "less likely" to reduce processing time
#
def datagramReceived(self, data, (host, port)):
_packettype = data[0:1]
_peerid = data[1:5]
# Authenticate the packet
# AUTHENTICATE THE PACKET
if not self.validate_auth(self._local['AUTH_KEY'], data):
logger.warning('(%s) AuthError: IPSC packet failed authentication. Type %s: Peer ID: %s', self._network, h(_packettype), int(h(_peerid), 16))
logger.warning('(%s) AuthError: IPSC packet failed authentication. Type %s: Peer ID: %s', self._network, h(_packettype), int_id(_peerid))
return
# Strip the hash, we won't need it anymore
# REMOVE SHA-1 AUTHENTICATION HASH: WE NO LONGER NEED IT
data = self.strip_hash(data)
# Packets types that must be originated from a peer (including master peer)
# PACKETS THAT WE RECEIVE FROM ANY VALID PEER OR VALID MASTER
if _packettype in ANY_PEER_REQUIRED:
if not(valid_master(self._network, _peerid) == False or valid_peer(self._peers.keys(), _peerid) == False):
logger.warning('(%s) PeerError: Peer not in peer-list: %s', self._network, int(h(_peerid), 16))
logger.warning('(%s) PeerError: Peer not in peer-list: %s', self._network, int_id(_peerid))
return
# User, as in "subscriber" generated packets - a.k.a someone transmitted
# ORIGINATED BY SUBSCRIBER UNITS - a.k.a someone transmitted
if _packettype in USER_PACKETS:
# Extract commonly used items from the packet header
_src_sub = data[6:9]
@@ -818,35 +977,36 @@ class IPSC(DatagramProtocol):
if _packettype == GROUP_VOICE:
self.reset_keep_alive(_peerid)
self.group_voice(self._network, _src_sub, _dst_sub, _ts, _end, _peerid, data)
self._notify_event(self._network, 'group_voice', {'peer_id': int(h(_peerid), 16)})
self._notify_event(self._network, 'group_voice', {'peer': int_id(_peerid)})
return
elif _packettype == PVT_VOICE:
self.reset_keep_alive(_peerid)
self.private_voice(self._network, _src_sub, _dst_sub, _ts, _end, _peerid, data)
self._notify_event(self._network, 'private_voice', {'peer_id': int(h(_peerid), 16)})
self._notify_event(self._network, 'private_voice', {'peer': int_id(_peerid)})
return
elif _packettype == GROUP_DATA:
self.reset_keep_alive(_peerid)
self.group_data(self._network, _src_sub, _dst_sub, _ts, _end, _peerid, data)
self._notify_event(self._network, 'group_data', {'peer_id': int(h(_peerid), 16)})
self._notify_event(self._network, 'group_data', {'peer': int_id(_peerid)})
return
elif _packettype == PVT_DATA:
self.reset_keep_alive(_peerid)
self.private_data(self._network, _src_sub, _dst_sub, _ts, _end, _peerid, data)
self._notify_event(self._network, 'private_voice', {'peer_id': int(h(_peerid), 16)})
self._notify_event(self._network, 'private_voice', {'peer': int_id(_peerid)})
return
return
# Other peer-required types that we don't do much or anything with yet
# MOTOROLA XCMP/XNL CONTROL PROTOCOL: We don't process these (yet)
elif _packettype == XCMP_XNL:
self.xcmp_xnl(self._network, data)
return
elif _packettype == CALL_MON_ORIGIN:
self.call_mon_origin(self._network, data)
# ORIGINATED BY PEERS, NOT IPSC MAINTENANCE: Call monitoring is all we've found here so far
elif _packettype == CALL_MON_STATUS:
self.call_mon_status(self._network, data)
return
elif _packettype == CALL_MON_RPT:
@@ -857,30 +1017,33 @@ class IPSC(DatagramProtocol):
self.call_mon_nack(self._network, data)
return
# Connection maintenance packets that fall into this category
# IPSC CONNECTION MAINTENANCE MESSAGES
elif _packettype == DE_REG_REQ:
de_register_peer(self._network, _peerid)
logger.warning('(%s) Peer De-Registration Request From: %s', self._network, int(h(_peerid), 16))
logger.warning('(%s) Peer De-Registration Request From: %s', self._network, int_id(_peerid))
return
elif _packettype == DE_REG_REPLY:
logger.warning('(%s) Peer De-Registration Reply From: %s', self._network, int(h(_peerid), 16))
logger.warning('(%s) Peer De-Registration Reply From: %s', self._network, int_id(_peerid))
return
elif _packettype == RPT_WAKE_UP:
self.repeater_wake_up(self._network, data)
logger.debug('(%s) Repeater Wake-Up Packet From: %s', self._network, int(h(_peerid), 16))
logger.debug('(%s) Repeater Wake-Up Packet From: %s', self._network, int_id(_peerid))
return
return
# Packets types that must be originated from a peer
#
# THE FOLLOWING PACKETS ARE RECEIVED ONLY IF WE ARE OPERATING AS A PEER
#
# ONLY ACCEPT FROM A PREVIOUSLY VALIDATED PEER
if _packettype in PEER_REQUIRED:
if not valid_peer(self._peers.keys(), _peerid):
logger.warning('(%s) PeerError: Peer %s not in peer-list', self._network, int(h(_peerid), 16))
logger.warning('(%s) PeerError: Peer %s not in peer-list', self._network, int_id(_peerid))
return
# Packets we send...
# REQUESTS FROM PEERS: WE MUST REPLY IMMEDIATELY FOR IPSC MAINTENANCE
if _packettype == PEER_ALIVE_REQ:
_hex_mode = (data[5])
_hex_flags = (data[6:10])
@@ -895,54 +1058,66 @@ class IPSC(DatagramProtocol):
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))
return
elif _packettype == PEER_REG_REQ:
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(h(_peerid), 16))
logger.info('(%s) Peer Registration Request From: %s', self._network, int_id(_peerid))
return
# Packets we receive...
# ANSWERS FROM REQUESTS WE SENT TO PEERS: WE DO NOT REPLY
elif _packettype == PEER_ALIVE_REPLY:
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))
return
elif _packettype == PEER_REG_REPLY:
if _peerid in self._peers.keys():
self._peers[_peerid]['STATUS']['CONNECTED'] = True
logger.info('(%s) Registration Reply From: %s', self._network, int(h(_peerid), 16))
logger.info('(%s) Registration Reply From: %s', self._network, int_id(_peerid))
return
return
# PACKETS ONLY ACCEPTED FROM OUR MASTER
# Packets types that must be originated from a Master
# Packets we receive...
# PACKETS WE ONLY ACCEPT IF WE HAVE FINISHED REGISTERING WITH OUR MASTER
if _packettype in MASTER_REQUIRED:
if not valid_master(self._network, _peerid):
logger.warning('(%s) MasterError: %s is not the master peer', self._network, int(h(_peerid), 16))
logger.warning('(%s) MasterError: %s is not the master peer', self._network, int_id(_peerid))
return
# ANSWERS FROM REQUESTS WE SENT TO THE MASTER: WE DO NOT REPLY
if _packettype == MASTER_ALIVE_REPLY:
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))
return
elif _packettype == PEER_LIST_REPLY:
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))
return
return
# When we hear from the master, record it's ID, flag that we're connected, and reset the dead counter.
# THIS MEANS WE HAVE SUCCESSFULLY REGISTERED TO OUR MASTER - RECORD MASTER INFORMATION
elif _packettype == MASTER_REG_REPLY:
_hex_mode = (data[5])
_hex_flags = (data[6:10])
_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
@@ -950,42 +1125,86 @@ class IPSC(DatagramProtocol):
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'])
return
# We know about these types, but absolutely don't take an action
# THE FOLLOWING PACKETS ARE RECEIVED ONLLY IF WE ARE OPERATING AS A MASTER
# REQUESTS FROM PEERS: WE MUST REPLY IMMEDIATELY FOR IPSC MAINTENANCE
# REQUEST TO REGISTER TO THE IPSC
elif _packettype == MASTER_REG_REQ:
# We can't operate as a master as of now, so we should never receive one of these.
logger.debug('(%s) Master Registration Packet Received - WE ARE NOT A MASTER!', self._network)
return
# If there's a packet type we don't know about, it should be logged so we can figure it out and take an appropriate action!
_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'])
return
# REQUEST FOR A KEEP-ALIVE REPLY (WE KNOW THE PEER IS STILL ALIVE TOO)
elif _packettype == MASTER_ALIVE_REQ:
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))
return
# REQUEST FOR A PEER LIST
elif _packettype == PEER_LIST_REQ:
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))
return
# PACKET IS OF AN UNKNOWN TYPE. LOG IT AND IDENTTIFY IT!
else:
self.unknown_message(self._network, _packettype, _peerid, data)
return
#************************************************
# Derived Class
# used in the rare event of an
# unauthenticated IPSC network.
#************************************************
class UnauthIPSC(IPSC):
# There isn't a hash to build, so just return the data
#
def hashed_packet(self, _key, _data):
return _data
# Remove the hash from a packet and return the payload... except don't
#
def strip_hash(self, _data):
return _data
# Everything is validated, so just return True
#
def validate_auth(self, _key, _data):
return True
#************************************************
@@ -993,13 +1212,15 @@ class UnauthIPSC(IPSC):
#************************************************
if __name__ == '__main__':
logger.info('DMRlink \'dmrlink.py\' (c) 2013 N0MJS & the K0USY Group - SYSTEM STARTING...')
logger.info('DMRlink \'dmrlink.py\' (c) 2013, 2014 N0MJS & the K0USY Group - SYSTEM STARTING...')
# 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)
networks = {}
for ipsc_network in NETWORK:
if NETWORK[ipsc_network]['LOCAL']['ENABLED']:
if NETWORK[ipsc_network]['LOCAL']['AUTH_ENABLED']:
networks[ipsc_network] = IPSC(ipsc_network)
else:
networks[ipsc_network] = UnauthIPSC(ipsc_network)
networks[ipsc_network] = IPSC(ipsc_network)
reactor.listenUDP(NETWORK[ipsc_network]['LOCAL']['PORT'], networks[ipsc_network])
reactor.run()
+2 -13
View File
@@ -72,22 +72,11 @@ LOG_LEVEL: CRITICAL
# MASTER_PEER: Must be False, we cannot yet act as a master peer.
# AUTH_ENABLED: Do we use authenticated IPSC?
# AUTH_KEY: The Authentication key (up to 40 hex characters)
# MASTER_IP: IP address of the IPSC master
# MASTER_PORT: UDP port of the IPSC master
# MASTER_IP: IP address of the IPSC master (ignored if DMRlink is the master)
# MASTER_PORT: UDP port of the IPSC master (ignored if DMRlinkn is the master)
#
# ...Repeat the block for each IPSC network to join.
#
[IPSC1]
ENABLED: True
RADIO_ID: 1
PORT: 50000
ALIVE_TIMER: 5
TS1_LINK: True
TS2_LINK: True
AUTH_ENABLED: True
AUTH_KEY: 1
MASTER_IP: 1.2.3.4
MASTER_PORT: 50000
[IPSC1]
ENABLED: True
+134
View File
@@ -0,0 +1,134 @@
This is the internal structure dmrlink uses to hold master, peer and local information for each IPSC. the actual numbers are bogus, and the encoded FLAGS and MODE bytes don't match the binary decoding. This example is only to illustrate and document the struture in a "pretty print" type format only. Yeah, you could just pretty print it, but this is a little cleaner and you don't have to go in and take the extra 5 mintues this way.
{
'MASTER': {
'STATUS': {
'KEEP_ALIVES_OUTSTANDING': 1,
'KEEP_ALIVES_MISSED': 0,
'CONNECTED': True,
'KEEP_ALIVES_SENT': 10,
'PEER_LIST': True },
'MODE_DECODE': {
'TS_1': True,
'TS_2': True,
'PEER_MODE': 'DIGITAL',
'PEER_OP': True },
'FLAGS_DECODE': {
'VOICE': True,
'RCM': True,
'XNL_SLAVE': True,
'MASTER': True,
'CON_APP': True,
'XNL_CON': False,
'CSBK': True,
'DATA': True,
'XNL_MASTER': False,
'AUTH': True },
'IP': '10.10.10.1',
'RADIO_ID': '\x00\x00\x00\x01',
'FLAGS': '\x00\x00\xe0\x3d',
'MODE': '\x6a',
'PORT': 50001 },
'PEERS': {
'\x00\x00\x01\x03': {
'STATUS': {
'KEEP_ALIVES_OUTSTANDING': 1,
'KEEP_ALIVES_MISSED': 0,
'CONNECTED': True,
'KEEP_ALIVES_SENT': 8 },
'MODE_DECODE': {
'TS_1': True,
'TS_2': True,
'PEER_MODE': 'DIGITAL',
'PEER_OP': True },
'FLAGS_DECODE': {
'VOICE': True,
'RCM': False,
'XNL_SLAVE': False,
'MASTER': False,
'CON_APP': True,
'XNL_CON': False,
'CSBK': False,
'DATA': True,
'XNL_MASTER': False,
'AUTH': True },
'IP': '10.10.20.1',
'FLAGS': '\x00\x00\x00\x1c',
'MODE': '\x6a',
'PORT': 51990 },
'\x00\x00\x05\x80': {
'STATUS': {
'KEEP_ALIVES_OUTSTANDING': 1,
'KEEP_ALIVES_MISSED': 0,
'CONNECTED': True,
'KEEP_ALIVES_SENT': 8},
'MODE_DECODE': {
'TS_1': True,
'TS_2': True,
'PEER_MODE': 'DIGITAL',
'PEER_OP': True },
'FLAGS_DECODE': {
'VOICE': True,
'RCM': False,
'XNL_SLAVE': False,
'MASTER': False,
'CON_APP': True,
'XNL_CON': False,
'CSBK': False,
'DATA': True,
'XNL_MASTER': False,
'AUTH': True },
'IP': '10.10.20.2',
'FLAGS': '\x00\x00\x00\x01',
'MODE': '\x6a',
'PORT': 50900 },
'\x00\x04\xa2\x37': {
'STATUS': {
'KEEP_ALIVES_OUTSTANDING': 1,
'KEEP_ALIVES_MISSED': 0,
'CONNECTED': True,
'KEEP_ALIVES_SENT': 8 },
'MODE_DECODE': {
'TS_1': True,
'TS_2': True,
'PEER_MODE': 'DIGITAL',
'PEER_OP': True },
'FLAGS_DECODE': {
'VOICE': True,
'RCM': False,
'XNL_SLAVE': False,
'MASTER': False,
'CON_APP': False,
'XNL_CON': False,
'CSBK': True,
'DATA': True,
'XNL_MASTER': True,
'AUTH': True },
'IP': '10.10.20.3',
'FLAGS': '\x00\x00\x00\x01',
'MODE': '\x6a',
'PORT': 50000 },
'LOCAL': {
'TS2_LINK': True,
'AUTH_KEY': '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xab\xcd\xef\xf0',
'CON_APP': True,
'RADIO_ID': '\x00\x67\x03',
'ENABLED': True,
'ALIVE_TIMER': 5,
'TS1_LINK': True,
'RCM': True,
'AUTH_ENABLED': True,
'IPSC_MODE': 'DIGITAL',
'DATA_CALL': True,
'NUM_PEERS': 6,
'PORT': 50001,
'VOICE_CALL': True,
'MASTER_PEER': False,
'CSBK_CALL': True,
'XNL_CALL': True,
'XNL_MASTER': True,
'MODE': '\x6a',
'MAX_MISSED': 20,
'FLAGS': '\x00\x00\xe0\xdc',
'PEER_OPER': True }
}
+58 -7
View File
@@ -1,4 +1,4 @@
# Copyright (c) 2013 Cortney T. Buffington, N0MJS and the K0USY Group. n0mjs@me.com
# Copyright (c) 2013, 2014 Cortney T. Buffington, N0MJS and the K0USY Group. n0mjs@me.com
#
# This work is licensed under the Creative Commons Attribution-ShareAlike
# 3.0 Unported License.To view a copy of this license, visit
@@ -8,7 +8,7 @@
# Known IPSC Message Types
CALL_CONFIRMATION = '\x05' # Confirmation FROM the recipient of a confirmed call.
CALL_MON_ORIGIN = '\x61' # |
CALL_MON_STATUS = '\x61' # |
CALL_MON_RPT = '\x62' # | Exact meaning unknown
CALL_MON_NACK = '\x63' # |
XCMP_XNL = '\x70' # XCMP/XNL control message
@@ -19,8 +19,8 @@ PVT_DATA = '\x84'
RPT_WAKE_UP = '\x85' # Similar to OTA DMR "wake up"
MASTER_REG_REQ = '\x90' # FROM peer TO master
MASTER_REG_REPLY = '\x91' # FROM master TO peer
PEER_LIST_REQ = '\x92'
PEER_LIST_REPLY = '\x93'
PEER_LIST_REQ = '\x92' # From peer TO master
PEER_LIST_REPLY = '\x93' # From master TO peer
PEER_REG_REQ = '\x94' # Peer registration request
PEER_REG_REPLY = '\x95' # Peer registration reply
MASTER_ALIVE_REQ = '\x96' # FROM peer TO master
@@ -44,10 +44,10 @@ IPSC_VER_22 = '\x04'
LINK_TYPE_IPSC = '\x04'
# IPSC Version and Link Type are Used for a 4-byte version field in registration packets
IPSC_VER = LINK_TYPE_IPSC + IPSC_VER_19 + LINK_TYPE_IPSC + IPSC_VER_17
IPSC_VER = LINK_TYPE_IPSC + IPSC_VER_17 + LINK_TYPE_IPSC + IPSC_VER_16
# Packets that must originate from a peer (or master peer)
ANY_PEER_REQUIRED = [GROUP_VOICE, PVT_VOICE, GROUP_DATA, PVT_DATA, CALL_MON_ORIGIN, CALL_MON_RPT, CALL_MON_NACK, XCMP_XNL, RPT_WAKE_UP, DE_REG_REQ]
ANY_PEER_REQUIRED = [GROUP_VOICE, PVT_VOICE, GROUP_DATA, PVT_DATA, CALL_MON_STATUS, CALL_MON_RPT, CALL_MON_NACK, XCMP_XNL, RPT_WAKE_UP, DE_REG_REQ]
# Packets that must originate from a non-master peer
PEER_REQUIRED = [PEER_ALIVE_REQ, PEER_ALIVE_REPLY, PEER_REG_REQ, PEER_REG_REPLY]
@@ -58,6 +58,57 @@ MASTER_REQUIRED = [PEER_LIST_REPLY, MASTER_ALIVE_REPLY]
# User-Generated Packet Types
USER_PACKETS = [GROUP_VOICE, PVT_VOICE, GROUP_DATA, PVT_DATA]
# RCM (Repeater Call Monitor) Constants
TS = {
'\x00': '1',
'\x01': '2'
}
NACK = {
'\x05': 'BSID Start',
'\x06': 'BSID End'
}
TYPE = {
'\x30': 'Private Data Set-Up',
'\x31': 'Group Data Set-Up',
'\x32': 'Private CSBK Set-Up',
'\x47': 'Radio Check Request',
'\x45': 'Call Alert',
'\x4D': 'Remote Monitor Request',
'\x4F': 'Group Voice',
'\x50': 'Private Voice',
'\x51': 'Group Data',
'\x52': 'Private Data',
'\x53': 'All Call'
}
SEC = {
'\x00': 'None',
'\x01': 'Basic',
'\x02': 'Enhanced'
}
STATUS = {
'\x01': 'Active',
'\x02': 'End',
'\x05': 'TS In Use',
'\x08': 'RPT Disabled',
'\x09': 'RF Interference',
'\x0A': 'BSID ON',
'\x0B': 'Timeout',
'\x0C': 'TX Interrupt'
}
REPEAT = {
'\x01': 'Repeating',
'\x02': 'Idle',
'\x03': 'TS Disabled',
'\x04': 'TS Enabled'
}
# Conditions for accepting certain types of messages... the cornerstone of a secure IPSC system :)
'''
REQ_VALID_PEER = [
@@ -71,7 +122,7 @@ REQ_VALID_MASTER = [
]
REQ_MASTER_CONNECTED = [
CALL_MON_ORIGIN,
CALL_MON_STATUS,
CALL_MON_RPT,
CALL_MON_NACK,
XCMP_XNL,
+14 -23
View File
@@ -1,4 +1,4 @@
# Copyright (c) 2013 Cortney T. Buffington, N0MJS and the K0USY Group. n0mjs@me.com
#!/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
@@ -13,7 +13,16 @@ from twisted.internet import reactor
from binascii import b2a_hex as h
import time
from dmrlink import IPSC, UnauthIPSC, NETWORK, networks, get_info, int_id, subscriber_ids, peer_ids, talkgroup_ids, logger
from dmrlink import IPSC, NETWORK, networks, get_info, int_id, subscriber_ids, peer_ids, talkgroup_ids, logger
__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.2a'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = 'n0mjs@me.com'
__status__ = 'Production'
class logIPSC(IPSC):
@@ -71,29 +80,11 @@ class logIPSC(IPSC):
_src_sub = get_info(int_id(_src_sub), subscriber_ids)
print('({}) Private Data Packet Received From: {} To: {}' .format(_network, _src_sub, _dst_sub))
class logUnauthIPSC(logIPSC):
# There isn't a hash to build, so just return the data
#
def hashed_packet(self, _key, _data):
return _data
# Remove the hash from a packet and return the payload... except don't
#
def strip_hash(self, _data):
return _data
# Everything is validated, so just return True
#
def validate_auth(self, _key, _data):
return True
if __name__ == '__main__':
logger.info('DMRlink \'log.py\' (c) 2013 N0MJS & the K0USY Group - SYSTEM STARTING...')
logger.info('DMRlink \'log.py\' (c) 2013, 2014 N0MJS & the K0USY Group - SYSTEM STARTING...')
for ipsc_network in NETWORK:
if NETWORK[ipsc_network]['LOCAL']['ENABLED']:
if NETWORK[ipsc_network]['LOCAL']['AUTH_ENABLED']:
networks[ipsc_network] = logIPSC(ipsc_network)
else:
networks[ipsc_network] = logUnauthIPSC(ipsc_network)
networks[ipsc_network] = logIPSC(ipsc_network)
reactor.listenUDP(NETWORK[ipsc_network]['LOCAL']['PORT'], networks[ipsc_network])
reactor.run()
Executable
+73
View File
@@ -0,0 +1,73 @@
#!/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.
# This is a sample application that "records" and replays transmissions for testing.
from __future__ import print_function
from twisted.internet import reactor
from binascii import b2a_hex as h
import sys, time
from dmrlink import IPSC, NETWORK, networks, logger, dmr_nat, int_id, send_to_ipsc, hex_str_3
__author__ = 'Cortney T. Buffington, N0MJS'
__copyright__ = 'Copyright (c) 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.1b'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = 'n0mjs@me.com'
__status__ = 'pre-alpha'
try:
from playback_config import *
except ImportError:
sys.exit('Configuration file not found or invalid')
HEX_TGID = hex_str_3(TGID)
class playbackIPSC(IPSC):
def __init__(self, *args, **kwargs):
IPSC.__init__(self, *args, **kwargs)
self.CALL_DATA = []
#************************************************
# CALLBACK FUNCTIONS FOR USER PACKET TYPES
#************************************************
#
def group_voice(self, _network, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
if HEX_TGID == _dst_sub and TS == _ts:
if not _end:
if not self.CALL_DATA:
logger.info('(%s) Receiving transmission to be played back from subscriber: %s', _network, int_id(_src_sub))
_tmp_data = _data
#_tmp_data = dmr_nat(_data, _src_sub, NETWORK[_network]['LOCAL']['RADIO_ID'])
self.CALL_DATA.append(_tmp_data)
if _end:
self.CALL_DATA.append(_data)
time.sleep(2)
logger.info('(%s) Playing back transmission from subscriber: %s', _network, int_id(_src_sub))
for i in self.CALL_DATA:
_tmp_data = i
_tmp_data = _tmp_data.replace(_peerid, NETWORK[_network]['LOCAL']['RADIO_ID'])
_tmp_data = self.hashed_packet(NETWORK[_network]['LOCAL']['AUTH_KEY'], _tmp_data)
# Send the packet to all peers in the target IPSC
send_to_ipsc(_network, _tmp_data)
time.sleep(0.06)
self.CALL_DATA = []
if __name__ == '__main__':
logger.info('DMRlink \'playback.py\' (c) 2013, 2014 N0MJS & the K0USY Group - SYSTEM STARTING...')
for ipsc_network in NETWORK:
if NETWORK[ipsc_network]['LOCAL']['ENABLED']:
networks[ipsc_network] = playbackIPSC(ipsc_network)
reactor.listenUDP(NETWORK[ipsc_network]['LOCAL']['PORT'], networks[ipsc_network])
reactor.run()
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env python
#
# THESE ARE THE THINGS THAT YOU NEED TO CONFIGURE TO USE playback.py
# TGID TO LISTEN FOR AND REPEAT ON
TGID = 10
# TIMESLOT TO LISTEN FOR AND REPEAT ON
TS = 0
+83 -96
View File
@@ -1,4 +1,4 @@
# Copyright (c) 2013 Cortney T. Buffington, N0MJS and the K0USY Group. n0mjs@me.com
#!/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
@@ -16,52 +16,28 @@ from twisted.internet import reactor
from twisted.internet import task
from binascii import b2a_hex as h
import time
import datetime
import binascii
import dmrlink
from dmrlink import IPSC, UnauthIPSC, NETWORK, networks, get_info, int_id, subscriber_ids, peer_ids, talkgroup_ids, logger
from dmrlink import IPSC, NETWORK, networks, get_info, int_id, subscriber_ids, peer_ids, talkgroup_ids, logger
# Constants
__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.2a'
__maintainer__ = 'Cort Buffington, N0MJS'
__email__ = 'n0mjs@me.com'
__status__ = 'Production'
TS = {
'\x00': '1',
'\x01': '2'
}
NACK = {
'\x05': 'BSID Start',
'\x06': 'BSID End'
}
TYPE = {
'\x30': 'Private Data Set-Up',
'\x31': 'Group Data Set-Up',
'\x32': 'Private CSBK Set-Up',
'\x47': 'Radio Check Request',
'\x45': 'Call Alert',
'\x4D': 'Remote Monitor Request',
'\x4F': 'Group Voice',
'\x50': 'Private Voice',
'\x51': 'Group Data',
'\x52': 'Private Data',
'\x53': 'All Call'
}
SEC = {
'\x00': 'None',
'\x01': 'Basic',
'\x02': 'Enhanced'
}
STATUS = {
'\x01': 'Active',
'\x02': 'End',
'\x05': 'TS In Use',
'\x0A': 'BSID ON',
'\x0B': 'Timeout',
'\x0C': 'TX Interrupt'
}
try:
from ipsc.ipsc_message_types import *
except ImportError:
sys.exit('IPSC message types file not found or invalid')
status = True
rpt = False
nack = False
class rcmIPSC(IPSC):
@@ -71,18 +47,22 @@ class rcmIPSC(IPSC):
#************************************************
# CALLBACK FUNCTIONS FOR USER PACKET TYPES
#************************************************
def call_mon_origin(self, _network, _data):
_source = _data[1:5]
#
def call_mon_status(self, _network, _data):
if not status:
return
_source = _data[1:5]
_ipsc_src = _data[5:9]
_rf_src = _data[16:19]
_rf_tgt = _data[19:22]
_ts = _data[13]
_status = _data[15]
_type = _data[22]
_sec = _data[24]
_seq_num = _data[9:13]
_ts = _data[13]
_status = _data[15] # suspect [14:16] but nothing in leading byte?
_rf_src = _data[16:19]
_rf_tgt = _data[19:22]
_type = _data[22]
_prio = _data[23]
_sec = _data[24]
_source = get_info(int_id(_source), peer_ids)
_ipsc_src = get_info(int_id(_ipsc_src), peer_ids)
_rf_src = get_info(int_id(_rf_src), subscriber_ids)
@@ -91,68 +71,75 @@ class rcmIPSC(IPSC):
else:
_rf_tgt = get_info(int_id(_rf_tgt), subscriber_ids)
print('Call Monitor - Call Status')
print('TIME: ', datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
print('DATA SOURCE: ', _source)
print('IPSC: ', _network)
print('IPSC Source: ', _ipsc_src)
print('Timeslot: ', TS[_ts])
print('Status: ', STATUS[_status])
print('Type: ', TYPE[_type])
try:
print('Status: ', STATUS[_status])
except KeyError:
print('Status (unknown): ', h(status))
try:
print('Type: ', TYPE[_type])
except KeyError:
print('Type (unknown): ', h(_type))
print('Source Sub: ', _rf_src)
print('Target Sub: ', _rf_tgt)
print()
def call_mon_rpt(self, _network, _data):
#print('({}) Repeater Call Monitor Repeating Packet: {}' .format(_network, h(_data)))
pass
if not rpt:
return
_source = _data[1:5]
_ts1_state = _data[5]
_ts2_state = _data[6]
_source = get_info(int_id(_source), peer_ids)
print('Call Monitor - Repeater State')
print('TIME: ', datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
print('DATA SOURCE: ', _source)
try:
print('TS1 State: ', REPEAT[_ts1_state])
except KeyError:
print('TS1 State (unknown): ', h(_ts1_state))
try:
print('TS2 State: ', REPEAT[_ts2_state])
except KeyError:
print('TS2 State (unknown): ', h(_ts2_state))
print()
def call_mon_nack(self, _network, _data):
#print('({}) Repeater Call Monitor NACK Packet: {}' .format(_network, h(_data)))
pass
if not nack:
return
_source = _data[1:5]
_nack = _data[5]
def xcmp_xnl(self, _network, _data):
#print('({}) XCMP/XNL Packet Received From: {}' .format(_network, h(_data)))
pass
_source = get_info(int_id(_source), peer_ids)
print('Call Monitor - Transmission NACK')
print('TIME: ', datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
print('DATA SOURCE: ', _source)
try:
print('NACK Cause: ', NACK[_nack])
except KeyError:
print('NACK Cause (unknown): ', h(_nack))
print()
def group_voice(self, _network, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
pass
def private_voice(self, _network, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
pass
def group_data(self, _network, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
pass
def private_data(self, _network, _src_sub, _dst_sub, _ts, _end, _peerid, _data):
pass
def repeater_wake_up(self, _network, _data):
_source = _data[1:5]
_source_dec = int_id(_source)
_source_name = get_info(_source_dec, peer_ids)
print('({}) Repeater Wake-Up Packet Received: {} ({})' .format(_network, _source_name, _source_dec))
#print('({}) Repeater Wake-Up Packet Received: {} ({})' .format(_network, _source_name, _source_dec))
class rcmUnauthIPSC(rcmIPSC):
# There isn't a hash to build, so just return the data
#
def hashed_packet(self, _key, _data):
return _data
# Remove the hash from a packet and return the payload... except don't
#
def strip_hash(self, _data):
return _data
# Everything is validated, so just return True
#
def validate_auth(self, _key, _data):
return True
if __name__ == '__main__':
logger.info('DMRlink \'rcm.py\' (c) 2013 N0MJS & the K0USY Group - SYSTEM STARTING...')
logger.info('DMRlink \'rcm.py\' (c) 2013, 2014 N0MJS & the K0USY Group - SYSTEM STARTING...')
for ipsc_network in NETWORK:
if (NETWORK[ipsc_network]['LOCAL']['ENABLED']):
if NETWORK[ipsc_network]['LOCAL']['AUTH_ENABLED'] == True:
networks[ipsc_network] = rcmIPSC(ipsc_network)
else:
networks[ipsc_network] = rcmUnauthIPSC(ipsc_network)
if NETWORK[ipsc_network]['LOCAL']['ENABLED']:
networks[ipsc_network] = rcmIPSC(ipsc_network)
reactor.listenUDP(NETWORK[ipsc_network]['LOCAL']['PORT'], networks[ipsc_network])
reactor.run()
+1 -1
View File
@@ -1 +1 @@
Worldwide,1
Worldwide,1
1 Worldwide 1 Local 2 North America 3 T6-DCI Bridge 3100 Kansas Statewide 3120 Massachussetts 3120 Missouri 3129 Massachussetts 3125 Midwest 3169 Northeast 3172