pull/20/head
hp3icc 2 weeks ago
parent cc82de74fc
commit d778af3545

2
.gitignore vendored

@ -118,6 +118,8 @@ hblink.cfg
*.config
*.bak
rules.py
config/dashboard_credentials.json
config/encryption_key.secret
subscriber_ids.*
local_subscriber_ids.*
peer_ids.*

@ -0,0 +1,56 @@
modules = ["python-3.12", "bash", "web"]
[workflows]
runButton = "Project"
[[workflows.workflow]]
name = "Project"
mode = "parallel"
author = "agent"
[[workflows.workflow.tasks]]
task = "workflow.run"
args = "FreeDMR Server"
[[workflows.workflow.tasks]]
task = "workflow.run"
args = "Password Dashboard"
[[workflows.workflow]]
name = "FreeDMR Server"
author = "agent"
[workflows.workflow.metadata]
outputType = "console"
[[workflows.workflow.tasks]]
task = "shell.exec"
args = "python bridge_master.py -c ./config/adn.cfg"
[[workflows.workflow]]
name = "Password Dashboard"
author = "agent"
[[workflows.workflow.tasks]]
task = "shell.exec"
args = "python dashboard.py"
waitForPort = 5000
[workflows.workflow.metadata]
outputType = "webview"
[[ports]]
localPort = 4321
externalPort = 3000
[[ports]]
localPort = 5000
externalPort = 80
[agent]
expertMode = true
integrations = ["github:1.0.0"]
[nix]
channel = "stable-25_05"
packages = ["cargo", "gitFull", "glibcLocales", "libiconv", "libxcrypt", "openssl", "pkg-config", "rustc", "wkhtmltopdf"]

