ACL role constants diverge from upstream MeshCore -> admin logins are seen as non‑admin / guest by standard MeshCore clients #136

Closed
opened 2026-10-27 09:35:07 +01:00 by adam · 1 comment
Owner

Summary

OpenHop's ACL permission/role numbering does not match upstream MeshCore
(ClientACL.h). Most importantly, PERM_ACL_ADMIN = 0x02 in OpenHop vs 3
upstream
. The login‑response ACL byte is interpreted by every standard
MeshCore client using upstream's numbering (low 2 bits = role, ADMIN == 3),
so an OpenHop admin session is decoded as read‑write / non‑admin.

Effect: a user logs in with the correct admin password, OpenHop's own UI
correctly shows admin, but a standard MeshCore client (e.g. the KiekR
Android/iOS app talking through a stock MeshCore BLE companion) shows the
session as guest / non‑admin and therefore gates off all admin controls.

Environment

  • Repeater: OpenHop (Python)
  • Client: standard MeshCore client (reproduced with KiekR 0.6.0) via a stock
    MeshCore BLE companion, companion fw_ver = 13
  • Login response is the 14‑byte PUSH_CODE_LOGIN_SUCCESS (0x85) form

Root cause — OpenHop ACL constants

openhop_repeater → repeater/handler_helpers/acl.py:

PERM_ACL_GUEST      = 0x01
PERM_ACL_ADMIN      = 0x02
PERM_ACL_READ_WRITE = 0x01
PERM_ACL_ROLE_MASK  = 0x03

Admin authentication assigns permissions = PERM_ACL_ADMIN (= 0x02):

if admin_pwd and password == admin_pwd:
    permissions = PERM_ACL_ADMIN          # 0x02
elif guest_pwd and password == guest_pwd:
    permissions = PERM_ACL_READ_WRITE     # 0x01
else:
    return False, 0

Upstream MeshCore reference

MeshCore → src/helpers/ClientACL.h:

#define PERM_ACL_ROLE_MASK     3   // lower 2 bits
#define PERM_ACL_GUEST         0
#define PERM_ACL_READ_ONLY     1
#define PERM_ACL_READ_WRITE    2
#define PERM_ACL_ADMIN         3
// ...
bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; }

Divergence (role value in the low 2 bits)

Role Upstream MeshCore OpenHop
GUEST 0 0x01
READ_ONLY 1 —
READ_WRITE 2 0x01
ADMIN 3 0x02

Two concrete problems:

  1. PERM_ACL_ADMIN = 0x02 collides with upstream PERM_ACL_READ_WRITE = 2.
    A standard client computes role = permissions & PERM_ACL_ROLE_MASK →
    0x02 & 0x03 = 2 → READ_WRITE, and isAdmin() → (perms & 3) == 3 →
    2 == 3 → false. The admin session is therefore rendered as
    read‑write / non‑admin.
  2. PERM_ACL_GUEST and PERM_ACL_READ_WRITE are both 0x01 — they are
    indistinguishable from each other, and both differ from upstream
    (GUEST = 0).

Secondary: the login reply also flags admin on the wrong bit

openhop_core → src/pymc_core/node/handlers/login_server.py builds the login
reply:

reply_data = bytearray(13)
struct.pack_into("<I", reply_data, 0, current_time)  # timestamp
reply_data[4] = response_code                        # RESP_SERVER_LOGIN_OK = 0x00
reply_data[5] = 0                                    # legacy keep-alive
reply_data[6] = 1 if (permissions & 0x02) else 0     # "is_admin" flag
reply_data[7] = permissions                          # full ACL byte
struct.pack_into("<I", reply_data, 8, random.randint(0, 0xFFFFFFFF))
reply_data[12] = FIRMWARE_VER_LEVEL                  # 1

Upstream sets this byte from isAdmin():
reply_data[6] = client->isAdmin() ? 1 : 0 = (permissions & 3) == 3.
OpenHop's permissions & 0x02 is only self‑consistent with OpenHop's own
(non‑standard) numbering; against upstream numbering it would also mark
READ_WRITE (2) as admin.

