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

2
.gitignore vendored

@ -118,6 +118,8 @@ hblink.cfg
*.config *.config
*.bak *.bak
rules.py rules.py
config/dashboard_credentials.json
config/encryption_key.secret
subscriber_ids.* subscriber_ids.*
local_subscriber_ids.* local_subscriber_ids.*
peer_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 #!/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 Esteban Mackay, HP3ICC <setcom40@gmail.com>
# Copyright (C) 2025 Bruno Farias, CS8ABG <cs8abg@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> # Copyright (C) 2016-2019 Cortney T. Buffington, N0MJS <n0mjs@me.com>
# #
# This program is free software; you can redistribute it and/or modify # 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 AMI import AMI
#from API import FD_API, FD_APIUserDefinedContext #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. # 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' __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,2021, 2022' __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' __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' __license__ = 'GNU GPLv3'
__maintainer__ = 'Simon Adlem G7RZU' __maintainer__ = 'Esteban Mackay, HP3ICC'
__email__ = 'simon@gb7fr.org.uk' __email__ = 'setcom40@gmail.com'
#Set header bits #Set header bits
#used for slot rewrite and type rewrite #used for slot rewrite and type rewrite
@ -2838,7 +2841,8 @@ if __name__ == '__main__':
if cli_args.LOG_LEVEL: if cli_args.LOG_LEVEL:
CONFIG['LOGGER']['LOG_LEVEL'] = cli_args.LOG_LEVEL CONFIG['LOGGER']['LOG_LEVEL'] = cli_args.LOG_LEVEL
logger = log.config_logging(CONFIG['LOGGER']) 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.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') 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 = killserver_task.start(5)
killserver.addErrback(loopingErrHandle) 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 #more threads
reactor.suggestThreadPoolSize(100) reactor.suggestThreadPoolSize(100)

@ -69,7 +69,7 @@ def process_acls(_config):
# PROCESSED: (False, set([(1, 5), (3120124, 3120124), (3120101, 3120101)])) # PROCESSED: (False, set([(1, 5), (3120124, 3120124), (3120101, 3120101)]))
def acl_build(_acl, _max): def acl_build(_acl, _max):
if not _acl: if not _acl:
return(True, set((const.ID_MIN, _max))) return(True, [(const.ID_MIN, _max)])
acl = [] #set() acl = [] #set()
sections = _acl.split(':') sections = _acl.split(':')
@ -143,8 +143,12 @@ def build_config(_config_file):
'TG1_ACL': config.get(section, 'TGID_TS1_ACL', fallback='PERMIT:ALL'), 'TG1_ACL': config.get(section, 'TGID_TS1_ACL', fallback='PERMIT:ALL'),
'TG2_ACL': config.get(section, 'TGID_TS2_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), '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=''), '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'), 'SERVER_ID': config.getint(section, 'SERVER_ID', fallback=0).to_bytes(4, 'big'),
'DATA_GATEWAY': config.getboolean(section, 'DATA_GATEWAY', fallback=False), 'DATA_GATEWAY': config.getboolean(section, 'DATA_GATEWAY', fallback=False),
'VALIDATE_SERVER_IDS': config.getboolean(section, 'VALIDATE_SERVER_IDS', fallback=True), '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. #If you join the ADN-Systems network, you need to add your ServerID Here.
SERVER_ID: 0 SERVER_ID: 0
; Servidor de seguridad centralizado
URL_SECURITY:
PORT_SECURITY:
PASS_SECURITY:
USERS_PASS: user_passwords.json
HASH_ENCRYPT: encryption_key.secret
[REPORTS] [REPORTS]
[LOGGER] [LOGGER]

@ -45,10 +45,18 @@ REG_ACL: PERMIT:ALL
SUB_ACL: DENY:1 SUB_ACL: DENY:1
TGID_TS1_ACL: PERMIT:ALL TGID_TS1_ACL: PERMIT:ALL
TGID_TS2_ACL: PERMIT:ALL TGID_TS2_ACL: PERMIT:ALL
GEN_STAT_BRIDGES: False GEN_STAT_BRIDGES: True
ALLOW_NULL_PASSPHRASE: True
ANNOUNCEMENT_LANGUAGES: ANNOUNCEMENT_LANGUAGES:
SERVER_ID: 0000 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 # NOT YET WORKING: NETWORK REPORTING CONFIGURATION
# Enabling "REPORT" will configure a socket-based reporting # Enabling "REPORT" will configure a socket-based reporting

@ -8,11 +8,17 @@ SUB_ACL: DENY:1
TGID_TS1_ACL: PERMIT:ALL TGID_TS1_ACL: PERMIT:ALL
TGID_TS2_ACL: PERMIT:ALL TGID_TS2_ACL: PERMIT:ALL
GEN_STAT_BRIDGES: True GEN_STAT_BRIDGES: True
ALLOW_NULL_PASSPHRASE: True
ANNOUNCEMENT_LANGUAGES: ANNOUNCEMENT_LANGUAGES:
SERVER_ID: 0000 SERVER_ID: 0000
DATA_GATEWAY: False 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] [REPORTS]
REPORT: True 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 shutil
import csv import csv
import json
import os
import math 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.TRACE = 5
logging.addLevelName(logging.TRACE, 'TRACE') logging.addLevelName(logging.TRACE, 'TRACE')
@ -972,20 +1021,33 @@ class HBSYSTEM(DatagramProtocol):
_this_peer['LAST_PING'] = time() _this_peer['LAST_PING'] = time()
_sent_hash = _data[8:] _sent_hash = _data[8:]
_salt_str = bytes_4(_this_peer['SALT']) _salt_str = bytes_4(_this_peer['SALT'])
if self._CONFIG['GLOBAL']['ALLOW_NULL_PASSPHRASE'] and len(self._config['PASSPHRASE']) == 0: _radio_id_int = int_id(_peer_id)
_this_peer['CONNECTION'] = 'WAITING_CONFIG' _individual_password = get_user_password(_radio_id_int)
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']) if _individual_password is not None:
else: _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()) _calc_hash = bhex(sha256(_salt_str+self._config['PASSPHRASE']).hexdigest())
if _sent_hash == _calc_hash: if _sent_hash == _calc_hash:
_this_peer['CONNECTION'] = 'WAITING_CONFIG' _this_peer['CONNECTION'] = 'WAITING_CONFIG'
self.send_peer(_peer_id, b''.join([RPTACK, _peer_id])) 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: 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) self.transport.write(b''.join([MSTNAK, _peer_id]), _sockaddr)
del self._peers[_peer_id] 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: else:
self.transport.write(b''.join([MSTNAK, _peer_id]), _sockaddr) 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)) 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 # Install the required support programs
apt-get install python3 python3-pip -y 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 setproctitle
Pyro5 Pyro5
spyne 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] [Service]
User=root User=root
WorkingDirectory=/opt/adn 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] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

Loading…
Cancel
Save

Powered by TurnKey Linux.