@ -0,0 +1,92 @@
Building wheels for collected packages: bitarray, cffi
Building wheel for bitarray (pyproject.toml) ... error
error: subprocess-exited-with-error
× Building wheel for bitarray (pyproject.toml) did not run successfully.
│ exit code: 1
╰─> [23 lines of output]
running bdist_wheel
running build
running build_py
creating build/lib.linux-armv7l-cpython-313/bitarray
copying bitarray/util.py -> build/lib.linux-armv7l-cpython-313/bitarray
copying bitarray/test_util.py -> build/lib.linux-armv7l-cpython-313/bitarray
copying bitarray/test_bitarray.py -> build/lib.linux-armv7l-cpython-313/bitarray
copying bitarray/__init__.py -> build/lib.linux-armv7l-cpython-313/bitarray
copying bitarray/util.pyi -> build/lib.linux-armv7l-cpython-313/bitarray
copying bitarray/__init__.pyi -> build/lib.linux-armv7l-cpython-313/bitarray
copying bitarray/py.typed -> build/lib.linux-armv7l-cpython-313/bitarray
copying bitarray/pythoncapi_compat.h -> build/lib.linux-armv7l-cpython-313/bitarray
copying bitarray/bitarray.h -> build/lib.linux-armv7l-cpython-313/bitarray
copying bitarray/test_281.pickle -> build/lib.linux-armv7l-cpython-313/bitarray
running build_ext
building 'bitarray._bitarray' extension
creating build/temp.linux-armv7l-cpython-313/bitarray
arm-linux-gnueabihf-gcc -fno-strict-overflow -Wsign-compare -DNDEBUG -g -O2 -Wall -fPIC -I/usr/include/python3.13 -c bitarray/_bitarray.c -o build/temp.linux-armv7l-cpython-313/bitarray/_bitarray.o
bitarray/_bitarray.c:12:10: fatal error: Python.h: No such file or directory
12 | #include "Python.h"
| ^~~~~~~~~~
compilation terminated.
error: command '/usr/bin/arm-linux-gnueabihf-gcc' failed with exit code 1
[end of output]
note: This error originates from a subprocess, and is likely not a problem with pip.
ERROR: Failed building wheel for bitarray
Building wheel for cffi (pyproject.toml) ... error
error: subprocess-exited-with-error
× Building wheel for cffi (pyproject.toml) did not run successfully.
│ exit code: 1
╰─> [46 lines of output]
running bdist_wheel
running build
running build_py
creating build/lib.linux-armv7l-cpython-313/cffi
copying src/cffi/verifier.py -> build/lib.linux-armv7l-cpython-313/cffi
copying src/cffi/vengine_gen.py -> build/lib.linux-armv7l-cpython-313/cffi
copying src/cffi/vengine_cpy.py -> build/lib.linux-armv7l-cpython-313/cffi
copying src/cffi/setuptools_ext.py -> build/lib.linux-armv7l-cpython-313/cffi
copying src/cffi/recompiler.py -> build/lib.linux-armv7l-cpython-313/cffi
copying src/cffi/pkgconfig.py -> build/lib.linux-armv7l-cpython-313/cffi
copying src/cffi/model.py -> build/lib.linux-armv7l-cpython-313/cffi
copying src/cffi/lock.py -> build/lib.linux-armv7l-cpython-313/cffi
copying src/cffi/ffiplatform.py -> build/lib.linux-armv7l-cpython-313/cffi
copying src/cffi/error.py -> build/lib.linux-armv7l-cpython-313/cffi
copying src/cffi/cparser.py -> build/lib.linux-armv7l-cpython-313/cffi
copying src/cffi/commontypes.py -> build/lib.linux-armv7l-cpython-313/cffi
copying src/cffi/cffi_opcode.py -> build/lib.linux-armv7l-cpython-313/cffi
copying src/cffi/backend_ctypes.py -> build/lib.linux-armv7l-cpython-313/cffi
copying src/cffi/api.py -> build/lib.linux-armv7l-cpython-313/cffi
copying src/cffi/_shimmed_dist_utils.py -> build/lib.linux-armv7l-cpython-313/cffi
copying src/cffi/_imp_emulation.py -> build/lib.linux-armv7l-cpython-313/cffi
copying src/cffi/__init__.py -> build/lib.linux-armv7l-cpython-313/cffi
running egg_info
writing src/cffi.egg-info/PKG-INFO
writing dependency_links to src/cffi.egg-info/dependency_links.txt
writing entry points to src/cffi.egg-info/entry_points.txt
writing requirements to src/cffi.egg-info/requires.txt
writing top-level names to src/cffi.egg-info/top_level.txt
reading manifest file 'src/cffi.egg-info/SOURCES.txt'
reading manifest template 'MANIFEST.in'
adding license file 'LICENSE'
adding license file 'AUTHORS'
writing manifest file 'src/cffi.egg-info/SOURCES.txt'
copying src/cffi/_cffi_errors.h -> build/lib.linux-armv7l-cpython-313/cffi
copying src/cffi/_cffi_include.h -> build/lib.linux-armv7l-cpython-313/cffi
copying src/cffi/_embedding.h -> build/lib.linux-armv7l-cpython-313/cffi
copying src/cffi/parse_c_type.h -> build/lib.linux-armv7l-cpython-313/cffi
running build_ext
building '_cffi_backend' extension
creating build/temp.linux-armv7l-cpython-313/src/c
arm-linux-gnueabihf-gcc -fno-strict-overflow -Wsign-compare -DNDEBUG -g -O2 -Wall -fPIC -DFFI_BUILDING=1 -DUSE__THREAD -DHAVE_SYNC_SYNCHRONIZE -I/usr/include/ffi -I/usr/include/libffi -I/usr/include/python3.13 -c src/c/_cffi_backend.c -o build/temp.linux-armv7l-cpython-313/src/c/_cffi_backend.o
src/c/_cffi_backend.c:2:10: fatal error: Python.h: No such file or directory
2 | #include <Python.h>
| ^~~~~~~~~~
compilation terminated.
error: command '/usr/bin/arm-linux-gnueabihf-gcc' failed with exit code 1
[end of output]
note: This error originates from a subprocess, and is likely not a problem with pip.
ERROR: Failed building wheel for cffi
Failed to build bitarray cffi
ERROR: Failed to build installable wheels for some pyproject.toml based projects (bitarra

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

@ -1,9 +1,10 @@
#!/usr/bin/env python
#
###############################################################################
# Copyright (C) 2026 Joaquin Madrid Belando, EA5GVK <ea5gvk@gmail.com>
# Copyright (C) 2025 Esteban Mackay, HP3ICC <setcom40@gmail.com>
# Copyright (C) 2025 Bruno Farias, CS8ABG <cs8abg@gmail.com>
# Copyright (C) 2020 Simon Adlem, G7RZU <g7rzu@gb7fr.org.uk>
# Copyright (C) 2020-2023 Simon Adlem, G7RZU <g7rzu@gb7fr.org.uk>
# Copyright (C) 2016-2019 Cortney T. Buffington, N0MJS <n0mjs@me.com>
#
# This program is free software; you can redistribute it and/or modify
@ -91,13 +92,15 @@ from binascii import b2a_hex as ahex
from AMI import AMI
#from API import FD_API, FD_APIUserDefinedContext
from security_downloader import init_security_downloads, periodic_password_download
# Does anybody read this stuff? There's a PEP somewhere that says I should do this.
__author__ = 'Cortney T. Buffington, N0MJS, Forked by Simon Adlem - G7RZU'
__copyright__ = 'Copyright (c) 2016-2019 Cortney T. Buffington, N0MJS and the K0USY Group, Simon Adlem, G7RZU 2020,2021, 2022'
__credits__ = 'Colin Durbridge, G4EML, Steve Zingman, N4IRS; Mike Zingman, N4IRR; Jonathan Naylor, G4KLX; Hans Barthen, DL5DI; Torsten Shultze, DG1HT; Jon Lee, G4TSN; Norman Williams, M6NBP, Eric Craw KF7EEL'
__author__ = 'Cortney T. Buffington, N0MJS, Forked by Simon Adlem - G7RZU, Forked by Esteban Mackay HP3ICC'
__copyright__ = 'Copyright (c) 2016-2019 Cortney T. Buffington, N0MJS and the K0USY Group, Simon Adlem G7RZU 2020-2023, Esteban Mackay, HP3ICC 2024-2026'
__credits__ = 'Colin Durbridge, G4EML, Steve Zingman, N4IRS; Mike Zingman, N4IRR; Jonathan Naylor, G4KLX; Hans Barthen, DL5DI; Torsten Shultze, DG1HT; Jon Lee, G4TSN; Norman Williams, M6NBP, Eric Craw KF7EEL, Simon Adlem - G7RZU, Bruno Farias CS8ABG, Esteban Mackay HP3ICC, Joaquin Madrid Belando EA5GVK'
__license__ = 'GNU GPLv3'
__maintainer__ = 'Simon Adlem G7RZU'
__email__ = 'simon@gb7fr.org.uk'
__maintainer__ = 'Esteban Mackay, HP3ICC'
__email__ = 'setcom40@gmail.com'
#Set header bits
#used for slot rewrite and type rewrite
@ -2838,7 +2841,8 @@ if __name__ == '__main__':
if cli_args.LOG_LEVEL:
CONFIG['LOGGER']['LOG_LEVEL'] = cli_args.LOG_LEVEL
logger = log.config_logging(CONFIG['LOGGER'])
logger.info('\n\nCopyright (c) 2020, 2021, 2022, 2023 Simon G7RZU simon@gb7fr.org.uk')
logger.info('\n\nCopyright (c) 2024-2026 Esteban Mackay, HP3ICC setcom40@gmail.com')
logger.info('\n\nCopyright (c) 2020-2023 Simon G7RZU simon@gb7fr.org.uk')
logger.info('Copyright (c) 2013, 2014, 2015, 2016, 2018, 2019\n\tThe Regents of the K0USY Group. All rights reserved.\n')
logger.debug('(GLOBAL) Logging system started, anything from here on gets logged')
@ -3123,6 +3127,17 @@ if __name__ == '__main__':
killserver = killserver_task.start(5)
killserver.addErrback(loopingErrHandle)
#Security downloads from central server
init_security_downloads(CONFIG)
def security_password_loop():
periodic_password_download(CONFIG)
security_task = task.LoopingCall(security_password_loop)
security = security_task.start(300)
security.addErrback(loopingErrHandle)
logger.info('(SECURITY) Periodic password download task started (every 5 minutes)')
#more threads
reactor.suggestThreadPoolSize(100)

@ -69,7 +69,7 @@ def process_acls(_config):
# PROCESSED: (False, set([(1, 5), (3120124, 3120124), (3120101, 3120101)]))
def acl_build(_acl, _max):
if not _acl:
return(True, set((const.ID_MIN, _max)))
return(True, [(const.ID_MIN, _max)])
acl = [] #set()
sections = _acl.split(':')
@ -143,8 +143,12 @@ def build_config(_config_file):
'TG1_ACL': config.get(section, 'TGID_TS1_ACL', fallback='PERMIT:ALL'),
'TG2_ACL': config.get(section, 'TGID_TS2_ACL', fallback='PERMIT:ALL'),
'GEN_STAT_BRIDGES': config.getboolean(section, 'GEN_STAT_BRIDGES', fallback=True),
'ALLOW_NULL_PASSPHRASE': config.getboolean(section, 'ALLOW_NULL_PASSPHRASE', fallback=True),
'ANNOUNCEMENT_LANGUAGES': config.get(section, 'ANNOUNCEMENT_LANGUAGES', fallback=''),
'URL_SECURITY': config.get(section, 'URL_SECURITY', fallback=''),
'PORT_SECURITY': config.get(section, 'PORT_SECURITY', fallback=''),
'PASS_SECURITY': config.get(section, 'PASS_SECURITY', fallback=''),
'USERS_PASS': config.get(section, 'USERS_PASS', fallback='user_passwords.json'),
'HASH_ENCRYPT': config.get(section, 'HASH_ENCRYPT', fallback='encryption_key.secret'),
'SERVER_ID': config.getint(section, 'SERVER_ID', fallback=0).to_bytes(4, 'big'),
'DATA_GATEWAY': config.getboolean(section, 'DATA_GATEWAY', fallback=False),
'VALIDATE_SERVER_IDS': config.getboolean(section, 'VALIDATE_SERVER_IDS', fallback=True),

@ -8,6 +8,13 @@
#If you join the ADN-Systems network, you need to add your ServerID Here.
SERVER_ID: 0
; Servidor de seguridad centralizado
URL_SECURITY:
PORT_SECURITY:
PASS_SECURITY:
USERS_PASS: user_passwords.json
HASH_ENCRYPT: encryption_key.secret
[REPORTS]
[LOGGER]

@ -45,10 +45,18 @@ REG_ACL: PERMIT:ALL
SUB_ACL: DENY:1
TGID_TS1_ACL: PERMIT:ALL
TGID_TS2_ACL: PERMIT:ALL
GEN_STAT_BRIDGES: False
ALLOW_NULL_PASSPHRASE: True
GEN_STAT_BRIDGES: True
ANNOUNCEMENT_LANGUAGES:
SERVER_ID: 0000
DATA_GATEWAY: False
VALIDATE_SERVER_IDS: False
; Servidor de seguridad centralizado
URL_SECURITY:
PORT_SECURITY:
PASS_SECURITY:
USERS_PASS: user_passwords.json
HASH_ENCRYPT: encryption_key.secret
# NOT YET WORKING: NETWORK REPORTING CONFIGURATION
# Enabling "REPORT" will configure a socket-based reporting

@ -8,11 +8,17 @@ SUB_ACL: DENY:1
TGID_TS1_ACL: PERMIT:ALL
TGID_TS2_ACL: PERMIT:ALL
GEN_STAT_BRIDGES: True
ALLOW_NULL_PASSPHRASE: True
ANNOUNCEMENT_LANGUAGES:
SERVER_ID: 0000
DATA_GATEWAY: False
VALIDATE_SERVER_IDS: True
VALIDATE_SERVER_IDS: False
; Servidor de seguridad centralizado
URL_SECURITY:
PORT_SECURITY:
PASS_SECURITY:
USERS_PASS: user_passwords.json
HASH_ENCRYPT: encryption_key.secret
[REPORTS]
REPORT: True

@ -0,0 +1,161 @@
[GLOBAL]
PATH: ./
PING_TIME: 10
MAX_MISSED: 3
USE_ACL: True
REG_ACL: PERMIT:ALL
SUB_ACL: DENY:1
TGID_TS1_ACL: PERMIT:ALL
TGID_TS2_ACL: PERMIT:ALL
GEN_STAT_BRIDGES: True
ANNOUNCEMENT_LANGUAGES:
SERVER_ID: 0000
DATA_GATEWAY: False
VALIDATE_SERVER_IDS: False
; Servidor de seguridad centralizado
URL_SECURITY:
PORT_SECURITY:
PASS_SECURITY:
USERS_PASS: user_passwords.json
HASH_ENCRYPT: encryption_key.secret
[REPORTS]
REPORT: True
REPORT_INTERVAL: 10
REPORT_PORT: 4321
REPORT_CLIENTS: 127.0.0.1
[LOGGER]
LOG_FILE: /dev/null
LOG_HANDLERS: console-timed
LOG_LEVEL: DEBUG
LOG_NAME: ADN
[ALIASES]
TRY_DOWNLOAD: True
PATH: ./data/
PEER_FILE: peer_ids.json
SUBSCRIBER_FILE: subscriber_ids.json
TGID_FILE: talkgroup_ids.json
PEER_URL: https://adn.systems/files/peer_ids.json
SUBSCRIBER_URL: https://adn.systems/files/subscriber_ids.json
TGID_URL: https://adn.systems/files/talkgroup_ids.json
SERVER_ID_URL: https://adn.systems/files/server_ids.tsv
CHECKSUM_URL: https://adn.systems/files/file_checksums.json
LOCAL_SUBSCRIBER_FILE: subscriber_ids.json
STALE_DAYS: 1
SUB_MAP_FILE: sub_map.pkl
SERVER_ID_FILE: server_ids.tsv
CHECKSUM_FILE: file_checksums.json
KEYS_FILE: keys.json
#Control server shared allstar instance via dial / AMI
[ALLSTAR]
ENABLED: False
USER:llcgi
PASS: mypass
SERVER: my.asl.server
PORT: 5038
NODE: 0000
[OBP-TEST]
MODE: OPENBRIDGE
ENABLED: False
IP:
PORT: 62044
NETWORK_ID: 1
PASSPHRASE: mypass
TARGET_IP:
TARGET_PORT: 62044
USE_ACL: True
SUB_ACL: DENY:1
TGID_ACL: DENY:0-82,92-199,800-899,9990-9999,900999
RELAX_CHECKS: True
ENHANCED_OBP: True
PROTO_VER: 5
[SYSTEM]
MODE: MASTER
ENABLED: True
REPEAT: True
MAX_PEERS: 1
EXPORT_AMBE: False
IP:
PORT: 56400
PASSPHRASE: passw0rd
GROUP_HANGTIME: 5
USE_ACL: True
REG_ACL: DENY:1
SUB_ACL: DENY:1
TGID_TS1_ACL: PERMIT:ALL
TGID_TS2_ACL: PERMIT:ALL
DEFAULT_UA_TIMER: 60
SINGLE_MODE: False
VOICE_IDENT: False
TS1_STATIC:
TS2_STATIC:
DEFAULT_REFLECTOR: 0
ANNOUNCEMENT_LANGUAGE: es_ES
GENERATOR: 100
ALLOW_UNREG_ID: False
PROXY_CONTROL: True
OVERRIDE_IDENT_TG:
[ECHO]
MODE: MASTER
ENABLED: True
REPEAT: True
MAX_PEERS: 1
EXPORT_AMBE: False
IP: 127.0.0.1
PORT: 54917
PASSPHRASE: passw0rd
GROUP_HANGTIME: 5
USE_ACL: True
REG_ACL: DENY:1
SUB_ACL: DENY:1
TGID_TS1_ACL: DENY:ALL
TGID_TS2_ACL: PERMIT:9990
DEFAULT_UA_TIMER: 1
SINGLE_MODE: True
VOICE_IDENT: False
TS1_STATIC:
TS2_STATIC:9990
DEFAULT_REFLECTOR: 0
ANNOUNCEMENT_LANGUAGE: en_GB
GENERATOR: 0
ALLOW_UNREG_ID: True
PROXY_CONTROL: False
OVERRIDE_IDENT_TG:
[D-APRS]
MODE: MASTER
ENABLED: True
REPEAT: False
MAX_PEERS: 1
EXPORT_AMBE: False
IP:
PORT: 52555
PASSPHRASE: passw0rd
GROUP_HANGTIME: 0
USE_ACL: True
REG_ACL: DENY:1
SUB_ACL: DENY:1
TGID_TS1_ACL: PERMIT:ALL
TGID_TS2_ACL: PERMIT:ALL
DEFAULT_UA_TIMER: 10
SINGLE_MODE: False
VOICE_IDENT: False
TS1_STATIC:
TS2_STATIC:
DEFAULT_REFLECTOR: 0
ANNOUNCEMENT_LANGUAGE: es_ES
GENERATOR: 2
ALLOW_UNREG_ID: True
PROXY_CONTROL: False
OVERRIDE_IDENT_TG:

@ -0,0 +1 @@
{"subscriber_ids":"e3d7df1cea54b717e670ae310d92aa7fbb5230c18a5e42a5063b38d396562706f12ee487c5e6c61afa71bebbd01db267d68570bc11e877b088e5b5058220e77a","peer_ids":"d223516beafe7b427402f86642477a5d1d1479c88ae7368f1e8ba7f0728a0355271fbe59ba4e85773759e8824015aaa663a117b1d5e704f3886bf9e27e790e66","talkgroup_ids":"143624f8278a654d4d30769ccc18cdc77e135c6f372216cdad35b6fbfcf767043c38e886c150d3ae4757ce8125c49212b362a949de4b6d7e58e0b88f02bba5a1","server_ids":"536c1a52705bad24a3eb5167b1ea2af9f07e08618039c5e1ef49b65f361911b2c968bb58506ffe99798758c60c49b0de8660f2d924a2a66246a5b11dd0062893","timestamp":1767603901}

@ -0,0 +1,52 @@
Country OPB Net ID IP/Hostname Password Port
ADN_2131_Andorra 2131 2131.adn.systems passw0rd 62031
ADN_7221_Argentina 7221 7221.adn.systems passw0rd 62031
ADN_2061_Belgium 2061 2061.adn.systems passw0rd 62031
ADN_7241_Brazil 7241 7241.adn.systems passw0rd 62031
ADN_2841_Bulgaria 2841 2841.adn.systems passw0rd 62031
ADN_2842_Bulgaria 2842 2842.adn.systems passw0rd 62031
ADN_3021_Canada 3021 3021.adn.systems passw0rd 62031
ADN_7301_Chile 7301 7301.adn.systems passw0rd 62031
ADN_7121_Costa_Rica 7121 7121.adn.systems passw0rd 62031
ADN_3681_Cuba 3681 3681.adn.systems passw0rd 62031
ADN_3701_Dominican_Republic 3701 3701.adn.systems passw0rd 62031
ADN_7401_Ecuador 7401 7401.adn.systems passw0rd 62031
ADN_7061_El_Salvador 7061 7061.adn.systems passw0rd 62031
ADN_2481_Estonia 2481 2481.adn.systems passw0rd 62031
ADN_2081_France-Francophonie 2081 2081.adn.systems passw0rd 62031
ADN_2082_France_Digital 2082 2082.adn.systems passw0rd 62031
ADN_2084_France_Hauts_de_France 2084 2084.adn.systems passw0rd 62031
ADN_2087_France_Limousin 2087 2087.adn.systems passw0rd 62031
ADN_2083_France_Yvelines 2083 2083.adn.systems passw0rd 62031
ADN_2021_Greece 2021 2021.adn.systems passw0rd 62031
ADN_2022_Greece_Hellas_Node 2022 2022.adn.systems passw0rd 62031
ADN_7081_Honduras 7081 7081.adn.systems passw0rd 62031
ADN_2221_Italy 2221 2221.adn.systems passw0rd 62031
ADN_2224_Italy_Multi-Net 2224 2224.adn.systems passw0rd 62031
ADN_2223_Italy_Sardinia 2223 2223.adn.systems passw0rd 62031
ADN_2222_Italy_Sud 2222 2222.adn.systems passw0rd 62031
ADN_3341_Mexico 3341 3341.adn.systems passw0rd 62031
ADN_6041_Morocco 6041 6041.adn.systems passw0rd 62031
ADN_7101_Nicaragua 7101 7101.adn.systems passw0rd 62031
ADN_7141_Panama 7141 7141.adn.systems passw0rd 62031
ADN_2601_Poland 2601 2601.adn.systems passw0rd 55580
ADN_2681_Portugal 2681 2681.adn.systems passw0rd 62031
ADN_3301_Puerto_Rico 3301 3301.adn.systems passw0rd 62031
ADN_6471_Reunion 6471 6471.adn.systems passw0rd 62031
ADN_2201_Serbia 2201 2201.adn.systems passw0rd 62031
ADN_2141_Spain 2141 2141.adn.systems passw0rd 62031
ADN_2142_Spain 2142 2142.adn.systems passw0rd 62031
ADN_2280_Switzerland 2280 2280.adn.systems passw0rd 62031
ADN_2281_Switzerland 2281 2281.adn.systems passw0rd 62031
ADN_2283_Switzerland 2283 2283.adn.systems passw0rd 62031
ADN_5201_Thailand 5201 5201.adn.systems passw0rd 62031
ADN_2861_Turkey 2861 2861.adn.systems passw0rd 62031
ADN_2551_Ukraine 2551 2551.adn.systems passw0rd 62031
ADN_2341_United_Kingdom 2341 2341.adn.systems passw0rd 62031
ADN_3103_USA_Central 3103 3103.adn.systems passw0rd 62031
ADN_3102_USA_East 3102 3102.adn.systems passw0rd 62031
ADN_3104_USA_South 3104 3104.adn.systems passw0rd 62031
ADN_3105_USA_West 3105 3105.adn.systems passw0rd 62031
ADN_7481_Uruguay 7481 7481.adn.systems passw0rd 62031
ADN_7341_Venezuela 7341 7341.adn.systems passw0rd 62031
ADN_7342_Venezuela 7342 7342.adn.systems passw0rd 62031
1 Country OPB Net ID IP/Hostname Password Port
2 ADN_2131_Andorra 2131 2131.adn.systems passw0rd 62031
3 ADN_7221_Argentina 7221 7221.adn.systems passw0rd 62031
4 ADN_2061_Belgium 2061 2061.adn.systems passw0rd 62031
5 ADN_7241_Brazil 7241 7241.adn.systems passw0rd 62031
6 ADN_2841_Bulgaria 2841 2841.adn.systems passw0rd 62031
7 ADN_2842_Bulgaria 2842 2842.adn.systems passw0rd 62031
8 ADN_3021_Canada 3021 3021.adn.systems passw0rd 62031
9 ADN_7301_Chile 7301 7301.adn.systems passw0rd 62031
10 ADN_7121_Costa_Rica 7121 7121.adn.systems passw0rd 62031
11 ADN_3681_Cuba 3681 3681.adn.systems passw0rd 62031
12 ADN_3701_Dominican_Republic 3701 3701.adn.systems passw0rd 62031
13 ADN_7401_Ecuador 7401 7401.adn.systems passw0rd 62031
14 ADN_7061_El_Salvador 7061 7061.adn.systems passw0rd 62031
15 ADN_2481_Estonia 2481 2481.adn.systems passw0rd 62031
16 ADN_2081_France-Francophonie 2081 2081.adn.systems passw0rd 62031
17 ADN_2082_France_Digital 2082 2082.adn.systems passw0rd 62031
18 ADN_2084_France_Hauts_de_France 2084 2084.adn.systems passw0rd 62031
19 ADN_2087_France_Limousin 2087 2087.adn.systems passw0rd 62031
20 ADN_2083_France_Yvelines 2083 2083.adn.systems passw0rd 62031
21 ADN_2021_Greece 2021 2021.adn.systems passw0rd 62031
22 ADN_2022_Greece_Hellas_Node 2022 2022.adn.systems passw0rd 62031
23 ADN_7081_Honduras 7081 7081.adn.systems passw0rd 62031
24 ADN_2221_Italy 2221 2221.adn.systems passw0rd 62031
25 ADN_2224_Italy_Multi-Net 2224 2224.adn.systems passw0rd 62031
26 ADN_2223_Italy_Sardinia 2223 2223.adn.systems passw0rd 62031
27 ADN_2222_Italy_Sud 2222 2222.adn.systems passw0rd 62031
28 ADN_3341_Mexico 3341 3341.adn.systems passw0rd 62031
29 ADN_6041_Morocco 6041 6041.adn.systems passw0rd 62031
30 ADN_7101_Nicaragua 7101 7101.adn.systems passw0rd 62031
31 ADN_7141_Panama 7141 7141.adn.systems passw0rd 62031
32 ADN_2601_Poland 2601 2601.adn.systems passw0rd 55580
33 ADN_2681_Portugal 2681 2681.adn.systems passw0rd 62031
34 ADN_3301_Puerto_Rico 3301 3301.adn.systems passw0rd 62031
35 ADN_6471_Reunion 6471 6471.adn.systems passw0rd 62031
36 ADN_2201_Serbia 2201 2201.adn.systems passw0rd 62031
37 ADN_2141_Spain 2141 2141.adn.systems passw0rd 62031
38 ADN_2142_Spain 2142 2142.adn.systems passw0rd 62031
39 ADN_2280_Switzerland 2280 2280.adn.systems passw0rd 62031
40 ADN_2281_Switzerland 2281 2281.adn.systems passw0rd 62031
41 ADN_2283_Switzerland 2283 2283.adn.systems passw0rd 62031
42 ADN_5201_Thailand 5201 5201.adn.systems passw0rd 62031
43 ADN_2861_Turkey 2861 2861.adn.systems passw0rd 62031
44 ADN_2551_Ukraine 2551 2551.adn.systems passw0rd 62031
45 ADN_2341_United_Kingdom 2341 2341.adn.systems passw0rd 62031
46 ADN_3103_USA_Central 3103 3103.adn.systems passw0rd 62031
47 ADN_3102_USA_East 3102 3102.adn.systems passw0rd 62031
48 ADN_3104_USA_South 3104 3104.adn.systems passw0rd 62031
49 ADN_3105_USA_West 3105 3105.adn.systems passw0rd 62031
50 ADN_7481_Uruguay 7481 7481.adn.systems passw0rd 62031
51 ADN_7341_Venezuela 7341 7341.adn.systems passw0rd 62031
52 ADN_7342_Venezuela 7342 7342.adn.systems passw0rd 62031

@ -0,0 +1 @@
<EFBFBD>}<7D>.