How a standard client decodes it (for reference)

The stock MeshCore BLE companion forwards these repeater‑reply bytes verbatim
into PUSH_CODE_LOGIN_SUCCESS: frame[1] = reply_data[6] (is_admin) and
frame[12] = reply_data[7] (ACL byte) — see MeshCore
examples/companion_radio/MyMesh.cpp. The client then derives the displayed
role from the ACL byte's low 2 bits. For an OpenHop admin (0x02) that yields
"not ADMIN(3)" → the session is shown as guest / non‑admin, even though the
admin password was correct.

Suggested fix

Align the ACL constants with upstream MeshCore in
repeater/handler_helpers/acl.py:

PERM_ACL_ROLE_MASK  = 0x03
PERM_ACL_GUEST      = 0
PERM_ACL_READ_ONLY  = 1
PERM_ACL_READ_WRITE = 2
PERM_ACL_ADMIN      = 3

and make the reply's admin flag match upstream isAdmin() in
login_server.py:

reply_data[6] = 1 if (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN else 0

This makes OpenHop wire‑compatible with every stock‑based MeshCore client for
admin/role recognition, with no change required on the client side.

Reproduction

  1. Configure an OpenHop repeater with an admin password.
  2. From a standard MeshCore client (reproduced with KiekR), log in to the
    repeater using the admin password.
  3. Observed: the client shows the session as guest / non‑admin; admin
    controls are unavailable.
    Expected: admin.

Notes

  • Constants and code above were read from the current HEAD of
    openhop-dev/openhop_repeater (repeater/handler_helpers/acl.py) and
    openhop-dev/openhop_core (src/pymc_core/node/handlers/login_server.py).
  • Upstream references are from MeshCore src/helpers/ClientACL.h and
    examples/companion_radio/MyMesh.cpp.
## Summary OpenHop's ACL permission/role numbering does not match upstream MeshCore (`ClientACL.h`). Most importantly, **`PERM_ACL_ADMIN = 0x02` in OpenHop vs `3` upstream**. The login‑response ACL byte is interpreted by every standard MeshCore client using upstream's numbering (low 2 bits = role, `ADMIN == 3`), so an OpenHop **admin** session is decoded as **read‑write / non‑admin**. Effect: a user logs in with the correct **admin** password, OpenHop's own UI correctly shows admin, but a standard MeshCore client (e.g. the KiekR Android/iOS app talking through a stock MeshCore BLE companion) shows the session as **guest / non‑admin** and therefore gates off all admin controls. ## Environment - Repeater: OpenHop (Python) - Client: standard MeshCore client (reproduced with KiekR 0.6.0) via a stock MeshCore BLE companion, companion `fw_ver = 13` - Login response is the 14‑byte `PUSH_CODE_LOGIN_SUCCESS (0x85)` form ## Root cause — OpenHop ACL constants `openhop_repeater` → `repeater/handler_helpers/acl.py`: ```python PERM_ACL_GUEST = 0x01 PERM_ACL_ADMIN = 0x02 PERM_ACL_READ_WRITE = 0x01 PERM_ACL_ROLE_MASK = 0x03 ``` Admin authentication assigns `permissions = PERM_ACL_ADMIN` (= `0x02`): ```python if admin_pwd and password == admin_pwd: permissions = PERM_ACL_ADMIN # 0x02 elif guest_pwd and password == guest_pwd: permissions = PERM_ACL_READ_WRITE # 0x01 else: return False, 0 ``` ## Upstream MeshCore reference `MeshCore` → `src/helpers/ClientACL.h`: ```c #define PERM_ACL_ROLE_MASK 3 // lower 2 bits #define PERM_ACL_GUEST 0 #define PERM_ACL_READ_ONLY 1 #define PERM_ACL_READ_WRITE 2 #define PERM_ACL_ADMIN 3 // ... bool isAdmin() const { return (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN; } ``` ### Divergence (role value in the low 2 bits) | Role | Upstream MeshCore | OpenHop | |-------------|-------------------|---------| | GUEST | `0` | `0x01` | | READ_ONLY | `1` | — | | READ_WRITE | `2` | `0x01` | | **ADMIN** | **`3`** | **`0x02`** | Two concrete problems: 1. **`PERM_ACL_ADMIN = 0x02` collides with upstream `PERM_ACL_READ_WRITE = 2`.** A standard client computes `role = permissions & PERM_ACL_ROLE_MASK` → `0x02 & 0x03 = 2` → `READ_WRITE`, and `isAdmin()` → `(perms & 3) == 3` → `2 == 3` → **false**. The admin session is therefore rendered as read‑write / non‑admin. 2. **`PERM_ACL_GUEST` and `PERM_ACL_READ_WRITE` are both `0x01`** — they are indistinguishable from each other, and both differ from upstream (`GUEST = 0`). ## Secondary: the login reply also flags admin on the wrong bit `openhop_core` → `src/pymc_core/node/handlers/login_server.py` builds the login reply: ```python reply_data = bytearray(13) struct.pack_into("<I", reply_data, 0, current_time) # timestamp reply_data[4] = response_code # RESP_SERVER_LOGIN_OK = 0x00 reply_data[5] = 0 # legacy keep-alive reply_data[6] = 1 if (permissions & 0x02) else 0 # "is_admin" flag reply_data[7] = permissions # full ACL byte struct.pack_into("<I", reply_data, 8, random.randint(0, 0xFFFFFFFF)) reply_data[12] = FIRMWARE_VER_LEVEL # 1 ``` Upstream sets this byte from `isAdmin()`: `reply_data[6] = client->isAdmin() ? 1 : 0` = `(permissions & 3) == 3`. OpenHop's `permissions & 0x02` is only self‑consistent with OpenHop's own (non‑standard) numbering; against upstream numbering it would also mark `READ_WRITE` (`2`) as admin. ## How a standard client decodes it (for reference) The stock MeshCore BLE companion forwards these repeater‑reply bytes verbatim into `PUSH_CODE_LOGIN_SUCCESS`: `frame[1] = reply_data[6]` (is_admin) and `frame[12] = reply_data[7]` (ACL byte) — see MeshCore `examples/companion_radio/MyMesh.cpp`. The client then derives the displayed role from the ACL byte's low 2 bits. For an OpenHop admin (`0x02`) that yields "not `ADMIN(3)`" → the session is shown as guest / non‑admin, even though the admin password was correct. ## Suggested fix Align the ACL constants with upstream MeshCore in `repeater/handler_helpers/acl.py`: ```python PERM_ACL_ROLE_MASK = 0x03 PERM_ACL_GUEST = 0 PERM_ACL_READ_ONLY = 1 PERM_ACL_READ_WRITE = 2 PERM_ACL_ADMIN = 3 ``` and make the reply's admin flag match upstream `isAdmin()` in `login_server.py`: ```python reply_data[6] = 1 if (permissions & PERM_ACL_ROLE_MASK) == PERM_ACL_ADMIN else 0 ``` This makes OpenHop wire‑compatible with every stock‑based MeshCore client for admin/role recognition, with no change required on the client side. ## Reproduction 1. Configure an OpenHop repeater with an admin password. 2. From a standard MeshCore client (reproduced with KiekR), log in to the repeater using the **admin** password. 3. **Observed:** the client shows the session as **guest / non‑admin**; admin controls are unavailable. **Expected:** admin. ## Notes - Constants and code above were read from the current `HEAD` of `openhop-dev/openhop_repeater` (`repeater/handler_helpers/acl.py`) and `openhop-dev/openhop_core` (`src/pymc_core/node/handlers/login_server.py`). - Upstream references are from MeshCore `src/helpers/ClientACL.h` and `examples/companion_radio/MyMesh.cpp`. <!-- github-sync-id:5141574509 -->
adam closed this issue 2026-10-27 09:35:07 +01:00
Author
Owner

I believe this work has been done in the DEV branch, just awaiting more testing before merging to Main.

I believe this work has been done in the DEV branch, just awaiting more testing before merging to Main. <!-- github-comment-id:5290961032 -->
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: adam/pyMC_Repeater#136