Documentation audit for the recent changes:
* MODE terminology MASTER->SERVER, PEER/CLIENT->OUTBOUND corrected across
README, INSTALL, CONFIGURING, ACLS, and the dashboard docs (the code only
accepts SERVER/OUTBOUND/OPENBRIDGE, so the old names in docs were wrong).
* README gains a top-of-file breaking-change warning (OBP routing moved to
OBP_BRIDGES; system-mode rename) with a pointer to the migration tool.
* CONFIGURING documents REPORT_TRANSPORT/REPORT_SOCKET (Unix-socket feed) and
a "Migrating from the old inline form" section for tools/migrate_obp_rules.py.
* dashboard/README documents HBLINK_TRANSPORT/HBLINK_SOCKET and corrects the
stale "changes appear on the next snapshot" claim (connect/disconnect are
granular pushes now).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
9.8 KiB
HBlink3 Access Control Lists (ACLs)
Access Control Lists are one of the most powerful — and most under-documented —
features in HBlink3. They let you decide, packet by packet, who may use your
system, which subscribers may talk through it, and which talkgroups are
allowed on each timeslot. This document explains the ACL grammar, the exact
matching rules (verified against the code in config.py and hblink.py), a set
of worked examples from simple to advanced, and honest guidance on performance.
ACLs are cheap and safe to use. Read the Performance section and you will see there is almost no reason not to use them.
1. The two-part grammar
Every ACL is a single string of the form:
ACTION:entry,entry,entry,...
- ACTION is either
PERMITorDENY. - entries are comma-separated, and each entry is one of:
- a single ID — e.g.
3120101 - a range — two IDs joined by a hyphen, low first — e.g.
1000-2000 - the literal keyword
ALL— matches every possible ID.
- a single ID — e.g.
Examples of valid ACLs:
PERMIT:ALL
DENY:1
DENY:1,1000-2000,4500-60000,17
PERMIT:3120000-3120999,3123456
An empty value is treated as PERMIT:ALL (permit everything).
There is exactly one ACTION per ACL. You cannot mix
PERMITandDENYentries in the same list. If you need "allow these but block those," you layer two ACLs — see Section 6.
2. The single most important rule: what happens to IDs you didn't list
This is the part that trips everyone up, so read it twice.
An ACL does two things at once. It defines an ACTION for the IDs you list, and it implies the opposite ACTION for every ID you don't list.
| You write | A listed ID is… | An unlisted ID is… | Mental model |
|---|---|---|---|
PERMIT:… |
permitted | denied | whitelist — only these get in |
DENY:… |
denied | permitted | blacklist — everyone except these |
So:
PERMIT:3120100-3120199means "allow only IDs 3120100–3120199, block the rest of the world."DENY:3120100-3120199means "block IDs 3120100–3120199, allow everyone else."
A listed ID gets the ACTION; an unlisted ID gets the opposite.
Rule of thumb: use DENY: when you want to block a small, known set and let
everyone else through. Use PERMIT: when you want a closed system where only an
explicit list is allowed.
PERMIT:ALL and DENY:1 (deny the reserved ID 1, permit all real IDs) are the
two most common "effectively open" settings.
3. The four ACL types
HBlink3 applies four kinds of ACL. Each is a separate config key.
| Key | Gates | Value domain | Notes |
|---|---|---|---|
REG_ACL |
Repeater/peer registration (login) | peer IDs, 1 … 4294967295 |
Only meaningful on SERVER systems. Outbound/OpenBridge links don't accept registrations. Always enforced — see §5. |
SUB_ACL |
Subscriber (the source radio ID of a call) | subscriber IDs, 1 … 16776415 |
Applied to every voice/data stream. |
TGID_TS1_ACL |
Destination talkgroup on Timeslot 1 | talkgroup IDs, 1 … 16776415 |
|
TGID_TS2_ACL |
Destination talkgroup on Timeslot 2 | talkgroup IDs, 1 … 16776415 |
The value domains matter: subscriber and talkgroup IDs are checked against a
maximum of 16776415 (const.ID_MAX), while registration (peer) IDs go up to
4294967295 (const.PEER_MAX). An entry outside the valid range causes
HBlink3 to exit at startup with an ACL CREATION ERROR — this is a
guardrail, not a bug (see Warnings).
4. Where each ACL is enforced
For an inbound DMRD voice/data frame, HBlink3 checks (in order):
- GLOBAL
SUB_ACL— source subscriber - GLOBAL
TGID_TS1_ACLorTGID_TS2_ACL— destination TG, by slot - SYSTEM
SUB_ACL— source subscriber - SYSTEM
TGID_TS1_ACLorTGID_TS2_ACL— destination TG, by slot
The call is dropped at the first ACL that denies it. It must pass all four to be forwarded.
For registration (a repeater logging into a master), both the GLOBAL and the
SYSTEM REG_ACL must permit the peer — if either one denies it, the login is
refused.
OpenBridge special case
OpenBridge systems do not register (no REG_ACL), and all OpenBridge traffic
is carried on Timeslot 1. That means the global TGID_TS1_ACL is your
talkgroup filter for OpenBridge peers. TS2 is forced to PERMIT:ALL internally
for OpenBridge systems.
5. The USE_ACL switch — and the one ACL it can't turn off
Each stanza has a USE_ACL boolean.
USE_ACL: True— enforce this stanza'sSUB_ACLand TGID ACLs.USE_ACL: False— skip the subscriber and talkgroup checks for this stanza (traffic passes freely).
Registration ACLs are always enforced. REG_ACL is not governed by
USE_ACL. Even with USE_ACL: False, a master will still refuse a repeater that
its REG_ACL denies. This is by design: registration is your front door.
Note: the stanzas still require the ACL keys to be present even when
USE_ACL: False. Leave them atPERMIT:ALLif unused.
6. Layering GLOBAL and SYSTEM ACLs
Because GLOBAL is checked first and SYSTEM second, and both must pass, you
can compose behaviors that a single PERMIT/DENY line cannot express. Think of
GLOBAL as your site-wide policy and SYSTEM as per-connection refinement.
Example — a global blacklist plus a per-system whitelist.
[GLOBAL]
USE_ACL: True
SUB_ACL: DENY:1,2,3 # site-wide: block a few abusive IDs, allow the rest
[LOCAL-CLUB-REPEATER]
USE_ACL: True
SUB_ACL: PERMIT:3120000-3120999 # this repeater: ONLY our club's ID block
A subscriber must (a) not be one of the globally blocked IDs and (b) fall inside the club's block. The intersection is exactly what you want, and neither ACL alone could express it.
7. Worked examples
All examples assume USE_ACL: True in the relevant stanza.
Open system, tidy. Allow every real subscriber, block only the reserved ID 1:
SUB_ACL: DENY:1
TGID_TS1_ACL: PERMIT:ALL
TGID_TS2_ACL: PERMIT:ALL
Closed talkgroup policy on TS1. Permit only a curated set of statewide and local talkgroups on Timeslot 1; leave TS2 open:
TGID_TS1_ACL: PERMIT:8,9,3100-3199,31201,31203
TGID_TS2_ACL: DENY:1
3100-3199 covers the whole regional block in one range; 8, 9, 31201,
31203 are individual talkgroups. Everything else on TS1 is dropped.
Ban a handful of subscribers network-wide while permitting everyone else:
[GLOBAL]
SUB_ACL: DENY:1,2606234,3141592,3120555
Members-only repeater. Only your membership ID range may key up, and only on approved talkgroups:
[MEMBERS-ONLY]
USE_ACL: True
SUB_ACL: PERMIT:3120000-3120499
TGID_TS1_ACL: PERMIT:2,9,3120
TGID_TS2_ACL: PERMIT:3120,31205
Registration control on a server. Accept only two specific repeater IDs onto this server; reject all other login attempts:
[SERVER-1]
MODE: SERVER
REG_ACL: PERMIT:311111,311222
Large but efficient range list. Contiguous and adjacent ranges are merged at load time, so this…
SUB_ACL: PERMIT:1000-1999,2000-2999,3000-3999,3120101
…collapses internally to a single span 1000–3999 plus the single ID 3120101.
Write ranges however is clearest to you; the engine tidies them up.
Performance
Short version: turn ACLs on. They are fast.
- All of the work of reading and organizing your ACLs happens once, when HBlink3 starts — not while traffic is flowing.
- Checking a packet is near-instant, and it stays fast no matter how long your lists get. A list with a thousand entries is checked essentially as quickly as a list with ten. You will not see the difference on real traffic.
- Runs of consecutive IDs are combined internally, so a config that looks huge often takes very little memory.
Where cost actually comes from — and it is small:
- Every enabled ACL type adds one check per packet. With GLOBAL + SYSTEM layering that's up to four checks per frame. Constant, tiny, predictable.
- Very large lists of thousands of individual (non-consecutive) IDs use a little more memory and startup time, but checking them stays fast.
Practical guidance:
- Use ranges (
1000-2000) instead of listing many consecutive IDs — it's less to type and smaller in memory. - If a stanza genuinely uses no ACLs,
USE_ACL: Falseskips the subscriber/TGID checks entirely (registration is still enforced). - Don't hesitate to write expressive ACLs. The design specifically optimizes for exactly this; in practice the cost is negligible.
Warnings
PERMITis a whitelist — it denies everyone you didn't list. The most common mistake is writingPERMIT:3120101intending "also allow 3120101" and accidentally locking out the entire rest of the world. Re-read Section 2.- Out-of-range entries stop HBlink3 at startup. A subscriber/TGID entry above
16776415, or a peer entry above 4294967295, triggers
ACL CREATION ERROR, VALUE OUT OF RANGEand the program exits. Check your numbers if HBlink3 won't start after an ACL edit. - Write ranges low-to-high (
1000-2000, not2000-1000). A reversed range will silently never match anything. REG_ACLignoresUSE_ACL. You cannot "disable" registration filtering by settingUSE_ACL: False; setREG_ACL: PERMIT:ALLif you want it open.- GLOBAL and SYSTEM ACLs are ANDed. If a call is mysteriously dropped, check the GLOBAL stanza too — a global ACL can veto traffic a permissive system ACL would otherwise allow.
- The keys must exist even when unused. Leave unused ACLs at
PERMIT:ALLrather than deleting them.