@ -0,0 +1,76 @@
# Servicios Systemd para ADN Systems DMR
## Archivos de Servicio
- **adn-bridge.service** - Servidor DMR principal (bridge_master.py)
- **adn-dashboard.service** - Panel de administración de contraseñas (dashboard.py)
## Instalación
### 1. Copiar el proyecto a /opt/adn-dmr
```bash
sudo mkdir -p /opt/adn-dmr
sudo cp -r /ruta/del/proyecto/* /opt/adn-dmr/
```
### 2. Copiar los servicios a systemd
```bash
sudo cp adn-bridge.service /etc/systemd/system/
sudo cp adn-dashboard.service /etc/systemd/system/
```
### 3. Recargar systemd
```bash
sudo systemctl daemon-reload
```
### 4. Habilitar servicios para arranque automático
```bash
sudo systemctl enable adn-bridge.service
sudo systemctl enable adn-dashboard.service
```
### 5. Iniciar los servicios
```bash
sudo systemctl start adn-bridge.service
sudo systemctl start adn-dashboard.service
```
## Comandos Útiles
### Ver estado de los servicios
```bash
sudo systemctl status adn-bridge.service
sudo systemctl status adn-dashboard.service
```
### Ver logs en tiempo real
```bash
sudo journalctl -u adn-bridge.service -f
sudo journalctl -u adn-dashboard.service -f
```
### Reiniciar servicios
```bash
sudo systemctl restart adn-bridge.service
sudo systemctl restart adn-dashboard.service
```
### Detener servicios
```bash
sudo systemctl stop adn-bridge.service
sudo systemctl stop adn-dashboard.service
```
### Deshabilitar arranque automático
```bash
sudo systemctl disable adn-bridge.service
sudo systemctl disable adn-dashboard.service
```
## Notas
- Los servicios asumen que el proyecto está instalado en `/opt/adn-dmr`
- Si usas otra ruta, edita `WorkingDirectory` en los archivos .service
- El dashboard escucha en el puerto 5000
- Asegúrate de tener las dependencias Python instaladas antes de iniciar

@ -0,0 +1,16 @@
[Unit]
Description=ADN Systems DMR Bridge Master - Servidor DMR principal
After=network.target
[Service]
Type=simple
User=root
WorkingDirectory=/opt/adn-dmr
ExecStart=/usr/bin/python3 bridge_master.py -c ./config/adn.cfg
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target

@ -65,9 +65,58 @@ from urllib.request import urlopen
import shutil
import csv
import json
import os
import math
from password_crypto import decrypt_password
USER_PASSWORDS = {}
USER_PASSWORDS_FILE = 'data/user_passwords.json'
USER_PASSWORDS_LAST_LOAD = 0
USER_PASSWORDS_RELOAD_INTERVAL = 10
def load_user_passwords():
global USER_PASSWORDS, USER_PASSWORDS_LAST_LOAD
current_time = time()
if current_time - USER_PASSWORDS_LAST_LOAD < USER_PASSWORDS_RELOAD_INTERVAL:
return USER_PASSWORDS
try:
if os.path.exists(USER_PASSWORDS_FILE):
with open(USER_PASSWORDS_FILE, 'r') as f:
data = json.load(f)
encrypted_passwords = data.get('passwords', {})
USER_PASSWORDS = {}
for radio_id, pwd in encrypted_passwords.items():
USER_PASSWORDS[radio_id] = decrypt_password(pwd)
logger.debug('(AUTH) Loaded %d individual passwords from %s', len(USER_PASSWORDS), USER_PASSWORDS_FILE)
else:
USER_PASSWORDS = {}
except (FileNotFoundError, json.JSONDecodeError) as e:
logger.warning('(AUTH) Could not load user passwords: %s', e)
USER_PASSWORDS = {}
USER_PASSWORDS_LAST_LOAD = current_time
return USER_PASSWORDS
def get_user_password(radio_id):
passwords = load_user_passwords()
radio_id_str = str(radio_id)
if not radio_id_str.isdigit():
return None
if radio_id_str in passwords:
return passwords[radio_id_str].encode('utf-8')
if len(radio_id_str) == 9:
base_id = radio_id_str[:7]
if base_id in passwords:
logger.debug('(AUTH) Radio ID %s using base ID %s password', radio_id_str, base_id)
return passwords[base_id].encode('utf-8')
return None
logging.TRACE = 5
logging.addLevelName(logging.TRACE, 'TRACE')
@ -972,20 +1021,33 @@ class HBSYSTEM(DatagramProtocol):
_this_peer['LAST_PING'] = time()
_sent_hash = _data[8:]
_salt_str = bytes_4(_this_peer['SALT'])
if self._CONFIG['GLOBAL']['ALLOW_NULL_PASSPHRASE'] and len(self._config['PASSPHRASE']) == 0:
_this_peer['CONNECTION'] = 'WAITING_CONFIG'
self.send_peer(_peer_id, b''.join([RPTACK, _peer_id]))
logger.info('(%s) Peer %s has completed the login exchange successfully', self._system, _this_peer['RADIO_ID'])
else:
_radio_id_int = int_id(_peer_id)
_individual_password = get_user_password(_radio_id_int)
if _individual_password is not None:
_calc_hash = bhex(sha256(_salt_str + _individual_password).hexdigest())
if _sent_hash == _calc_hash:
_this_peer['CONNECTION'] = 'WAITING_CONFIG'
self.send_peer(_peer_id, b''.join([RPTACK, _peer_id]))
logger.info('(%s) Peer %s has completed the login exchange successfully (individual password)', self._system, _this_peer['RADIO_ID'])
else:
logger.warning('(%s) Peer %s has FAILED the login exchange (wrong individual password)', self._system, _this_peer['RADIO_ID'])
self.transport.write(b''.join([MSTNAK, _peer_id]), _sockaddr)
del self._peers[_peer_id]
elif len(self._config['PASSPHRASE']) > 0:
_calc_hash = bhex(sha256(_salt_str+self._config['PASSPHRASE']).hexdigest())
if _sent_hash == _calc_hash:
_this_peer['CONNECTION'] = 'WAITING_CONFIG'
self.send_peer(_peer_id, b''.join([RPTACK, _peer_id]))
logger.info('(%s) Peer %s has completed the login exchange successfully', self._system, _this_peer['RADIO_ID'])
logger.info('(%s) Peer %s has completed the login exchange successfully (global passphrase)', self._system, _this_peer['RADIO_ID'])
else:
logger.info('(%s) Peer %s has FAILED the login exchange successfully', self._system, _this_peer['RADIO_ID'])
logger.warning('(%s) Peer %s has FAILED the login exchange (wrong global passphrase)', self._system, _this_peer['RADIO_ID'])
self.transport.write(b''.join([MSTNAK, _peer_id]), _sockaddr)
del self._peers[_peer_id]
else:
logger.warning('(%s) Peer %s has FAILED - no individual password configured and no global passphrase', self._system, _this_peer['RADIO_ID'])
self.transport.write(b''.join([MSTNAK, _peer_id]), _sockaddr)
del self._peers[_peer_id]
else:
self.transport.write(b''.join([MSTNAK, _peer_id]), _sockaddr)
logger.info('(%s) Login challenge from Radio ID that has not logged in: %s', self._system, int_id(_peer_id))

@ -2,5 +2,5 @@
# Install the required support programs
apt-get install python3 python3-pip -y
pip3 install -r requirements.txt
pip3 install -r requirements.txt --break-system-packages

@ -0,0 +1,55 @@
#!/bin/bash
#
# Script de instalacion para ADN Systems DMR Peer Server
# Debian 13 (Trixie) - ARM64/ARMv7
#
# Este script instala las dependencias necesarias para compilar
# y ejecutar el servidor DMR en sistemas ARM con Debian 13
#
set -e
echo "=============================================="
echo " ADN Systems DMR Peer Server"
echo " Instalador para Debian 13 (Trixie) ARM"
echo "=============================================="
echo ""
if [ "$EUID" -ne 0 ]; then
echo "Error: Este script debe ejecutarse como root (sudo)"
exit 1
fi
echo "[1/4] Actualizando repositorios..."
apt-get update
echo ""
echo "[2/4] Instalando dependencias de compilacion..."
apt-get install -y \
build-essential \
python3-dev \
python3-pip \
python3-venv \
libffi-dev \
libssl-dev \
git
echo ""
echo "[3/4] Instalando dependencias de Python..."
pip3 install --break-system-packages -r requirements.txt
echo ""
echo "[4/4] Verificando instalacion..."
python3 -c "import bitarray; import twisted; import flask; print('Todas las dependencias instaladas correctamente')"
echo ""
echo "=============================================="
echo " Instalacion completada exitosamente"
echo "=============================================="
echo ""
echo "Para iniciar el servidor:"
echo " python3 bridge_master.py -c ./config/adn.cfg"
echo ""
echo "Para iniciar el dashboard:"
echo " python3 dashboard.py"
echo ""

@ -0,0 +1,48 @@
from cryptography.fernet import Fernet
import os
import base64
import hashlib
ENCRYPTION_KEY_FILE = 'config/encryption_key.secret'
def get_or_create_key():
if os.path.exists(ENCRYPTION_KEY_FILE):
with open(ENCRYPTION_KEY_FILE, 'rb') as f:
return f.read()
else:
key = Fernet.generate_key()
os.makedirs(os.path.dirname(ENCRYPTION_KEY_FILE), exist_ok=True)
with open(ENCRYPTION_KEY_FILE, 'wb') as f:
f.write(key)
return key
def get_fernet():
key = get_or_create_key()
return Fernet(key)
def encrypt_password(password):
if not password:
return password
fernet = get_fernet()
encrypted = fernet.encrypt(password.encode('utf-8'))
return encrypted.decode('utf-8')
def decrypt_password(encrypted_password):
if not encrypted_password:
return encrypted_password
try:
fernet = get_fernet()
decrypted = fernet.decrypt(encrypted_password.encode('utf-8'))
return decrypted.decode('utf-8')
except Exception:
return encrypted_password
def is_encrypted(value):
if not value:
return False
try:
fernet = get_fernet()
fernet.decrypt(value.encode('utf-8'))
return True
except Exception:
return False

@ -0,0 +1,209 @@
# ADN Systems DMR Peer Server
## Overview
ADN Systems DMR Peer Server is a fork of FreeDMR, implementing a Digital Mobile Radio (DMR) network server. Launched in April 2024 by international amateur radio enthusiasts, it operates on an Open Bridge Protocol (OBP) fostering a decentralized network architecture. The system handles DMR voice and data communication, acting as a conference bridge/reflector that routes traffic between connected systems (repeaters, hotspots, peers) based on configurable bridge rules.
## User Preferences
Preferred communication style: Simple, everyday language.
## System Architecture
### Core Protocol Implementation
**Problem**: Need to implement HomeBrew Repeater Protocol (HBP) and Open Bridge Protocol for DMR communication
**Solution**: Python-based protocol handlers using Twisted framework for asynchronous networking
- `hblink.py` serves as the core protocol engine implementing HBSYSTEM and OPENBRIDGE classes
- Supports both peer and master/server modes for network topology flexibility
- Uses DatagramProtocol for UDP-based DMR packet handling
- Implements custom authentication using SHA256/BLAKE2b hashing with HMAC for security
**Rationale**: Twisted provides battle-tested async I/O suitable for handling multiple simultaneous connections with low latency requirements critical for voice communication.
### Bridge/Routing Architecture
**Problem**: Route voice traffic between multiple DMR systems based on talkgroups and timeslots
**Solution**: Conference bridge pattern implemented in `bridge_master.py` and `bridge.py`
- Traffic routing based on rules defined in `rules.py` configuration
- Systems can be dynamically activated/deactivated on conference bridges
- Supports timeout-based automatic disconnection
- Static routing option available for permanent connections
**Alternatives Considered**: End-to-end routing was rejected in favor of conference bridge approach for better scalability and simpler management.
**Pros**: Flexible routing, easy management, scalable
**Cons**: Systems must individually join bridges (not transparent end-to-end)
### Proxy Architecture
**Problem**: Allow multiple hotspots/repeaters to connect through a single public IP address
**Solution**: UDP proxy implementation in `hotspot_proxy_v2.py` and `hotspot_proxy_self_service.py`
- Dynamic port allocation for incoming connections
- Connection tracking with timeout-based cleanup
- Blacklist support for access control
- Self-service variant includes database-backed client management
**Rationale**: Many amateur radio operators are behind NAT/firewalls; proxy enables connectivity without port forwarding.
### Voice Synthesis System
**Problem**: Generate voice announcements for system events (linking, unlinking, status)
**Solution**: AMBE (Advanced Multi-Band Excitation) voice codec integration
- Pre-recorded indexed voice files in multiple languages (en_GB, es_ES, fr_FR, etc.)
- `read_ambe.py` handles reading/parsing of AMBE audio files
- `mk_voice.py` generates HBP-compliant voice packets from AMBE data
- `playback.py` and `play_ambe.py` handle voice injection into streams
**Rationale**: Provides accessible feedback to users without requiring external TTS systems, maintains compatibility with DMR audio codecs.
### Individual Password Authentication
**Problem**: Need individual password authentication per Radio ID (indicativo) for enhanced security
**Solution**: JSON-based password storage with automatic reload
- Password file: `data/user_passwords.json` stores individual passwords by Radio ID
- Web dashboard: `dashboard.py` provides admin interface for password management
- Auto-reload: Passwords are reloaded every 10 seconds without server restart
- Fallback: Global passphrase used if no individual password is configured
- Mandatory authentication: Individual or global password required (no null passphrase allowed)
**Suffix Support for Radio IDs**:
- Radio IDs of 9 digits (ej: 214501601-214501699) can use the password of their 7-digit base ID (ej: 2145016)
- Useful for users with multiple hotspots/devices using suffix extensions
- Priority: Exact ID match first, then base ID match
**Authentication Priority**:
1. Individual password for exact Radio ID (if configured)
2. Individual password for base ID (7 digits, for 9-digit IDs with suffix)
3. Global passphrase (if configured)
4. Reject connection (no valid credentials)
**Dashboard Credentials**: Stored in `config/dashboard_credentials.json` file (not uploaded to GitHub)
- Supports multiple administrators with the following format:
```json
{
"admins": [
{"user": "admin", "password": "admin123"},
{"user": "operador1", "password": "clave456"}
]
}
```
- Also supports legacy single-admin format for backwards compatibility
**Dashboard Web con Acceso Dual**:
- Pagina principal (`/`): Selector entre acceso usuario y administrador
- Acceso Usuario (`/user/login`): Login con Radio ID y contrasena para cambiar su propia contrasena
- Acceso Administrador (`/admin/login`): Login para gestionar todas las contrasenas
- Los usuarios pueden cambiar su contrasena sin necesitar al administrador
**Busqueda por Indicativo (Callsign)**:
- Descarga automatica diaria de la base de datos de RadioID.net (`data/user.csv`)
- Formato CSV con campos: RADIO_ID, CALLSIGN, FIRST_NAME, LAST_NAME, CITY, STATE, COUNTRY
- Funcion de busqueda en el panel de administracion para encontrar Radio IDs por indicativo
- Muestra toda la informacion del registro: nombre, ciudad, estado, pais
- Soporta busqueda parcial (ej: "EA4" muestra todos los indicativos que empiezan por EA4)
- Limita resultados a 20 para mejor rendimiento
- Boton "Usar ID" para autorellenar el formulario de agregar contrasena
### Servidor de Seguridad Centralizado (Rama: descentralizada)
**Problema**: Gestionar credenciales de forma centralizada desde un servidor remoto
**Solucion**: Descarga automatica de archivos de seguridad desde servidor central
**Configuracion en adn.cfg seccion [GLOBAL]**:
- `URL_SECURITY`: IP o DNS del servidor central de seguridad
- `PORT_SECURITY`: Puerto del servidor central
- `PASS_SECURITY`: Contrasena de autenticacion del administrador
- `USERS_PASS`: Nombre del archivo de contrasenas (default: user_passwords.json)
- `HASH_ENCRYPT`: Nombre del archivo de clave de encriptacion (default: encryption_key.secret)
**Comportamiento de Descarga**:
- `encryption_key.secret`: Se descarga SOLO al arrancar el servidor, sobrescribe el existente
- `user_passwords.json`: Se descarga cada 5 minutos, compara contenido antes de actualizar
**Sintaxis de Descarga (curl)**:
```
curl -L "http://URL_SECURITY:PORT_SECURITY/descargar?pass=PASS_SECURITY&encryption_key.secret"
curl -L "http://URL_SECURITY:PORT_SECURITY/descargar?pass=PASS_SECURITY&user_passwords.json"
```
**Seguridad y Tolerancia a Fallos**:
- Si la descarga falla, se conservan los archivos locales existentes
- No se permite contrasena nula (ALLOW_NULL_PASSPHRASE eliminado)
- Autenticacion obligatoria: contrasena individual o global requerida
**Archivos Descargados**:
- `config/encryption_key.secret`: Clave de encriptacion para descifrar contrasenas
- `data/user_passwords.json`: Archivo JSON con contrasenas encriptadas por Radio ID
### Configuration Management
**Problem**: Complex multi-system configuration with ACLs, bridges, and network parameters
**Solution**: INI-based configuration parsed by `config.py`
- Centralized configuration in `config/adn.cfg`
- ACL (Access Control List) processing for registration and talkgroup filtering
- Support for both whitelist and blacklist approaches
- Language preferences and multi-language support via `languages.py`
### Monitoring and Reporting
**Problem**: Need visibility into system operation, connections, and traffic
**Solution**: Network-based reporting protocol
- `report_receiver.py` and `report_sql.py` consume events from the core server
- Pickle-based serialization for config/bridge state transmission
- Opcode-based protocol defined in `reporting_const.py`
- SQL integration option for persistent event logging
**Rationale**: Separates monitoring concerns from core routing logic, enables multiple monitoring tools.
### Asterisk Integration
**Problem**: Integration with Asterisk PBX for advanced features (app_rpt)
**Solution**: Asterisk Manager Interface (AMI) client in `AMI.py`
- Sends RPT (repeater) commands to Asterisk
- Uses Twisted LineReceiver for line-based protocol handling
- Supports node-specific command routing
### API Layer
**Problem**: External systems need programmatic access to server functions
**Solution**: XML-RPC and SOAP API implementation
- `API.py` implements Spyne-based SOAP service (FD_API)
- Peer validation and authentication endpoints
- `api_client.py` provides example XML-RPC client
- Key-based authentication for peer systems
## External Dependencies
### Network Protocols
- **Twisted** (>= 16.3.0) - Asynchronous networking framework for all protocol handling
- **dmr_utils3** (>= 0.1.19) - DMR protocol utilities for packet encoding/decoding, BPTC, Golay error correction
### Database Systems
- **MySQL/MariaDB** - Via twisted.enterprise.adbapi and MySQLdb
- Used by `proxy_db.py` for self-service proxy client management
- Stores client registrations, authentication, and connection tracking
- Optional for report_sql.py event logging
### Remote Object Communication
- **Pyro5** - Python Remote Objects for inter-process communication
- Used by proxy services for distributed architecture
- Enables separation of proxy components across processes/hosts
### Third-Party Services
- **radioid.net API** - DMR ID database lookups
- `peer_ids.json`, `subscriber_ids.json`, `talkgroup_ids.json` downloaded from external sources
- Cached locally with staleness checking via `utils.py::try_download()`
- Provides callsign/name resolution for DMR IDs
### Supporting Libraries
- **bitarray/bitstring** - Binary data manipulation for DMR packet construction
- **configparser** - INI file parsing for hblink.cfg
- **setproctitle** - Process naming for easier system monitoring
- **resettabletimer** - Timeout management for connection tracking
- **hashlib/hmac** - Cryptographic functions for authentication
### Language/Voice Assets
- Pre-recorded AMBE voice files in Audio/ directory
- Multiple language support (en_GB, es_ES, fr_FR, de_DE, dk_DK, it_IT, no_NO, pl_PL, se_SE, pt_PT, cy_GB, el_GR, th_TH, CW)
- Voice file indexing via i8n_voice_map.py

@ -7,3 +7,8 @@ resettabletimer>=0.7.0
setproctitle
Pyro5
spyne
flask
mysql-connector-python
cryptography
markdown
pdfkit

@ -0,0 +1,210 @@
#!/usr/bin/env python
import os
import logging
import urllib.request
import urllib.error
import tempfile
import shutil
import socket
from time import time
logger = logging.getLogger(__name__)
DOWNLOAD_INTERVAL_PASSWORDS = 300
DOWNLOAD_INTERVAL_ENCRYPTION = None
_last_passwords_download = 0
_last_passwords_size = 0
_last_passwords_content = None
def resolve_hostname(hostname, timeout=10):
try:
old_timeout = socket.getdefaulttimeout()
socket.setdefaulttimeout(timeout)
ip = socket.gethostbyname(hostname)
socket.setdefaulttimeout(old_timeout)
logger.debug('(SECURITY) Resolved %s to %s', hostname, ip)
return ip
except socket.gaierror as e:
logger.error('(SECURITY) DNS resolution failed for %s: %s', hostname, str(e))
return None
except Exception as e:
logger.error('(SECURITY) Unexpected error resolving %s: %s', hostname, str(e))
return None
def build_download_url(config, filename):
url_security = config['GLOBAL'].get('URL_SECURITY', '').strip()
port_security = config['GLOBAL'].get('PORT_SECURITY', '').strip()
pass_security = config['GLOBAL'].get('PASS_SECURITY', '').strip()
if not url_security or not port_security or not pass_security:
return None, None
try:
socket.inet_aton(url_security)
host = url_security
except socket.error:
host = resolve_hostname(url_security)
if not host:
logger.error('(SECURITY) Could not resolve hostname: %s', url_security)
return None, None
url = f"http://{host}:{port_security}/descargar?pass={pass_security}&file={filename}"
return url, url_security
def download_file_safely(url, dest_path, timeout=60):
try:
temp_fd, temp_path = tempfile.mkstemp()
os.close(temp_fd)
try:
logger.debug('(SECURITY) Attempting download from: %s', url)
req = urllib.request.Request(url)
req.add_header('User-Agent', 'ADN-Systems-DMR/1.0')
with urllib.request.urlopen(req, timeout=timeout) as response:
content = response.read()
if len(content) == 0:
logger.warning('(SECURITY) Downloaded file is empty, keeping existing: %s', dest_path)
os.unlink(temp_path)
return False
with open(temp_path, 'wb') as f:
f.write(content)
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
shutil.move(temp_path, dest_path)
logger.info('(SECURITY) Successfully downloaded: %s (%d bytes)', dest_path, len(content))
return True
except urllib.error.HTTPError as e:
logger.error('(SECURITY) HTTP error downloading %s: %s (Code: %d)', dest_path, str(e), e.code)
if os.path.exists(temp_path):
os.unlink(temp_path)
return False
except urllib.error.URLError as e:
logger.error('(SECURITY) URL error downloading %s: %s', dest_path, str(e.reason))
if os.path.exists(temp_path):
os.unlink(temp_path)
return False
except socket.timeout:
logger.error('(SECURITY) Timeout downloading %s', dest_path)
if os.path.exists(temp_path):
os.unlink(temp_path)
return False
except Exception as e:
logger.error('(SECURITY) Unexpected error downloading %s: %s', dest_path, str(e))
return False
def download_encryption_key(config):
hash_encrypt = config['GLOBAL'].get('HASH_ENCRYPT', 'encryption_key.secret').strip()
dest_path = os.path.join('config', hash_encrypt)
url, original_host = build_download_url(config, hash_encrypt)
if not url:
logger.debug('(SECURITY) Security server not configured, skipping encryption key download')
return False
logger.info('(SECURITY) Downloading encryption key from central server...')
return download_file_safely(url, dest_path)
def download_user_passwords(config, force=False):
global _last_passwords_download, _last_passwords_size, _last_passwords_content
current_time = time()
if not force and (current_time - _last_passwords_download) < DOWNLOAD_INTERVAL_PASSWORDS:
return False
users_pass = config['GLOBAL'].get('USERS_PASS', 'user_passwords.json').strip()
dest_path = os.path.join('data', users_pass)
url, original_host = build_download_url(config, users_pass)
if not url:
logger.debug('(SECURITY) Security server not configured, skipping passwords download')
return False
try:
logger.debug('(SECURITY) Downloading passwords from: %s', url)
req = urllib.request.Request(url)
req.add_header('User-Agent', 'ADN-Systems-DMR/1.0')
with urllib.request.urlopen(req, timeout=60) as response:
new_content = response.read()
new_size = len(new_content)
if new_size == 0:
logger.warning('(SECURITY) Downloaded passwords file is empty, keeping existing')
_last_passwords_download = current_time
return False
if _last_passwords_content is not None:
if new_content == _last_passwords_content:
logger.debug('(SECURITY) Passwords file unchanged, no update needed')
_last_passwords_download = current_time
return False
temp_fd, temp_path = tempfile.mkstemp()
os.close(temp_fd)
with open(temp_path, 'wb') as f:
f.write(new_content)
os.makedirs(os.path.dirname(dest_path), exist_ok=True)
shutil.move(temp_path, dest_path)
_last_passwords_content = new_content
_last_passwords_size = new_size
_last_passwords_download = current_time
logger.info('(SECURITY) Successfully updated passwords file: %s (%d bytes)', dest_path, new_size)
return True
except urllib.error.HTTPError as e:
logger.error('(SECURITY) HTTP error downloading passwords: %s (Code: %d)', str(e), e.code)
_last_passwords_download = current_time
return False
except urllib.error.URLError as e:
logger.error('(SECURITY) URL error downloading passwords: %s', str(e.reason))
_last_passwords_download = current_time
return False
except socket.timeout:
logger.error('(SECURITY) Timeout downloading passwords')
_last_passwords_download = current_time
return False
except Exception as e:
logger.error('(SECURITY) Unexpected error downloading passwords: %s', str(e))
_last_passwords_download = current_time
return False
def init_security_downloads(config):
url_security = config['GLOBAL'].get('URL_SECURITY', '').strip()
if not url_security:
logger.info('(SECURITY) Central security server not configured')
return False
port_security = config['GLOBAL'].get('PORT_SECURITY', '').strip()
logger.info('(SECURITY) Initializing centralized security downloads...')
logger.info('(SECURITY) Security server: %s:%s', url_security, port_security)
try:
socket.inet_aton(url_security)
logger.info('(SECURITY) Using IP address: %s', url_security)
except socket.error:
resolved_ip = resolve_hostname(url_security)
if resolved_ip:
logger.info('(SECURITY) Resolved hostname %s to IP: %s', url_security, resolved_ip)
else:
logger.error('(SECURITY) Failed to resolve hostname: %s', url_security)
return False
download_encryption_key(config)
download_user_passwords(config, force=True)
return True
def periodic_password_download(config):
download_user_passwords(config)

@ -5,7 +5,7 @@ After=syslog.target network.target
[Service]
User=root
WorkingDirectory=/opt/adn
ExecStart=/usr/bin/python3 bridge_master.py -c ./config/adn.cfg -r ./config/rules.py
ExecStart=/usr/bin/python3 bridge_master.py -c ./config/adn.cfg
[Install]
WantedBy=multi-user.target

Loading…
Cancel
Save

Powered by TurnKey Linux.