Skip to content

Conversation

@freke
Copy link
Owner

@freke freke commented Feb 10, 2026

ocb128 keep track of stats
feat: implement mumble.erl API with comprehensive documentation

Add complete Mumble protocol implementation including:

API Implementation:

  • start_server/1,2,3 with certificate validation and auto-generation
  • start_client/4,5 with TLS support and callback handling
  • stop_listener/1 and stop_client/1 for cleanup
  • send/2, send_voice/2, get_state/1 for client operations
  • server_version/0 and serverconfig/0 for server info

Connection Handling:

  • mumble_client_conn: gen_statem-based client connection handler
  • mumble_server_conn: server-side connection with OCB-AES128 encryption
  • mumble_server_sup: supervisor managing TCP and UDP servers
  • mumble_udp_server: UDP voice traffic handling

Protocol Support:

  • mumble_tcp_proto: TCP message packing/unpacking
  • mumble_udp_proto: UDP packet handling
  • mumble_varint: Variable-length integer encoding
  • mumble_msg: Record to map conversions

Behaviours:

  • mumble_client_behaviour: client callback interface
  • mumble_server_behaviour: server handler interface

Documentation:

  • Complete -moduledoc for all modules
  • Function -doc attributes with inputs/outputs
  • Type documentation for public types
  • Usage examples throughout

Testing:

  • mumble_api_tests: API validation tests
  • mumble_client_api_tests: client functionality tests
  • mumble_cert_tests: certificate generation tests
  • mumble_SUITE: integration tests

All 128 EUnit tests, 34 CT tests, and 5 property tests pass. Dialyzer type checking passes with no warnings.

Refs: erlmur architecture v1.0

refactor: split mumble.erl into specialized modules

test: add comprehensive connection logic unit tests

Fix eunit tests

  • Fixed client connection tests that were failing due to unhandled process exits
  • Added trap_exit and logger suppression to prevent test failures
  • Fixed mock handler and transport setup for server tests
  • Client tests now pass successfully

Add mock transport support for server tests

  • Fixed client connection tests that were failing due to unhandled process exits
  • Added trap_exit and logger suppression to prevent test failures
  • Fixed mock handler and transport setup for server tests
  • Client tests now pass successfully

Add mock transport support for server tests

  • Fixed client connection tests that were failing due to unhandled process exits
  • Added trap_exit and logger suppression to prevent test failures
  • Fixed mock handler and transport setup for server tests
  • Client tests now pass successfully

Fixed build warnings

Fixed ping

Summary by CodeRabbit

  • New Features
    • Full Mumble protocol library: server & client APIs, TLS support with automatic self-signed cert generation, TCP/UDP voice transport, message packing/unpacking, and connection lifecycle utilities.
  • Refactor
    • Modular OTP apps and extracted crypto library for OCB-AES128.
  • Documentation
    • Development guide and detailed "establishing connection" protocol docs added.
  • Tests
    • Extensive unit, property and end-to-end test suites.
  • Chores
    • Updated config and ignore files.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Restructures the project into a Rebar3 multi-app layout: adds a mumble_protocol and ocb128_crypto app with protocol, client/server, crypto, cert, and tests; introduces new erlmur application modules and supervisors; removes legacy monolithic erlmur implementation and many src/test files.

Changes

Cohort / File(s) Summary
Top-level config & docs
\.elp\.toml, \.gitignore, AGENTS\.md, rebar\.config, justfile, devenv\.nix, erlang_ls\.yaml
Adds project config and docs; updates rebar/project settings, build targets, devenv, and ignore rules.
Erlmur app bootstrap
apps/erlmur/src/erlmur\.app\.src, apps/erlmur/src/erlmur\.erl
Replaces previous monolith: app descriptor updated to depend on mumble_protocol; erlmur:start/0 simplified to ensure application start.
Erlmur OTP modules
apps/erlmur/src/erlmur_app\.erl, apps/erlmur/src/erlmur_sup\.erl, apps/erlmur/src/erlmur_server_handler\.erl, apps/erlmur/src/erlmur_user_manager\.erl
Adds OTP application callback, supervisor, server handler implementing mumble_server_behaviour, and user-manager gen_server (session tracking, ETS, broadcasts).
Mumble protocol app & headers
apps/mumble_protocol/src/mumble_protocol\.app\.src, apps/mumble_protocol/include/mumble_protocol\.hrl, apps/mumble_protocol/rebar\.config
New OTP app descriptor, detailed protocol records/macros, and rebar config for protobuf/hooks/docs.
Mumble public API & certs
apps/mumble_protocol/src/mumble\.erl, apps/mumble_protocol/src/mumble_cert\.erl
Public mumble API for server/client lifecycle, auto cert generation/validation and cert helpers.
Server implementation & supervision
apps/mumble_protocol/src/mumble_server\.erl, apps/mumble_protocol/src/mumble_server_sup\.erl, apps/mumble_protocol/src/mumble_server_conn\.erl
Server lifecycle, supervisor to start UDP and Ranch listener, per-connection gen_statem with TLS/auth/crypto/voice handling.
Client implementation
apps/mumble_protocol/src/mumble_client\.erl, apps/mumble_protocol/src/mumble_client_conn\.erl, apps/mumble_protocol/src/mumble_client_behaviour\.erl
Client API and gen_statem client FSM; client behaviour for callbacks.
Serialization & transport
apps/mumble_protocol/src/mumble_tcp_proto\.erl, apps/mumble_protocol/src/mumble_udp_proto\.erl, apps/mumble_protocol/src/mumble_msg\.erl, apps/mumble_protocol/src/mumble_varint\.erl, apps/mumble_protocol/src/mumble_version\.erl
TCP/UDP framing, varint handling, message <-> map conversion, version encoding/decoding, UDP voice/ping handling.
UDP server & voice
apps/mumble_protocol/src/mumble_udp_server\.erl, apps/mumble_protocol/src/mumble_udp_proto\.erl
UDP listener/gen_server, ping handling, routing to sessions and voice packet parsing.
Message-level server conn & protocol behaviour
apps/mumble_protocol/src/mumble_server_behaviour\.erl, apps/mumble_protocol/src/mumble_msg\.erl
Behaviour spec for server handlers and comprehensive message conversions.
Testing & mocks
apps/mumble_protocol/test/*, apps/mumble_protocol/test/mock_*, apps/mumble_protocol/test/test_utils\.erl
Large CT/EUnit suites, e2e and unit tests, property tests, mock transport and mock handler for test harness.
OCB128 crypto app
apps/ocb128_crypto/src/ocb128_crypto\.erl, apps/ocb128_crypto/include/ocb128_crypto\.hrl, apps/ocb128_crypto/src/ocb128_crypto\.app\.src, apps/ocb128_crypto/test/*, apps/ocb128_crypto/rebar\.config
New crypto app (renamed from erlmur_crypto) with stats tracking, API adjustments, and updated tests.
Removed legacy monolith
src/erlmur_*.erl, include/erlmur\.hrl, test/erlmur_SUITE\.erl
Deletes legacy session, server, UDP/TCP message handling, stores (user/channel/session), id generator, ACL, stats, and legacy tests — replaced by apps above.

Sequence Diagram(s)

sequenceDiagram
    participant App as erlmur_app
    participant Sup as erlmur_sup
    participant UserMgr as erlmur_user_manager
    participant Server as mumble_server

    App->>App: start/2 (application callback)
    App->>Server: start_server(opts)
    Server->>Sup: start_link(Options)
    Sup->>UserMgr: start_link()
    UserMgr-->>Sup: {ok, Pid}
    Sup-->>Server: supervisor started
    Server-->>App: {ok, {mumble_server, SupPid, ListenerRef}}
Loading
sequenceDiagram
    participant Client as Mumble Client
    participant Conn as mumble_client_conn
    participant ServerConn as mumble_server_conn
    participant Handler as erlmur_server_handler
    participant UserMgr as erlmur_user_manager

    Client->>Conn: start_link(Host,Port,Opts)
    Conn->>ServerConn: TLS connect / send Version
    ServerConn->>Handler: init(Opts)
    Handler->>UserMgr: register_user(SessionId, Pid)
    Handler-->>ServerConn: {ok, NewState}
    ServerConn->>Conn: ServerSync
    Client->>Conn: send(TextMessage)
    Conn->>ServerConn: forward TextMessage
    ServerConn->>Handler: handle_msg(TextMessage)
    Handler->>UserMgr: broadcast_text(SessionId, Text)
Loading
sequenceDiagram
    participant UDP as External UDP
    participant UDPSrv as mumble_udp_server
    participant UserMgr as erlmur_user_manager
    participant Sess as mumble_server_conn

    UDP->>UDPSrv: incoming packet
    UDPSrv->>UserMgr: get_session_by_udp(IP,Port)
    UserMgr-->>UDPSrv: {ok, SessionId}
    UDPSrv->>Sess: udp_packet(SessionId, Bin)
    Sess->>Sess: decrypt & parse (OCB-AES128)
    Sess->>UserMgr: broadcast_voice(SessionId, Frames)
    UserMgr->>UDP: forward voice to peers
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title Check ✅ Passed Title check skipped as CodeRabbit has written the PR title.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch umbrella

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai bot changed the title @coderabbitai Extract mumble_protocol library and refactor server architecture Feb 10, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/erlmur/src/erlmur.app.src (1)

13-17: ⚠️ Potential issue | 🟠 Major

Remove unused env configuration or update code to use it.

The env config in erlmur.app.src (lines 14–15) defines cert_pem and key_pem, but erlmur_app.erl lines 27–28 ignore these settings and hardcode the paths instead:

CertFile = filename:join(PrivDir, "server.pem"),
KeyFile = filename:join(PrivDir, "server.key"),

The configuration variables are not read from the environment at all. Additionally, only server.key exists in priv/; cert.pem is missing. Either remove the unused env config or refactor the code to read from cert_pem and key_pem as intended.

apps/ocb128_crypto/test/ocb128_crypto_benchmark_SUITE.erl (1)

3-11: ⚠️ Potential issue | 🔴 Critical

Missing exports for individual test case functions — CT will fail to invoke them.

all/0 returns atoms like encrypt_16b, decrypt_16b, etc., but none of these functions (lines 75–93) are exported. Common Test requires test case functions to be exported to call them.

Proposed fix
 -export([
     all/0,
     groups/0,
     init_per_suite/1,
     end_per_suite/1,
     init_per_testcase/2,
     end_per_testcase/2,
-    run_benchmark/2
+    run_benchmark/2,
+    encrypt_16b/1, encrypt_32b/1, encrypt_128b/1,
+    encrypt_1kb/1, encrypt_4kb/1, encrypt_16kb/1,
+    encrypt_128kb/1, encrypt_1mb/1, encrypt_4mb/1,
+    decrypt_16b/1, decrypt_32b/1, decrypt_128b/1,
+    decrypt_1kb/1, decrypt_4kb/1, decrypt_16kb/1,
+    decrypt_128kb/1, decrypt_1mb/1, decrypt_4mb/1
 ]).
🤖 Fix all issues with AI agents
In `@apps/erlmur/priv/server.key`:
- Around line 1-28: A plaintext private key (apps/erlmur/priv/server.key) was
committed—remove it from the repo and history, stop tracking new .key files, and
replace with a secure provisioning flow: remove the file and purge it from Git
history (e.g., use git filter-repo or equivalent to delete
apps/erlmur/priv/server.key and its blobs), add a rule to .gitignore to cover
*.key so future keys are not committed, and update build/deploy to generate or
inject keys from secure sources/env vars (or reference secrets manager) instead
of bundling server.key in source control.

In `@apps/erlmur/src/erlmur_app.erl`:
- Around line 32-36: The start/2 function currently returns {ok, self()} and
starts ServerRef outside the supervision tree; change it to call
erlmur_sup:start_link() and capture its return ({ok, SupPid} or {ok, SupPid,
Info}), return {ok, SupPid} instead of self(), and remove the ad-hoc start of
ServerRef from erlmur_app:start/2; instead, add the Mumble server as a
supervised child in erlmur_sup (or ensure erlmur_sup starts ServerRef during
init/1) so the supervisor owns and can restart ServerRef on crash (adjust
erlmur_sup child_spec and startup logic accordingly).

In `@apps/mumble_protocol/src/mumble_client_conn.erl`:
- Around line 113-129: The authenticating/3 clause currently sends a hardcoded
username <<"TestUser">> instead of using credentials supplied to start_link/3;
update the code to store the passed-in Opts into the connection State (e.g. add
an opts field on the state record when initializing in init/start_link), then in
authenticating/3 read the username (and optional password/token) from State
(e.g. State#state.opts.username or similar) and use that value when building
AuthMsg in place of <<"TestUser">>; ensure start_link/3 and init thread the Opts
into State so authenticating/3 can access them.

In `@apps/mumble_protocol/src/mumble_server_conn.erl`:
- Around line 339-354: The handler sets Caps = #{} when Mod does not export
get_caps/1 which then crashes version_enc(Caps) because version_enc/1 only
matches #{major := Major, minor := Minor, patch := Patch}; fix by ensuring
version_enc always receives a map with defaults or by adding a fallback clause
in version_enc/1: either change handle_protocol_msg (where Caps is set) to
supply default keys (major, minor, patch) when get_caps/1 is absent, or add a
new version_enc(Caps) clause that reads missing keys via maps:get/3 (providing
sane defaults) so a function_clause error cannot occur; reference functions:
handle_protocol_msg, get_caps/1, version_enc/1 and the Caps variable when making
the change.
- Around line 315-329: process_messages currently only matches {ok, _} and
{transition, _, _} from handle_protocol_msg, so a {stop, Reason, NewStateData}
return (from handle_protocol_msg) will cause a case_clause crash; update
process_messages to handle the {stop, Reason, NewStateData} tuple (e.g., add a
branch in the case that returns or propagates {stop, Reason, NewStateData}
instead of falling through), and ensure process_messages_in_new_state and/or the
caller of process_messages can accept and handle that {stop, ...} result;
reference: process_messages/3, handle_protocol_msg/2,
process_messages_in_new_state/3.

In `@apps/mumble_protocol/src/mumble_server.erl`:
- Around line 58-73: The current sequence calls ranch:stop_listener then
attempts to terminate children and uses exit(SupPid, normal) which supervisors
ignore; instead stop the supervisor atomically using the supervisor API (e.g.
call supervisor:stop(SupPid) or exit(SupPid, shutdown)) so the supervisor and
its children are terminated cleanly, and only then call ranch:stop_listener;
replace the exit(SupPid, normal) usage in stop_listener/1 with
supervisor:stop(SupPid) (or an explicit shutdown exit) and reorder/error-handle
so ranch:stop_listener is invoked after a successful supervisor stop (and return
appropriate {ok, ...} / {error, Reason} tuples on failures).

In `@apps/mumble_protocol/src/mumble_tcp_proto.erl`:
- Around line 110-126: The unpack/2 function (unpack) lacks a clause for when
the input binary is non-empty but too short for a full header/payload, causing
function_clause crashes on partial TCP frames; add a trailing clause like
unpack(PartialBin, Acc) -> {lists:reverse(Acc), PartialBin} so callers can
buffer leftover bytes, update callers to accept the new return shape ({Messages,
RestBinary}) or alternatively log+discard if you intentionally want to drop
partial frames; ensure references to unpack/2 in surrounding code are updated to
handle the tuple return and to prepend RestBinary to the next received chunk.

In `@apps/mumble_protocol/src/mumble_udp_server.erl`:
- Around line 102-140: The handler passes the 8-bit-matched Data directly to
'MumbleUDP_gpb':decode_msg but the protocol uses a 3-bit type + 5-bit target
header; update the handle_info clause to strip the 3+5 bit header before
decoding and reattach the same header (<<1:3,0:5>>) to the encoded response
before gen_udp:send. Specifically, change the pattern matching on Data in the
handle_info clause to extract the protobuf payload (e.g., match the 3-bit type
and 5-bit target then Payload) before calling
'MumbleUDP_gpb':decode_msg(Payload, 'Ping') and when sending use
gen_udp:send(Socket, IP, PortNo, <<1:3,0:5, EncodedResponse/binary>>) so the
logic in handle_info mirrors mumble_udp_proto:handle_ping/3.

In `@apps/mumble_protocol/test/e2e_connection_SUITE.erl`:
- Around line 13-31: The tests generate certs into ?config(priv_dir, Config) and
set application env vars in init_per_suite, but erlmur_app:start/2 currently
ignores those and hardcodes CertFile = filename:join(code:priv_dir(erlmur),
"server.pem"); update erlmur_app:start/2 to first attempt to read certificate
paths via application:get_env(erlmur, cert_pem) and application:get_env(erlmur,
key_pem) (use those when set and readable), and only fall back to
filename:join(code:priv_dir(erlmur), "server.pem") / "server.key" when env
values are absent or files missing; additionally, in init_per_suite replace the
raw os:cmd("openssl ...") call with a checked invocation (capture exit/status
and fail the test early or ct:fail if openssl fails) so missing openssl is
detected. Ensure file existence checks are performed before starting erl mur in
erlmur_app:start/2.

In `@apps/mumble_protocol/test/mock_mumble_handler.erl`:
- Around line 1-41: The module lacks an -export directive so its callbacks are
not callable; add an export list that exports start_link/1, stop/1, init/1,
handle_msg/2, get_caps/1, and authenticate/2 (matching the functions
implemented) so the behaviour callbacks (e.g., calls to
mock_mumble_handler:init/1, start_link/1, handle_msg/2, get_caps/1,
authenticate/2) are available to other modules like mumble_server_conn.

In `@apps/mumble_protocol/test/mock_transport.erl`:
- Line 156: The messages/0 function returns the wrong tuple shape; update
messages/0 to return a flat 3-tuple of atoms as Ranch expects (the message tag
triple), e.g., change the current nested {ok, {ssl, ssl_closed}} to the correct
{ssl, ssl_closed, ssl_error} so that the messages/0 function provides the
expected {Tag, Closed, Error} tuple for ranch_transport.

In `@apps/mumble_protocol/test/prop_protocol_tests.erl`:
- Around line 1-3: The module prop_protocol_tests is missing an export directive
so PropEr can't discover the properties; add an -export([...]) clause that
exports prop_varint_roundtrip/0 and prop_tcp_proto_roundtrip/0 (e.g.
-export([prop_varint_roundtrip/0, prop_tcp_proto_roundtrip/0]).) Place this
export in the prop_protocol_tests module alongside the existing -module and
-include_lib directives so the test runner can find and execute those property
functions.
🟠 Major comments (23)
justfile-41-41 (1)

41-41: ⚠️ Potential issue | 🟠 Major

find -name "doc" is overly broad and may delete source documentation.

This find searches the entire project directory for any directory named doc, which could delete source documentation (e.g., apps/mumble_protocol/docs/ wouldn't match, but any directory literally named doc would). Consider scoping to generated output only:

Proposed fix — scope to _build
-	find {{justfile_directory()}} -name "doc" -exec rm -rf {} +
+	find {{justfile_directory()}}/_build -name "doc" -exec rm -rf {} +
apps/mumble_protocol/test/prop_protocol_tests.erl-4-8 (1)

4-8: ⚠️ Potential issue | 🟠 Major

Unbounded integer() generator will produce values that overflow the 64-bit varint encoding.

PropEr's integer() generator can produce arbitrarily large integers. The encoder's final clause packs into 64 bits (Num:64), silently truncating values outside that range and breaking the roundtrip. Constrain the generator to the representable range.

Proposed fix
 prop_varint_roundtrip() ->
-    ?FORALL(V, integer(),
+    ?FORALL(V, integer(-16#8000000000000000, 16#7FFFFFFFFFFFFFFF),
         begin
             {V, <<>>} == mumble_varint:decode(mumble_varint:encode(V))
         end).
apps/mumble_protocol/src/mumble_client_conn.erl-151-153 (1)

151-153: ⚠️ Potential issue | 🟠 Major

ssl:send/2 return value is ignored throughout the module.

ssl:send/2 can return {error, Reason} (e.g., on closed socket), but the return value is discarded on lines 124, 127, 152, 174, and 215. At minimum, log errors or propagate them to avoid silent data loss.

apps/mumble_protocol/src/mumble_cert.erl-101-122 (1)

101-122: ⚠️ Potential issue | 🟠 Major

Shell command injection risk via unsanitized inputs in io_lib:format.

Subject, KeyFile, and CertFile are interpolated directly into a shell command string passed to os:cmd/1. If any of these contain shell metacharacters (e.g., a Subject like "/CN=foo'; rm -rf /; echo '"), arbitrary commands can be executed.

Consider using open_port({spawn_executable, ...}, [{args, [...]}]) instead of os:cmd/1 to avoid shell interpretation entirely, or at minimum sanitize/validate inputs.

🔒 Proposed fix using open_port for safe argument passing
 generate_self_signed_cert(CertFile, KeyFile, Subject, Days) ->
     case ensure_cert_dir(CertFile) of
         ok ->
-            Cmd = io_lib:format(
-                "openssl req -x509 -newkey rsa:2048 -keyout ~s -out ~s -days ~B -nodes -subj '~s' 2>&1",
-                [KeyFile, CertFile, Days, Subject]
-            ),
-
-            _Output = os:cmd(Cmd),
+            OpenSSL = os:find_executable("openssl"),
+            DaysStr = integer_to_list(Days),
+            Port = open_port({spawn_executable, OpenSSL}, [
+                {args, ["req", "-x509", "-newkey", "rsa:2048",
+                         "-keyout", KeyFile, "-out", CertFile,
+                         "-days", DaysStr, "-nodes", "-subj", Subject]},
+                exit_status, stderr_to_stdout, binary
+            ]),
+            collect_port_output(Port),
             case {filelib:is_file(CertFile), filelib:is_file(KeyFile)} of

You'd also need a helper to drain the port:

collect_port_output(Port) ->
    receive
        {Port, {exit_status, _Status}} -> ok;
        {Port, {data, _Data}} -> collect_port_output(Port)
    after 30000 ->
        port_close(Port),
        {error, timeout}
    end.
apps/mumble_protocol/test/mumble_cert_tests.erl-56-202 (1)

56-202: ⚠️ Potential issue | 🟠 Major

Tests exercise the local helper, not the production mumble_cert module.

All certificate tests call the local generate_cert/2,4 helper (which shells out to openssl directly) rather than the production functions mumble_cert:generate_self_signed_cert/4, mumble_cert:validate_cert_files/2, or mumble_cert:ensure_auto_certs/2. This means the actual certificate management code has no unit test coverage from this file.

Consider adding tests that exercise the production module's API directly.

apps/mumble_protocol/test/mumble_client_api_tests.erl-7-11 (1)

7-11: ⚠️ Potential issue | 🟠 Major

Five test functions are defined but not registered in the test generator — they will never run.

Only client_start_invalid_certs and client_invalid_ref are listed. The following functions are dead test code: client_start_no_certs, client_send_message, client_send_voice, client_get_state, and client_stop. If these are intentionally excluded (e.g., they require a running server), consider adding a comment explaining why, or wire them up with appropriate fixtures.

apps/ocb128_crypto/test/prop_ocb128_crypto.erl-91-95 (1)

91-95: ⚠️ Potential issue | 🟠 Major

Line 94: =:= comparison doesn't assert — the lost stat check silently passes on mismatch.

Lines 92–93 correctly use = (pattern match) which will crash on mismatch. Line 94 uses =:= which evaluates to true/false but the result is discarded, so a wrong lost count will never be caught.

Proposed fix
-                    (N - 1 - I) =:= Stats#crypto_stats.lost
+                    (N - 1 - I) = Stats#crypto_stats.lost
apps/mumble_protocol/src/mumble_msg.erl-217-232 (1)

217-232: ⚠️ Potential issue | 🟠 Major

List comprehensions over potentially undefined lists will crash.

raw_to_map for BanList, ACL (groups/acls), UserList, and VoiceTarget iterates the nested list fields with [ ... || B <- Bans]. If the protobuf field is undefined (not set), the comprehension crashes with a bad_generator error.

Guard against this by defaulting undefined to []:

Example fix for BanList
 raw_to_map(#'BanList'{bans = Bans, query = Query}) ->
     #{
         message_type => 'BanList',
         bans => [
             #{
                 address => B#'BanList.BanEntry'.address,
                 mask => B#'BanList.BanEntry'.mask,
                 name => B#'BanList.BanEntry'.name,
                 hash => B#'BanList.BanEntry'.hash,
                 reason => B#'BanList.BanEntry'.reason,
                 start => B#'BanList.BanEntry'.start,
                 duration => B#'BanList.BanEntry'.duration
             }
-         || B <- Bans
+         || B <- (case Bans of undefined -> []; _ -> Bans end)
         ],
         query => Query
     };

Apply the same pattern for ACL (groups, acls), UserList (users), and VoiceTarget (targets).

Also applies to: 266-302, 340-367

apps/mumble_protocol/src/mumble_msg.erl-38-39 (1)

38-39: ⚠️ Potential issue | 🟠 Major

No catch-all clause — unknown message types crash with function_clause.

Both raw_to_map/1 and from_map/1 will raise function_clause on any unrecognized record or map. If the protobuf schema adds a new message type, or an unknown record arrives from the wire, the entire connection process crashes.

Consider adding a catch-all that returns an {error, {unknown_message, ...}} tuple or at least a tagged unknown map, so callers can handle it gracefully.

Also applies to: 499-499

apps/mumble_protocol/src/mumble_server_sup.erl-57-79 (1)

57-79: 🛠️ Refactor suggestion | 🟠 Major

No validation that Port, CertFile, and KeyFile are present and non-undefined.

If any required option is missing, proplists:get_value returns undefined and the children will crash at startup with a confusing error. Validate early in init/1 and return a clear error.

Also, the log messages on lines 77–78 ("Starting UDP server…", "Starting TCP listener…") fire inside init/1 before the supervisor actually starts its children, which is misleading when reading logs.

apps/mumble_protocol/src/mumble_server_sup.erl-61-61 (1)

61-61: ⚠️ Potential issue | 🟠 Major

Default server_handler is mock_mumble_handler — a test module.

If server_handler is omitted from the options, production code silently uses a test mock handler. This should either have no default (crash early on missing config) or default to a real handler module.

Proposed fix
-    ServerHandler = proplists:get_value(server_handler, Options, mock_mumble_handler),
+    ServerHandler = case proplists:get_value(server_handler, Options) of
+        undefined -> error({missing_required_option, server_handler});
+        Handler -> Handler
+    end,
apps/mumble_protocol/src/mumble_udp_server.erl-141-160 (1)

141-160: ⚠️ Potential issue | 🟠 Major

Positional record access with element(3, User) is fragile and hard to read.

If the #user{} record definition in erlmur_user_manager is reordered, this will silently extract the wrong field. The record isn't available here because it's local to erlmur_user_manager.erl rather than in a shared header.

Consider either:

  1. Moving the #user{} record to a shared .hrl file and using record syntax, or
  2. Exposing an accessor like erlmur_user_manager:user_pid(User), or
  3. Having get_all_users/0 return a list of maps/tuples with named keys.
apps/erlmur/src/erlmur_server_handler.erl-61-68 (1)

61-68: ⚠️ Potential issue | 🟠 Major

Granting all permissions (0xFFFFFFFF) is a security risk even for an MVP.

Any connected client will have every permission including destructive ones (e.g., banning users, deleting channels). Consider starting with a sane default (e.g., speak + text + traverse) and expanding as features are implemented, to avoid accidental misuse during testing or early deployment.

apps/erlmur/src/erlmur_user_manager.erl-52-54 (1)

52-54: 🛠️ Refactor suggestion | 🟠 Major

get_all_users/0 exposes an internal #user{} record to callers.

The #user{} record is defined locally in this module, so callers must use positional access (e.g., element(3, User) in mumble_udp_server). This is fragile and a maintenance hazard.

Consider returning a list of maps or moving the record to a shared .hrl file.

apps/erlmur/src/erlmur_user_manager.erl-143-152 (1)

143-152: ⚠️ Potential issue | 🟠 Major

register_udp_addr does not remove the previous address from udp_map.

If a user's UDP address changes (e.g., NAT rebinding), the old {Addr, Pid} entry remains in udp_map. Subsequent lookups for the stale address will return the wrong pid, and the stale entry is never cleaned up.

Proposed fix — remove old addr before inserting new one
 handle_cast({register_udp_addr, SessionId, Addr}, State = `#state`{users = Users, udp_map = UdpMap}) ->
     case ets:lookup(Users, SessionId) of
-        [User] ->
+        [User = `#user`{udp_addr = OldAddr}] ->
+            case OldAddr of
+                undefined -> ok;
+                _ -> ets:delete(UdpMap, OldAddr)
+            end,
             ets:insert(Users, User#user{udp_addr = Addr}),
             ets:insert(UdpMap, {Addr, User#user.pid}),
             logger:debug("Registered UDP addr ~p for session ~p", [Addr, SessionId]);
apps/mumble_protocol/src/mumble_server.erl-38-38 (1)

38-38: ⚠️ Potential issue | 🟠 Major

Default server_handler is mock_mumble_handler — a test module.

If a caller omits server_handler from the options map, the server silently starts with a mock/test handler. This should either be the production handler (erlmur_server_handler) or have no default so the caller is forced to specify one.

Proposed fix
-    ServerHandler = maps:get(server_handler, Options, mock_mumble_handler),
+    case maps:find(server_handler, Options) of
+        {ok, ServerHandler} -> ok;
+        error -> error(missing_server_handler)
+    end,
apps/mumble_protocol/src/mumble_udp_proto.erl-34-41 (1)

34-41: ⚠️ Potential issue | 🟠 Major

Malformed UDP packets will crash the caller with badmatch/function_clause.

UDP input is untrusted. Several paths crash on malformed data:

  • Line 69: <<T:64>> = Timestamp crashes if the remaining bits ≠ 64.
  • Line 61: <<V:Len/binary, R1/binary>> = R0 crashes if R0 is shorter than Len.
  • split_voice_positional_speex_celt has no fallback clause for truncated frames.

Since handle/2 is called from mumble_server_conn, an unhandled crash will propagate up. Consider wrapping the body in a try/catch or adding guard clauses to avoid crashing on invalid input.

Proposed fix — wrap in try/catch at the entry point
 -spec handle(`#session`{}, binary()) -> ok.
-handle(Session, <<1:3, Timestamp/bits>>) ->
-    handle_ping(Session, Timestamp, false);
-handle(Session, <<Type:3, Target:5, Rest/binary>>) ->
+handle(Session, <<1:3, Timestamp/bits>>) ->
+    try
+        handle_ping(Session, Timestamp, false)
+    catch
+        _:Reason ->
+            logger:warning("[mumble_udp_proto] Malformed ping packet: ~p", [Reason]),
+            ok
+    end;
+handle(Session, <<Type:3, Target:5, Rest/binary>>) ->
+    try
         logger:debug("DataMsg~nType ~p~nTarget ~p", [Type, Target]),
         {Counter, R} = mumble_varint:decode(Rest),
         {Voice, Positional} = split_voice_positional(Type, R),
         mumble_server_conn:voice_data(Session#session.session_pid,
-                                  {voice_data, Type, Target, Counter, Voice, Positional}).
+                                  {voice_data, Type, Target, Counter, Voice, Positional})
+    catch
+        _:Reason ->
+            logger:warning("[mumble_udp_proto] Malformed voice packet (type ~p): ~p", [Type, Reason]),
+            ok
+    end.
apps/mumble_protocol/test/mumble_SUITE.erl-353-378 (1)

353-378: ⚠️ Potential issue | 🟠 Major

auto_cert_basic_test fails in CI — address pipeline failure.

The pipeline reports auto_cert_basic_test failed: cert_generation_failed, "Cert file not created". This means mumble_cert:ensure_auto_certs/2 is failing in the CI environment, likely because OpenSSL isn't available there or the priv_dir doesn't exist at runtime. Consider:

  1. Adding a precondition check (skip test if OpenSSL unavailable).
  2. Ensuring the priv directory is created before cert generation.
  3. Using an Erlang-native cert generation approach (e.g., public_key module) to avoid the OpenSSL CLI dependency.
apps/mumble_protocol/src/mumble.erl-130-130 (1)

130-130: ⚠️ Potential issue | 🟠 Major

Production API defaults to mock_mumble_handler — test module should not be a production default.

mock_mumble_handler is a test-only module. Using it as the default server_handler in the public start_server/1 API means any caller that omits the option will get mock behavior. This also applies to start_server/2 and start_server/3 (lines 168, 178) which hard-code the same default.

Either require the handler to be explicitly provided (no default) or use a real default handler module.

apps/mumble_protocol/test/mock_transport.erl-176-185 (1)

176-185: ⚠️ Potential issue | 🟠 Major

Blocking receive inside handle_call deadlocks the gen_server.

The recv handler (line 178-185) blocks the gen_server process in a receive loop when the message queue is empty. No other calls (send, setopts, get_sent_messages, etc.) can be processed during this wait. For a mock that needs concurrent inject + recv, consider returning {error, empty} immediately or using a continuation-based approach.

apps/mumble_protocol/test/mumble_SUITE.erl-238-240 (1)

238-240: ⚠️ Potential issue | 🟠 Major

Accessing UDP server state by element(3, State) is extremely brittle.

If the #state{} record in mumble_udp_server changes (field added, reordered, etc.), this silently reads the wrong field. Use sys:get_state/1 with pattern matching on the record, or add a public API function to mumble_udp_server that returns the port.

Recommended approach

Add a get_port/1 or port/0 API to mumble_udp_server and call it here instead of reaching into internal state:

-            State = sys:get_state(UDPPid),
-            Port = element(3, State),  %% port is the 3rd field in `#state`{}
+            Port = mumble_udp_server:get_port(UDPPid),
apps/mumble_protocol/src/mumble_server_conn.erl-191-195 (1)

191-195: ⚠️ Potential issue | 🟠 Major

Catch-all in authenticating stops the process on any unhandled event.

A stray or unexpected message (e.g., a late timer, system message, or monitor notification) will {stop, unhandled} the connection. Contrast with the established catch-all (line 284-286) which uses keep_state_and_data. Consider making authenticating equally resilient.

Proposed fix
 authenticating(Type, Msg, StateData) ->
     logger:warning("State: authenticating~nUnhandled ~p~n~p~n~p", [
         Type, Msg, StateData
     ]),
-    {stop, unhandled}.
+    {keep_state_and_data, ?TIMEOUT}.
apps/mumble_protocol/include/mumble_protocol.hrl-1-3 (1)

1-3: ⚠️ Potential issue | 🟠 Major

Missing include guard — re-inclusion will cause "record already defined" errors.

This header defines records and macros but has no -ifndef/-define/-endif guard. If it's transitively included more than once (e.g., from multiple headers), compilation will fail.

Proposed fix
+-ifndef(MUMBLE_PROTOCOL_HRL).
+-define(MUMBLE_PROTOCOL_HRL, true).
+
 -define(MUMBLE_PROTOCOL_VERSION_MAJOR, 1).
 -define(MUMBLE_PROTOCOL_VERSION_MINOR, 2).
 -define(MUMBLE_PROTOCOL_VERSION_PATCH, 4).
 
 ...  %% rest of the file
+
+-endif.
🟡 Minor comments (29)
AGENTS.md-63-67 (1)

63-67: ⚠️ Potential issue | 🟡 Minor

Incorrect Erlang syntax in error handling guideline.

Line 65 states try...of...else, but Erlang's try expression does not have an else clause. The correct form for cleanup is try Expr of Pattern -> Body catch Class:Reason -> Handler after Cleanup -> end. Consider correcting to try...of...catch...after.

.gitignore-35-37 (1)

35-37: ⚠️ Potential issue | 🟡 Minor

LGTM — but consider adding *.key to this file as well.

The new ignore entries for editor/agent directories are appropriate. Note that *.pem is already ignored (Line 18), but *.key is not — this allowed apps/erlmur/priv/server.key to be committed. Adding *.key here would prevent future accidental commits of private key files.

apps/mumble_protocol/docs/establishing_connection.md-99-139 (1)

99-139: ⚠️ Potential issue | 🟡 Minor

Diagram section numbering is inconsistent with the document headings.

The document defines five phases numbered 1–5 (Transport → Initial Exchange → Crypto → State Sync → Finalization), but the Mermaid diagram uses a different numbering: Version/Authenticate messages appear outside any numbered note, "2" is Crypto & Config, "3" is State Sync, "4" is Finalization, and "5" is Maintenance. This will confuse readers cross-referencing the text with the diagram.

Also, Line 120 says ChannelState (removed links) — the phrase "removed links" is ambiguous. Consider "without links" or "initial channel state" to clarify that links haven't been stripped but simply aren't included yet.

apps/mumble_protocol/src/mumble_varint.erl-56-59 (1)

56-59: ⚠️ Potential issue | 🟡 Minor

Large positive integers (≥ 2⁶⁴) silently truncate in the 64-bit encoding.

When Num >= 16#10000000000000000, the Num:64 bit-syntax silently wraps, so decode(encode(N)){N, <<>>}. Consider adding a guard or raising an error for out-of-range values.

Proposed fix
-encode(Num) ->
-    <<2#11110100:8, Num:64>>.
+encode(Num) when Num < 16#10000000000000000 ->
+    <<2#11110100:8, Num:64>>;
+encode(Num) ->
+    error({varint_out_of_range, Num}).
apps/mumble_protocol/src/mumble_client_conn.erl-217-224 (1)

217-224: ⚠️ Potential issue | 🟡 Minor

UDP send functions are stubs, but timers actively invoke them.

send_udp_ping/1 and send_udp/2 only log debug messages. However, line 194 starts a repeating 5-second timer that calls send_udp_ping, and line 183 re-arms it. This creates perpetual no-op timer cycles. Consider either implementing UDP or not starting the timer until it's ready.

Do you want me to open an issue to track the UDP implementation?

apps/ocb128_crypto/src/ocb128_crypto.erl-256-260 (1)

256-260: ⚠️ Potential issue | 🟡 Minor

Add minimum_otp_vsn to rebar.config to enforce OTP 25+ requirement.

The project's README documents "Erlang/OTP 25 or later" as a prerequisite, but this requirement is not enforced in rebar.config. Add {minimum_otp_vsn, "25"} to ensure the build fails early on incompatible OTP versions. Additionally, if supporting OTP 25 specifically, explicitly enable the maybe_expr feature in erl_opts since it was experimental and disabled by default in OTP 25:

{erl_opts, [
    debug_info,
    -enable-feature maybe_expr,
    {i, "apps/mumble_protocol/include"},
    {i, "_build/default/plugins/gpb/include"}
]}.
apps/mumble_protocol/test/mumble_cert_tests.erl-138-158 (1)

138-158: ⚠️ Potential issue | 🟡 Minor

Test doesn't verify any reuse logic.

reuse_existing_cert_test generates a cert, sleeps, then reads the same file's mtime — confirming only that a file doesn't change by itself. It never calls any production mumble_cert function (e.g., ensure_auto_certs/2 or validate_cert_files/2) to verify that existing certs are actually reused rather than regenerated.

apps/erlmur/src/erlmur_sup.erl-23-32 (1)

23-32: ⚠️ Potential issue | 🟡 Minor

Misleading log message: child hasn't started yet at line 31.

init/1 only builds the child specification — the supervisor starts children after init/1 returns. The log on line 31 ("User manager started") is inaccurate and could mislead during debugging. Line 24 is similarly premature.

Proposed fix
 init([]) ->
-    logger:info("[erlmur_sup] Starting user manager..."),
+    logger:info("[erlmur_sup] Initializing supervision tree"),
     UserManager = #{
         id => erlmur_user_manager,
         start => {erlmur_user_manager, start_link, []},
         restart => permanent,
         type => worker
     },
-    logger:info("[erlmur_sup] User manager started"),
     {ok, {{one_for_one, 5, 10}, [UserManager]}}.
apps/mumble_protocol/test/mumble_cert_tests.erl-114-117 (1)

114-117: ⚠️ Potential issue | 🟡 Minor

Weak assertion: "test" matches almost any string.

The fallback check string:find(Subject, "test") on line 116 is too broad and would pass even if the subject were completely wrong. Consider matching a more specific substring like "test.example.com" or "TestOrg" only.

Proposed fix
         ?assert(
             (string:find(Subject, "test.example.com") =/= nomatch) orelse
-            (string:find(Subject, "test") =/= nomatch)
+            (string:find(Subject, "TestOrg") =/= nomatch)
         )
apps/mumble_protocol/README.md-31-37 (1)

31-37: ⚠️ Potential issue | 🟡 Minor

Several spelling errors in the documentation.

  • Line 31: "recieved" → "received"
  • Line 37: "reciveing" → "receiving"
  • Line 45: "explicitely" → "explicitly"
Proposed fix (lines 31, 37)
-If no `Ping` is recieved for 30sec the client will be disconnected from the server.
+If no `Ping` is received for 30 seconds the client will be disconnected from the server.
-If reciveing a `UDPTunnel` message the connection is marked "UDP verified" false, until new UDP verification is done.
+If receiving a `UDPTunnel` message the connection is marked "UDP verified" false, until new UDP verification is done.
apps/mumble_protocol/rebar.config-20-28 (1)

20-28: ⚠️ Potential issue | 🟡 Minor

Normalize indentation on lines 26–27.

Lines 26–27 use tabs while the rest of the file uses spaces. For consistency, use spaces throughout.

apps/erlmur/src/erlmur_app.erl-18-21 (1)

18-21: ⚠️ Potential issue | 🟡 Minor

list_to_atom/1 on environment variable input risks atom table exhaustion.

While this is a startup-only path and unlikely to be exploited in practice, using list_to_existing_atom/1 with a try/catch fallback (or a whitelist) would be safer.

Proposed fix
-        LevelStr -> list_to_atom(LevelStr)
+        LevelStr ->
+            try list_to_existing_atom(LevelStr)
+            catch error:badarg ->
+                logger:warning("[erlmur] Invalid log level '~s', defaulting to info", [LevelStr]),
+                info
+            end
apps/mumble_protocol/test/mumble_api_tests.erl-62-74 (1)

62-74: ⚠️ Potential issue | 🟡 Minor

Tests depend on validation order in mumble:start_server/2 — may break if file-existence checks run first.

start_server_error_cert_only_test passes "/tmp/cert.pem" (which may or may not exist) with undefined as the key file, expecting {error, cert_key_mismatch}. If the production code checks file existence before the undefined-pairing check, this will return {error, {cert_file_not_found, ...}} when /tmp/cert.pem doesn't exist, or succeed past file validation when it does. Same issue for start_server_error_key_only_test.

Use clearly non-existent but deterministic paths, or generate temp files in the test, to avoid environment-dependent behavior.

apps/mumble_protocol/test/e2e_connection_SUITE.erl-160-165 (1)

160-165: ⚠️ Potential issue | 🟡 Minor

Fragile: accessing record fields by positional index with element/7.

If the -record(state, ...) definition in mumble_client_conn changes (field added, removed, or reordered), element(7, StateData) will silently read the wrong field or crash. The comment on lines 163-164 documenting the assumed layout is helpful but doesn't prevent breakage.

Consider exporting a helper or using a test-only header to include the record definition, or querying the state through a public API (e.g., a dedicated is_udp_verified/1 function).

apps/mumble_protocol/test/e2e_connection_SUITE.erl-71-88 (1)

71-88: ⚠️ Potential issue | 🟡 Minor

Recursive wait_for_* helpers can loop indefinitely on a stream of unexpected messages.

wait_for_ping/1, wait_for_text/1, and wait_for_udp_tunnel_voice/2 recurse when they receive an unexpected {mumble_msg, _}. Each recursive call resets the after timeout, so a steady stream of non-matching messages would keep these spinning without ever timing out.

Pass a deadline (e.g., erlang:monotonic_time(millisecond) + Timeout) and compute the remaining time on each recursion, or use a monotonic deadline and max(0, Deadline - Now) as the after value.

Example fix for wait_for_ping
-wait_for_ping(Timestamp) ->
+wait_for_ping(Timestamp) ->
+    wait_for_ping(Timestamp, erlang:monotonic_time(millisecond) + 2000).
+
+wait_for_ping(Timestamp, Deadline) ->
+    Remaining = max(0, Deadline - erlang:monotonic_time(millisecond)),
     receive
         {mumble_msg, #{message_type := 'Ping', timestamp := TS} = M} when TS == Timestamp ->
             ct:pal("Received expected Ping: ~p", [M]),
             verify_ping_response(M),
             ok;
         {mumble_msg, #{message_type := Type}} when 
             Type == 'Version'; Type == 'CryptSetup'; 
             Type == 'CodecVersion'; Type == 'ServerSync' ->
-            wait_for_ping(Timestamp);
+            wait_for_ping(Timestamp, Deadline);
         {mumble_msg, M} ->
             ct:pal("Received unexpected msg while waiting for ping: ~p", [M]),
-            wait_for_ping(Timestamp)
-    after 2000 ->
+            wait_for_ping(Timestamp, Deadline)
+    after Remaining ->
         ct:fail(ping_timeout)
     end.

Also applies to: 108-123, 171-197

apps/mumble_protocol/src/mumble_udp_server.erl-38-40 (1)

38-40: ⚠️ Potential issue | 🟡 Minor

send/3 spec is misleading — gen_server:cast always returns ok.

The typespec declares ok | {error, term()} but the implementation uses gen_server:cast, which unconditionally returns ok. The caller can never observe an error. Either change the spec to -spec send(...) -> ok. or switch to gen_server:call if you need error propagation.

Proposed fix
--spec send(IP :: inet:ip_address(), inet:port_number(), binary()) -> ok | {error, term()}.
+-spec send(IP :: inet:ip_address(), inet:port_number(), binary()) -> ok.
apps/mumble_protocol/src/mumble_server.erl-86-95 (1)

86-95: ⚠️ Potential issue | 🟡 Minor

erlang:system_info(otp_release) returns a charlist, not a binary.

The #version{} record's os_version field likely expects a binary (consistent with the release and os fields on lines 92-93). Same issue as in erlmur_server_handler:get_caps/1.

Proposed fix
-        os_version = erlang:system_info(otp_release)
+        os_version = list_to_binary(erlang:system_info(otp_release))
apps/mumble_protocol/src/mumble_version.erl-36-46 (1)

36-46: ⚠️ Potential issue | 🟡 Minor

decode/1 conflates V1 and V2 formats for versions where major = 0 and minor = 0.

The heuristic "≤ 0xFFFFFFFF → V1, else → V2" fails for V2-encoded versions with major = 0, minor = 0: e.g., version 0.0.1 encodes as V2 = 0x0000_0000_0001_0000 (= 65536), which falls into the V1 guard and is incorrectly decoded as {major=0, minor=1, patch=0}.

For real Mumble versions (≥ 1.0.0) this won't trigger, but it makes the API subtly incorrect. Consider requiring the caller to specify the format explicitly:

Proposed fix
--spec decode(non_neg_integer()) -> `#version`{}.
-decode(V1) when V1 >= 0, V1 =< 16#FFFFFFFF ->
+-spec decode(v1 | v2, non_neg_integer()) -> `#version`{}.
+decode(v1, V1) when V1 >= 0, V1 =< 16#FFFFFFFF ->
     <<Major:16, Minor:8, Patch:8>> = <<V1:32>>,
     `#version`{major = Major, minor = Minor, patch = Patch};
-decode(V2) when V2 > 16#FFFFFFFF, V2 =< 16#FFFFFFFFFFFFFFFF ->
+decode(v2, V2) when V2 >= 0, V2 =< 16#FFFFFFFFFFFFFFFF ->
     <<Major:16, Minor:16, Patch:16, _Zero:16>> = <<V2:64>>,
     `#version`{major = Major, minor = Minor, patch = Patch}.
apps/erlmur/src/erlmur_server_handler.erl-78-86 (1)

78-86: ⚠️ Potential issue | 🟡 Minor

Convert erlang:system_info(otp_release) to binary for consistency with documented behavior.

erlang:system_info(otp_release) returns a charlist, but the get_caps() callback documentation and all other implementations in the codebase use binaries for os_version. This causes a type inconsistency that may cause encoding failures in downstream protobuf serialization.

Proposed fix
-        os_version => erlang:system_info(otp_release)
+        os_version => list_to_binary(erlang:system_info(otp_release))

Note: The same issue exists in mumble_server.erl line 94 and should be corrected there as well.

apps/mumble_protocol/test/mock_transport.erl-190-201 (1)

190-201: ⚠️ Potential issue | 🟡 Minor

setopts with {active, true} sends spurious ssl_passive and empty data messages.

Sending {ssl_passive, self()} followed by {ssl, self(), <<>>} when active mode is enabled doesn't match real SSL behavior and could confuse tested code. Consider removing these notifications or only sending them when there's actual queued data to deliver.

apps/mumble_protocol/include/mumble_protocol.hrl-85-85 (1)

85-85: ⚠️ Potential issue | 🟡 Minor

Type spec regular | type looks like a placeholder — the atom type is likely a copy-paste artifact.

type = regular :: regular | type

Did you mean a different atom here, such as temporary or another connection type?

apps/mumble_protocol/src/mumble_server_conn.erl-302-311 (1)

302-311: ⚠️ Potential issue | 🟡 Minor

No clause for empty TCP data — byte_size(Data) >= 1 guard will crash on empty binary.

If {ssl, Socket, <<>>} is delivered (unlikely but possible with certain TLS implementations), the guard fails and there is no matching clause, causing a function_clause crash propagated to the state callback. Consider adding a clause or removing the guard (an empty decode is harmless).

apps/mumble_protocol/test/mumble_SUITE.erl-77-96 (1)

77-96: ⚠️ Potential issue | 🟡 Minor

OpenSSL availability check and error detection are fragile.

os:cmd("which openssl") doesn't work on Windows and may return unexpected output on some Unix systems. The error check "Error" ++ _ at line 94 only catches outputs starting with that literal prefix — OpenSSL errors on different platforms may have different formats. Consider a more robust check, such as verifying the exit code or checking for the generated file's existence immediately after.

apps/mumble_protocol/test/mumble_SUITE.erl-566-571 (1)

566-571: ⚠️ Potential issue | 🟡 Minor

cleanup_auto_generated_certs/0 deletes all .pem/.key files in priv_dir.

The wildcard *.pem and *.key will delete any PEM/key file, not just auto-generated ones. If priv_dir contains other certificate files, they would be inadvertently removed. Consider targeting specific filenames (e.g., auto_server.pem, auto_server.key).

Proposed fix
 cleanup_auto_generated_certs() ->
-    PrivDir = code:priv_dir(mumble_protocol),
-    Files = filelib:wildcard(filename:join(PrivDir, "*.pem")) ++
-            filelib:wildcard(filename:join(PrivDir, "*.key")),
-    [file:delete(F) || F <- Files],
-    ok.
+    case code:priv_dir(mumble_protocol) of
+        {error, _} -> ok;
+        PrivDir ->
+            file:delete(filename:join(PrivDir, "auto_server.pem")),
+            file:delete(filename:join(PrivDir, "auto_server.key")),
+            ok
+    end.
apps/mumble_protocol/test/mock_transport.erl-68-70 (1)

68-70: ⚠️ Potential issue | 🟡 Minor

accept/2 parameter order is swapped vs. ranch_transport callback.

Ranch calls Transport:accept(ListenSocket, Timeout), but here the arguments are accept(_Timeout, ListenSocket) — reversed. While the mock ignores both values, this mismatch could mask bugs if the mock is ever extended to use the parameters, and doesn't match the behaviour spec.

Proposed fix
-accept(_Timeout, ListenSocket) ->
+accept(ListenSocket, _Timeout) ->
     %% Mock implementation
     {ok, {mock_accepted, ListenSocket}}.
apps/mumble_protocol/src/mumble_server_conn.erl-498-499 (1)

498-499: ⚠️ Potential issue | 🟡 Minor

maybe_undefined(0) converts legitimate zero counts to undefined in Ping responses.

A client with 0 late or 0 lost packets is a valid state. Converting it to undefined will cause these fields to be omitted from the protobuf message, which misrepresents the data. The Mumble protocol expects integer values here; undefined typically means "not set" / "no data", not "zero".

Proposed fix — remove the conversion or only omit truly unknown values
-maybe_undefined(0) -> undefined;
-maybe_undefined(V) -> V.
+maybe_undefined(undefined) -> undefined;
+maybe_undefined(V) -> V.
apps/mumble_protocol/src/mumble_server_conn.erl-447-457 (1)

447-457: ⚠️ Potential issue | 🟡 Minor

send_msg/2 has no fallback clause if mumble_tcp_proto:pack/1 returns a non-binary (e.g., error tuple).

The single case arm Bin when is_binary(Bin) will raise case_clause if pack/1 returns {error, _} or any other non-binary value. Consider adding a catch-all clause to handle packing failures gracefully.

Proposed fix
 send_msg(`#state`{socket = Socket, transport = Transport}, Map) ->
     case mumble_tcp_proto:pack(Map) of
         Bin when is_binary(Bin) ->
             logger:notice("Sending ~p (~p bytes)", [maps:get(message_type, Map), byte_size(Bin)]),
             case Transport:send(Socket, Bin) of
                 ok -> ok;
                 {error, Reason} ->
                     logger:error("Failed to send ~p: ~p", [maps:get(message_type, Map), Reason]),
                     {error, Reason}
-            end
+            end;
+        Other ->
+            logger:error("Failed to pack ~p: ~p", [maps:get(message_type, Map), Other]),
+            {error, {pack_failed, Other}}
     end.
apps/mumble_protocol/test/mock_transport.erl-223-239 (1)

223-239: ⚠️ Potential issue | 🟡 Minor

Messages in {active, once} mode are both queued and delivered — can cause double processing.

When inject_message fires with {active, once}, the data is appended to message_queue and sent to the owner as {ssl, self(), Data}. A subsequent recv call will return the same data from the queue. Real SSL active-once delivers the data and resets to passive without queuing. Consider removing from queue when delivering in active mode, or not queueing at all.

Proposed fix — don't queue when delivering in active mode
 handle_cast({inject_message, Data}, State = #{owner := Owner, opts := Opts}) ->
-    NewQueue = case State of
-        #{message_queue := Queue} -> Queue ++ [Data];
-        _ -> [Data]
-    end,
-    
     %% Check if we should notify the owner (active mode)
     case lists:keyfind(active, 1, Opts) of
         {active, true} when Owner =/= undefined ->
-            Owner ! {ssl, self(), Data};
+            Owner ! {ssl, self(), Data},
+            {noreply, State};
         {active, once} when Owner =/= undefined ->
-            Owner ! {ssl, self(), Data};
+            Owner ! {ssl, self(), Data},
+            %% Reset to passive after delivery
+            {noreply, State#{opts := lists:keyreplace(active, 1, Opts, {active, false})}};
         _ ->
-            ok
-    end,
-    
-    {noreply, State#{message_queue := NewQueue}};
+            NewQueue = maps:get(message_queue, State, []) ++ [Data],
+            {noreply, State#{message_queue := NewQueue}}
+    end;
apps/mumble_protocol/src/mumble.erl-125-158 (1)

125-158: ⚠️ Potential issue | 🟡 Minor

Partial cert specification (e.g., cert without key) silently falls through to the catch-all branch.

When a user provides cert_file but not key_file (or vice versa), the _ -> branch at line 149 passes undefined for the missing file to mumble_server:start_server/1, which will likely produce a confusing error downstream. Consider validating that both files are provided together or neither is.

Proposed fix — validate pairing
     case {CertFile, KeyFile, AutoCreate} of
         {undefined, undefined, true} ->
             %% Auto-generate certificates
             ...;
+        {undefined, undefined, false} ->
+            {error, {missing_certificates, "Either provide cert_file and key_file, or set auto_create_cert => true"}};
+        {undefined, _, _} ->
+            {error, {missing_option, cert_file}};
+        {_, undefined, _} ->
+            {error, {missing_option, key_file}};
         _ ->
             %% Use provided files
             ...
     end.

Comment on lines 1 to 28
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDDy+ryTbu84Ntf
DvTfpZh/TokUxA5Y2gFIIbj6KYjrmPsh2JDSgxuBAL6iYgVazd6IK/+WPEpc7eUR
e+9T2Yr00EAsM6rCaQ/uQFBOvvR7ngPtpZ2pFbfVtoJSA9dwfUjdpyhvRiZx4rl2
VwWamqEEuCI+xkLKTSNXFtvY5h8Yg0LLpHDT6+wTff68lrhttBevjkHvhNKe/php
Ys1ivz4OmLpVHOH6wfMWH6jErOql8I2N6vQ2GmumcGh9DzZRQc4stJU5biTFWCRp
Ez7iAZsA5PY/prRUSRezdECnFClphzK/gSegJcXFYC6pozHAAEAoR7GzQo6zndkc
c27P417fAgMBAAECggEAMoDgVuS/Fq6nVyMr/Z4gskHHMSCLFNcSupDcPkbFZwPc
TzQl0u73grR297qx5+SvUE+NCyr9AXBphDOEHYxA//nRe7VOB/yCm/Fh5ZXyyQoO
uFdeTfVoqEr93XrZMM5YI7hQwTcGhpy1OvWiX+kfvHBkgO2x3626/mVKiu18utz4
Swap3rbPhcRv0FHMX4P5FLKe++kATl/OPDg/pHVcAlEQ2plvjMbOeMZ4XeQ2xrrq
s6ZI3NHnbvLbo/hLyAKcee7NvRYzEc0Hl9A7D2LWHAfUKgbhBP31+JUK2TAMdCiH
EeRtQX4S5zrjOiVFJnQDbU86AcD9XCf8V+ousH7vSQKBgQD+D9hNEjJiiWhyjgIZ
icQD3BhpwA7DcasUPH1BWsie+iiq+YQul8DFPiQPat/utbLktrh84pkaXRNWPnb6
lKd/MEiGqFxY5Z6kbOFMMTg6tRPzdX+hNzp2YeO7xhvwE+5N5beTNi6iWkNEmt0X
2HlhiPz/Ifs3VkrCnEj3dZC5dwKBgQDFSkl43galeowR78EgK0h0rRzowJm6OrBk
ZUFCsZMx1D/LNyAnLmtq1aD5zepSqSrplGuallDkHdAYaxLAUSDPK74WuxwjJfAS
lJc/WOKskgZE30r4+iK4FjkUSbqlkI1+pVJJ/Y91DfdMKMx1B4fudyaLVHfp4Sl+
xJP60HVf2QKBgQDnAvNUCRLFZFoTp6qjHBCezddo+NizwDymFuZxsaGwZJjr9hvJ
3fTLpNhXKH/b51119+tCqyw5u54kkoHPEbJHxVh9EMN/lbKtoc8uX6bYQuSUQCfA
KJ7BG/teNyIaJ5VNQW6trduzhk2aKRKWZyDfnhclyflKdO1fjYoPcNgauwKBgC4t
0TzFW3DoWljocaACAZet3ZJtYo5ApoEZYkkmH3oOYFBEuvXw25vKHT/bfvmSN0Ut
RHKOtKhwggD1LxJs6u0TJA9WBKQFgUgYF77wIz6Zf3YnffkBM5c7PGkPSOzwBO6b
68L94tJXToc5FYJfHOYoZZE/WEIaPgg6IxkWKiMhAoGBAI/KEfGc+4nOs2ZwgNxB
uC4d15nIu6ukLLe8U7M++wJsQDW+0NAY3LSNtm8J23ytUki8kGQzJjH8mL8cPTN8
S/BqeelrOmml3WMKPw/F4FzHFXwQDTMYMjqMAhLjBNBGj6dG18LA19gmjawnQUpN
+noCfn6GIXzXpAhYLbyhoPhk
-----END PRIVATE KEY-----
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Private key committed to the repository — security risk.

This file contains a plaintext private key checked into version control. Even if intended only for development/testing, committed secrets persist in Git history and can be inadvertently deployed to production. Gitleaks also flagged this.

Recommendations:

  1. Remove this file from the repository and Git history (e.g., git filter-repo).
  2. Add *.key to .gitignore (note: *.pem is already ignored but *.key is not).
  3. Generate keys at build/deploy time or provide them via environment configuration.
🧰 Tools
🪛 Gitleaks (8.30.0)

[high] 1-28: Identified a Private Key, which may compromise cryptographic security and sensitive data encryption.

(private-key)

🤖 Prompt for AI Agents
In `@apps/erlmur/priv/server.key` around lines 1 - 28, A plaintext private key
(apps/erlmur/priv/server.key) was committed—remove it from the repo and history,
stop tracking new .key files, and replace with a secure provisioning flow:
remove the file and purge it from Git history (e.g., use git filter-repo or
equivalent to delete apps/erlmur/priv/server.key and its blobs), add a rule to
.gitignore to cover *.key so future keys are not committed, and update
build/deploy to generate or inject keys from secure sources/env vars (or
reference secrets manager) instead of bundling server.key in source control.

Comment on lines +113 to +129
authenticating(enter, _, State) ->
%% Send Version
V = #version{major = 1,
minor = 2,
patch = 4},
{V1, V2} = mumble_version:encode(V),
VerMsg =
#{message_type => 'Version',
version_v1 => V1,
version_v2 => V2,
release => <<"erlmur-client">>},
ssl:send(State#state.socket, mumble_tcp_proto:pack(VerMsg)),
%% Send Authenticate
AuthMsg = #{message_type => 'Authenticate', username => <<"TestUser">>},
ssl:send(State#state.socket, mumble_tcp_proto:pack(AuthMsg)),
ssl:setopts(State#state.socket, [{active, once}]),
keep_state_and_data;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Hardcoded username <<"TestUser">> — authentication ignores user-provided credentials.

Line 126 always sends <<"TestUser">> regardless of what was passed in Opts. The username (and optionally password/tokens) should be extracted from the options map passed through start_link/3.

🐛 Proposed fix
 authenticating(enter, _, State) ->
     %% Send Version
     V = `#version`{major = 1,
                  minor = 2,
                  patch = 4},
     {V1, V2} = mumble_version:encode(V),
     VerMsg =
         #{message_type => 'Version',
           version_v1 => V1,
           version_v2 => V2,
           release => <<"erlmur-client">>},
     ssl:send(State#state.socket, mumble_tcp_proto:pack(VerMsg)),
     %% Send Authenticate
-    AuthMsg = #{message_type => 'Authenticate', username => <<"TestUser">>},
+    AuthMsg = #{message_type => 'Authenticate', username => maps:get(username, State#state.opts, <<"TestUser">>)},
     ssl:send(State#state.socket, mumble_tcp_proto:pack(AuthMsg)),
     ssl:setopts(State#state.socket, [{active, once}]),
     keep_state_and_data;

This requires storing Opts in the state record:

--record(state, {socket, transport = ssl, session_id, parent, stats = `#stats`{}, udp_verified = false, udp_timer}).
+-record(state, {socket, transport = ssl, session_id, parent, opts = #{}, stats = `#stats`{}, udp_verified = false, udp_timer}).

And threading it through init:

 init({Host, Port, Opts, Parent}) ->
-    {ok, connecting, `#state`{parent = Parent}, {next_event, internal, {connect, Host, Port, Opts}}}.
+    {ok, connecting, `#state`{parent = Parent, opts = Opts}, {next_event, internal, {connect, Host, Port, Opts}}}.
🤖 Prompt for AI Agents
In `@apps/mumble_protocol/src/mumble_client_conn.erl` around lines 113 - 129, The
authenticating/3 clause currently sends a hardcoded username <<"TestUser">>
instead of using credentials supplied to start_link/3; update the code to store
the passed-in Opts into the connection State (e.g. add an opts field on the
state record when initializing in init/start_link), then in authenticating/3
read the username (and optional password/token) from State (e.g.
State#state.opts.username or similar) and use that value when building AuthMsg
in place of <<"TestUser">>; ensure start_link/3 and init thread the Opts into
State so authenticating/3 can access them.

Comment on lines +315 to +329
process_messages([Msg | Rest], Allowed, StateData) ->
MsgName = maps:get(message_type, Msg),
IsAllowed = Allowed == all orelse lists:member(MsgName, Allowed),
if
IsAllowed ->
case handle_protocol_msg(Msg, StateData) of
{ok, NewStateData} ->
process_messages(Rest, Allowed, NewStateData);
{transition, NextState, NewStateData} ->
process_messages_in_new_state(Rest, NextState, NewStateData)
end;
true ->
logger:warning("Message ~p not allowed", [MsgName]),
process_messages(Rest, Allowed, StateData)
end.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

{stop, ...} return from handle_protocol_msg is unhandled — will crash with case_clause.

handle_protocol_msg can return {stop, normal, StateData} (lines 407 and 444), but process_messages only matches {ok, _} and {transition, _, _}. This will raise a case_clause exception whenever authentication fails or a handler returns {stop, ...}.

🐛 Proposed fix
 process_messages([Msg | Rest], Allowed, StateData) ->
     MsgName = maps:get(message_type, Msg),
     IsAllowed = Allowed == all orelse lists:member(MsgName, Allowed),
     if
         IsAllowed ->
             case handle_protocol_msg(Msg, StateData) of
                 {ok, NewStateData} ->
                     process_messages(Rest, Allowed, NewStateData);
                 {transition, NextState, NewStateData} ->
-                    process_messages_in_new_state(Rest, NextState, NewStateData)
+                    process_messages_in_new_state(Rest, NextState, NewStateData);
+                {stop, Reason, NewStateData} ->
+                    {stop, Reason, NewStateData}
             end;
         true ->
             logger:warning("Message ~p not allowed", [MsgName]),
             process_messages(Rest, Allowed, StateData)
     end.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
process_messages([Msg | Rest], Allowed, StateData) ->
MsgName = maps:get(message_type, Msg),
IsAllowed = Allowed == all orelse lists:member(MsgName, Allowed),
if
IsAllowed ->
case handle_protocol_msg(Msg, StateData) of
{ok, NewStateData} ->
process_messages(Rest, Allowed, NewStateData);
{transition, NextState, NewStateData} ->
process_messages_in_new_state(Rest, NextState, NewStateData)
end;
true ->
logger:warning("Message ~p not allowed", [MsgName]),
process_messages(Rest, Allowed, StateData)
end.
process_messages([Msg | Rest], Allowed, StateData) ->
MsgName = maps:get(message_type, Msg),
IsAllowed = Allowed == all orelse lists:member(MsgName, Allowed),
if
IsAllowed ->
case handle_protocol_msg(Msg, StateData) of
{ok, NewStateData} ->
process_messages(Rest, Allowed, NewStateData);
{transition, NextState, NewStateData} ->
process_messages_in_new_state(Rest, NextState, NewStateData);
{stop, Reason, NewStateData} ->
{stop, Reason, NewStateData}
end;
true ->
logger:warning("Message ~p not allowed", [MsgName]),
process_messages(Rest, Allowed, StateData)
end.
🤖 Prompt for AI Agents
In `@apps/mumble_protocol/src/mumble_server_conn.erl` around lines 315 - 329,
process_messages currently only matches {ok, _} and {transition, _, _} from
handle_protocol_msg, so a {stop, Reason, NewStateData} return (from
handle_protocol_msg) will cause a case_clause crash; update process_messages to
handle the {stop, Reason, NewStateData} tuple (e.g., add a branch in the case
that returns or propagates {stop, Reason, NewStateData} instead of falling
through), and ensure process_messages_in_new_state and/or the caller of
process_messages can accept and handle that {stop, ...} result; reference:
process_messages/3, handle_protocol_msg/2, process_messages_in_new_state/3.

Comment on lines +339 to +354
handle_protocol_msg(#{message_type := 'Version'}, StateData = #state{handler_mod = Mod}) ->
Caps = case erlang:function_exported(Mod, get_caps, 1) of
true -> Mod:get_caps(StateData#state.handler_state);
false -> #{}
end,
{V1, V2} = version_enc(Caps),
ServerVersion = #{
message_type => 'Version',
version_v1 => V1,
version_v2 => V2,
os => maps:get(os, Caps, ~"Linux"),
release => maps:get(release, Caps, ~"1.2.4"),
os_version => maps:get(os_version, Caps, ~"1.0")
},
send_msg(StateData, ServerVersion),
{ok, StateData};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

version_enc(#{}) will crash with function_clause when handler doesn't export get_caps/1.

When erlang:function_exported(Mod, get_caps, 1) returns false, Caps is set to #{}. Then version_enc(Caps) is called, but version_enc/1 (line 475) only matches #{major := Major, minor := Minor, patch := Patch} — an empty map won't match, causing a function_clause crash.

🐛 Proposed fix — add a default clause or provide default caps
+version_enc(#{major := Major, minor := Minor, patch := Patch}) ->
+    V1 = (Major bsl 16) bor (Minor bsl 8) bor Patch,
+    V2 = (Major bsl 48) bor (Minor bsl 32) bor (Patch bsl 16),
+    {V1, V2};
+version_enc(_) ->
+    Major = ?MUMBLE_PROTOCOL_VERSION_MAJOR,
+    Minor = ?MUMBLE_PROTOCOL_VERSION_MINOR,
+    Patch = ?MUMBLE_PROTOCOL_VERSION_PATCH,
+    V1 = (Major bsl 16) bor (Minor bsl 8) bor Patch,
+    V2 = (Major bsl 48) bor (Minor bsl 32) bor (Patch bsl 16),
+    {V1, V2}.
-version_enc(#{major := Major, minor := Minor, patch := Patch}) ->
-    V1 = (Major bsl 16) bor (Minor bsl 8) bor Patch,
-    V2 = (Major bsl 48) bor (Minor bsl 32) bor (Patch bsl 16),
-    {V1, V2}.
🤖 Prompt for AI Agents
In `@apps/mumble_protocol/src/mumble_server_conn.erl` around lines 339 - 354, The
handler sets Caps = #{} when Mod does not export get_caps/1 which then crashes
version_enc(Caps) because version_enc/1 only matches #{major := Major, minor :=
Minor, patch := Patch}; fix by ensuring version_enc always receives a map with
defaults or by adding a fallback clause in version_enc/1: either change
handle_protocol_msg (where Caps is set) to supply default keys (major, minor,
patch) when get_caps/1 is absent, or add a new version_enc(Caps) clause that
reads missing keys via maps:get/3 (providing sane defaults) so a function_clause
error cannot occur; reference functions: handle_protocol_msg, get_caps/1,
version_enc/1 and the Caps variable when making the change.

Comment on lines +1 to +41
-module(mock_mumble_handler).
-behaviour(mumble_server_behaviour).

start_link(_Opts) ->
%% Simple mock - just return {ok, self()} as a placeholder pid
{ok, self()}.

stop(Pid) when is_pid(Pid) ->
catch exit(Pid, normal),
ok.

init(_Opts) ->
{ok, #{}}.

handle_msg(Msg, State) ->
case maps:get(message_type, Msg) of
'TextMessage' ->
%% Reply with another text message
Reply = #{
message_type => 'TextMessage',
message => <<"Echo: ", (maps:get(message, Msg))/binary>>
},
%% We need to know who to send it to, but mock doesn't have session pid easily.
%% mumble_server_conn handles casting back if we return it?
%% Actually mumble_server_conn:handle_protocol_msg calls Mod:handle_msg.
%% If we want to send something back, we'd typically use mumble_server_conn:send(self(), Reply),
mumble_server_conn:send(self(), Reply),
{ok, State};
_ ->
{ok, State}
end.

get_caps(_State) ->
#{major => 1, minor => 2, patch => 4,
os => <<"MockOS">>,
release => <<"1.0">>,
os_version => <<"1.0">>}.

authenticate(_AuthMsg, State) ->
UserInfo = #{session_id => 1, name => <<"MockUser">>},
{ok, UserInfo, State}.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Missing -export directive — all callbacks are unexported.

The module declares -behaviour(mumble_server_behaviour) but never exports any function. In Erlang, without an -export([...]) or -compile(export_all) directive, all functions are module-local. Any external call (e.g., mock_mumble_handler:init([]) from mumble_server_conn) will fail with undef.

Proposed fix
 -module(mock_mumble_handler).
 -behaviour(mumble_server_behaviour).
+
+-export([start_link/1, stop/1, init/1, handle_msg/2, get_caps/1, authenticate/2]).
🤖 Prompt for AI Agents
In `@apps/mumble_protocol/test/mock_mumble_handler.erl` around lines 1 - 41, The
module lacks an -export directive so its callbacks are not callable; add an
export list that exports start_link/1, stop/1, init/1, handle_msg/2, get_caps/1,
and authenticate/2 (matching the functions implemented) so the behaviour
callbacks (e.g., calls to mock_mumble_handler:init/1, start_link/1,
handle_msg/2, get_caps/1, authenticate/2) are available to other modules like
mumble_server_conn.

{ok, #mock_socket{pid = Pid}}.

cleanup(_) -> ok.
messages() -> {ok, {ssl, ssl_closed}}.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

messages/0 returns wrong format — Ranch expects {OK, Closed, Error} 3-tuple of atoms.

ranch_transport:messages/0 should return {ssl, ssl_closed, ssl_error} (a 3-tuple of message tag atoms). The current return {ok, {ssl, ssl_closed}} is a 2-tuple containing a nested tuple, which will cause Ranch to malfunction if it tries to destructure the message tags.

Proposed fix
-messages() -> {ok, {ssl, ssl_closed}}.
+messages() -> {ssl, ssl_closed, ssl_error}.
🤖 Prompt for AI Agents
In `@apps/mumble_protocol/test/mock_transport.erl` at line 156, The messages/0
function returns the wrong tuple shape; update messages/0 to return a flat
3-tuple of atoms as Ranch expects (the message tag triple), e.g., change the
current nested {ok, {ssl, ssl_closed}} to the correct {ssl, ssl_closed,
ssl_error} so that the messages/0 function provides the expected {Tag, Closed,
Error} tuple for ranch_transport.

Comment on lines +1 to +3
-module(prop_protocol_tests).
-include_lib("proper/include/proper.hrl").

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Missing -export directive — properties won't be discovered.

Without exporting prop_varint_roundtrip/0 and prop_tcp_proto_roundtrip/0, the rebar3 proper plugin (and PropEr directly) cannot find or execute them.

Proposed fix
 -module(prop_protocol_tests).
 -include_lib("proper/include/proper.hrl").
+
+-export([prop_varint_roundtrip/0, prop_tcp_proto_roundtrip/0]).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
-module(prop_protocol_tests).
-include_lib("proper/include/proper.hrl").
-module(prop_protocol_tests).
-include_lib("proper/include/proper.hrl").
-export([prop_varint_roundtrip/0, prop_tcp_proto_roundtrip/0]).
🤖 Prompt for AI Agents
In `@apps/mumble_protocol/test/prop_protocol_tests.erl` around lines 1 - 3, The
module prop_protocol_tests is missing an export directive so PropEr can't
discover the properties; add an -export([...]) clause that exports
prop_varint_roundtrip/0 and prop_tcp_proto_roundtrip/0 (e.g.
-export([prop_varint_roundtrip/0, prop_tcp_proto_roundtrip/0]).) Place this
export in the prop_protocol_tests module alongside the existing -module and
-include_lib directives so the test runner can find and execute those property
functions.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🤖 Fix all issues with AI agents
In `@apps/erlmur/src/erlmur_app.erl`:
- Line 30: Logger call in erlmur_app.erl passes a four-element list (PrivDir,
Port, CertFile, KeyFile) but the format string has no specifiers; update the
logger:info call so the format string contains four matching placeholders (e.g.
~p or ~s) and retain the existing argument list so each of PrivDir, Port,
CertFile, KeyFile is interpolated; locate the logger:info invocation in
erlmur_app.erl and replace the current plain string with one that includes four
specifiers.

In `@apps/mumble_protocol/rebar.config`:
- Around line 20-28: Update the ex_doc configuration: in the ex_doc tuple
(ex_doc) change the extras entries under extras from repo-root paths to
app-relative ones ("README.md" and "docs/establishing_connection.md") so ex_doc
resolves correctly for the app, remove the before_closing_body_tag entry if it's
unused (the before_closing_body_tag key), and normalize indentation by replacing
the tab characters on the lines containing with_mermaid and
before_closing_body_tag with spaces for consistent formatting.

In `@apps/mumble_protocol/src/mumble_cert.erl`:
- Around line 101-122: The generate_self_signed_cert function currently builds a
shell string with CertFile, KeyFile, Days, and Subject and calls os:cmd/1,
creating a command-injection risk; fix by replacing the os:cmd usage with an
exec-style call (open_port/2 or erl's port with {spawn_executable, "openssl"}
and {args, [...]}) to pass CertFile, KeyFile, Days and Subject as separate argv
entries (or, if you must keep os:cmd, strictly validate/escape shell
metacharacters for Subject/CertFile/KeyFile and reject unsafe input); update
generate_self_signed_cert (and callers like ensure_auto_certs/2) to either
validate inputs or use open_port with explicit args so no shell interpolation
occurs and then keep the existing file existence checks.

In `@apps/mumble_protocol/src/mumble_server_conn.erl`:
- Around line 191-195: The catch-all clause in function authenticating/3
currently matches every event (including the ?TIMEOUT timer) and returns {stop,
unhandled}; add an explicit clause that matches the timeout event (e.g.,
authenticating(?TIMEOUT, Msg, StateData) or authenticating(timeout, Msg,
StateData) depending on how ?TIMEOUT is delivered) placed before the catch-all
so the timeout case performs a clean shutdown (for example returning {stop,
timeout} or {stop, normal}) and does not fall through to the unhandled
logger/return in authenticating/3.
- Around line 480-490: The update_connection_stats/3 currently adds cumulative
values returned by ocb128_crypto:stats/1 directly into the running Stats
(server_ping), causing double-counting; change it to apply deltas by tracking
the previous crypto stats per-connection (e.g., add a prev_crypto_stats field to
the `#stats` or connection state) and compute Delta = CurrentCryptoStats -
PrevCryptoStats before adding to P#ping, then update PrevCryptoStats to
CurrentCryptoStats; alternatively, clear/reset the crypto counters after reading
if ocb128_crypto supports it, but do not add the raw cumulative CryptoStats to
server_ping in update_connection_stats/3.

In `@apps/mumble_protocol/src/mumble_udp_proto.erl`:
- Around line 34-35: The handler pattern currently captures the 5-bit target
into Timestamp (<<1:3, Timestamp/bits>>) which makes Timestamp 69 bits and
breaks the later v1.5 unpack (<<T:64>> = Timestamp); update the pattern in
handle/2 (and the other similar occurrence the comment mentions) to explicitly
extract the 5-bit target and the 64-bit timestamp (e.g., match <<1:3, Target:5,
T:64, _Rest/bits>> or at minimum <<1:3, Target:5, Timestamp:64>>) so the v1.5
branch can safely do <<T:64>> = Timestamp or use T directly; ensure any other
clause that previously used Timestamp/bits is adjusted consistently to bind
Target and a 64-bit timestamp.

In `@apps/mumble_protocol/src/mumble_udp_server.erl`:
- Around line 141-160: The handler in handle_info/2 is pulling the wrong field
with element(3, User) (which yields username) and passing a binary to
mumble_server_conn:udp_packet/3; change the lookup to use the user record pid
accessor (e.g. User#user.pid) or element(2, User) so Pid is an actual pid,
updating the lists:foreach fun to bind Pid = User#user.pid before calling
mumble_server_conn:udp_packet(Pid, Data, Addr).

In `@apps/mumble_protocol/src/mumble_varint.erl`:
- Around line 44-45: The small-negative varint handling in encode/decode is
off-spec: update encode(Num) to accept Num >= -4 andalso Num < 0 and encode
using the bitwise NOT convention by emitting (-(Num + 1)) into the 2-bit field
(so −1..−4 map to 0xFC..0xFF), and change the corresponding decode branch (the
clause matching the 2-bit prefix currently guarded with I > 0) to remove the I >
0 guard and return -(I + 1) when decoding that 2-bit prefix; adjust the guards
and arithmetic in the encode and decode clauses that reference encode/Num and
the decode variable I accordingly so −1..−4 are encoded/decoded to 0xFC..0xFF
per the Mumble spec.

In `@apps/mumble_protocol/src/mumble.erl`:
- Around line 125-158: The start_server/1 function improperly defaults
ServerHandler to the test module mock_mumble_handler and fails to validate that
both cert_file and key_file are present when auto_create_cert is false; update
the maps:get(server_handler, ...) call to require an explicit handler (returning
an error like {error, missing_server_handler} or raising) instead of defaulting
to mock_mumble_handler, and add validation before calling
mumble_server:start_server/1 to ensure either AutoCreate=true or both CertFile
and KeyFile are not undefined (if one or both are missing return a clear {error,
missing_cert_or_key}), and apply the same handler-default/validation policy to
the other start_server/2 and start_server/3 variants that currently hard-code
the mock.

In `@apps/mumble_protocol/test/mock_transport.erl`:
- Around line 68-70: The accept/2 function currently has its parameters reversed
(accept(_Timeout, ListenSocket)), which violates Ranch's ranch_transport
callback signature accept(Listener, Timeout); update the accept/2 definition to
accept(ListenSocket, Timeout) (and rename internal variables accordingly) so it
matches the expected order and return the same {ok, {mock_accepted,
ListenSocket}} tuple; also search for and adjust any internal or test calls to
accept/2 to use the corrected parameter order to avoid pattern-match failures.

In `@apps/ocb128_crypto/test/prop_ocb128_crypto.erl`:
- Around line 91-95: The anonymous function verifying crypto stats uses the
equality operator =:= which returns a boolean and doesn't fail the test; change
the comparison "(N - 1 - I) =:= Stats#crypto_stats.lost" to a pattern match
using "=" so mismatches raise an error and actually assert the lost value
(update the expression involving N, I and Stats#crypto_stats.lost inside the
fun({{_S, Stats}, I}) -> ... end).
🟠 Major comments (10)
apps/mumble_protocol/test/prop_protocol_tests.erl-4-8 (1)

4-8: ⚠️ Potential issue | 🟠 Major

Constrain the integer() generator to Mumble's supported range.

The unbounded integer() generator can produce values outside varint's 64-bit range, causing encode/1 to fail. Based on the mumble_varint:encode/1 implementation, the valid range is approximately -(2^63) to 2^64 - 1. Use integer(-(1 bsl 63), (1 bsl 64) - 1) to match the encoder's actual capabilities.

apps/mumble_protocol/src/mumble_client_conn.erl-94-109 (1)

94-109: ⚠️ Potential issue | 🟠 Major

ssl:connect called without a timeout — can block indefinitely.

If the remote host is unreachable or firewalled, ssl:connect/3 (which defaults to infinity timeout) will block the gen_statem init path forever. Specify an explicit timeout (e.g., from Opts or a default like 10 seconds).

🐛 Proposed fix
-    case ssl:connect(Host, Port, SslOptsList) of
+    Timeout = maps:get(connect_timeout, Opts, 10000),
+    case ssl:connect(Host, Port, SslOptsList, Timeout) of
apps/erlmur/src/erlmur_user_manager.erl-143-152 (1)

143-152: ⚠️ Potential issue | 🟠 Major

Stale UDP mapping when a user's address changes.

When register_udp_addr is called for a session that already has a udp_addr, the old address is not removed from udp_map. The old {OldAddr, Pid} entry remains, meaning get_session_by_udp could return a stale mapping for the old address.

🐛 Proposed fix — remove old mapping before inserting new one
 handle_cast({register_udp_addr, SessionId, Addr}, State = `#state`{users = Users, udp_map = UdpMap}) ->
     case ets:lookup(Users, SessionId) of
-        [User] ->
+        [User = `#user`{udp_addr = OldAddr}] ->
+            case OldAddr of
+                undefined -> ok;
+                _ -> ets:delete(UdpMap, OldAddr)
+            end,
             ets:insert(Users, User#user{udp_addr = Addr}),
             ets:insert(UdpMap, {Addr, User#user.pid}),
             logger:debug("Registered UDP addr ~p for session ~p", [Addr, SessionId]);
apps/mumble_protocol/README.md-87-88 (1)

87-88: ⚠️ Potential issue | 🟠 Major

Fix README example to match the actual start_link/3 API signature.

The README line 95 example is incompatible with the module's API. The function expects:

  • First argument: a string(), not a tuple
  • Third argument: a map(), not a proplist

Correct the example from:

{ok, ClientPid} = mumble_client_conn:start_link({127, 0, 0, 1}, 64738, Opts).

to:

Opts = #{certfile => "client_cert.pem", keyfile => "client_key.pem"},
{ok, ClientPid} = mumble_client_conn:start_link("127.0.0.1", 64738, Opts).

The README's arity reference to start_link/3 is correct; only the argument types and format are wrong.

apps/mumble_protocol/test/mumble_client_api_tests.erl-7-11 (1)

7-11: ⚠️ Potential issue | 🟠 Major

Only 2 of 7 test functions are included in the test generator — the rest are dead code.

client_start_no_certs, client_send_message, client_send_voice, client_get_state, and client_stop are defined but never referenced in mumble_client_api_test_/0, so EUnit will not execute them. Additionally, setup/0 and cleanup/1 are not wired into any EUnit fixture ({setup, ...} or {foreach, ...}).

If these tests require a running server (which they seem to, since they call mumble:start_client with "localhost"), wrap them in a {setup, fun setup/0, fun cleanup/1, [...]} fixture. Otherwise, add them to the test list.

Proposed fix — include all tests
 mumble_client_api_test_() ->
-    [
-        {"client start with invalid certs", fun client_start_invalid_certs/0},
-        {"client invalid ref handling", fun client_invalid_ref/0}
-    ].
+    [
+        {"client start with invalid certs", fun client_start_invalid_certs/0},
+        {"client invalid ref handling", fun client_invalid_ref/0},
+        {"client start no certs", fun client_start_no_certs/0},
+        {"client send message", fun client_send_message/0},
+        {"client send voice", fun client_send_voice/0},
+        {"client get state", fun client_get_state/0},
+        {"client stop", fun client_stop/0}
+    ].
apps/mumble_protocol/src/mumble_server_sup.erl-71-79 (1)

71-79: ⚠️ Potential issue | 🟠 Major

Replace custom MFA wrapper with ranch:child_spec/5 for proper listener embedding.

The start_tcp_listener/5 function directly calls ranch:start_listener/5, which starts the listener under Ranch's supervision tree (not under this supervisor). Using it as a child spec MFA creates an improper relationship:

  • The supervisor receives {ok, Pid} from the MFA and links to it, but Ranch owns the process via ranch_sup.
  • If Ranch restarts the listener, this supervisor doesn't see the restart.
  • If both try to restart simultaneously, the second attempt may fail because the listener name is already registered.

Use ranch:child_spec/5 instead to generate a proper child spec that embeds the listener directly in your supervision tree:

RanchListener = ranch:child_spec(
    mumble_tcp_listener,
    ranch_ssl,
    #{
        socket_opts => [...],
        num_acceptors => 10
    },
    mumble_server_conn,
    [Handler]
)

Then add RanchListener directly to the children list. This gives your supervisor true ownership and proper restart semantics.

apps/mumble_protocol/src/mumble_server.erl-34-46 (1)

34-46: ⚠️ Potential issue | 🟠 Major

Production default should not be a test mock module.

Line 38 defaults server_handler to mock_mumble_handler. This means calling start_server(#{}) in production uses a test mock. This should either have no default (requiring the caller to provide it) or default to a real handler module.

Suggested fix
-    ServerHandler = maps:get(server_handler, Options, mock_mumble_handler),
+    ServerHandler = maps:get(server_handler, Options),
apps/mumble_protocol/src/mumble_tcp_proto.erl-44-61 (1)

44-61: ⚠️ Potential issue | 🟠 Major

MESSAGE_TABLE is missing ten message types supported by mumble_msg.

mumble_msg.erl supports Reject, PermissionDenied, ACL, QueryUsers, ContextActionModify, ContextAction, VoiceTarget, SuggestConfig, PluginDataTransmission, and RequestBlob, but these are absent from MESSAGE_TABLE. Calling pack/1 for any of these will raise error({unknown_type, ...}), and incoming messages of these types will be silently skipped during unpack (with an error log).

apps/mumble_protocol/test/mumble_SUITE.erl-286-302 (1)

286-302: ⚠️ Potential issue | 🟠 Major

Multiple tests are stubs that don't test their named behavior.

client_connect_disconnect_test, client_with_callback_test, client_server_communication_test, and multiple_clients_test all only start and stop a server — they never create a client, exercise callbacks, or test multi-client scenarios. As written, these duplicate server_start_stop_test and give false confidence in test coverage.

Consider either implementing the actual test logic or marking them as {skip, "Not yet implemented"} to make their status explicit.

Also applies to: 305-320, 456-471, 474-489

apps/mumble_protocol/test/mock_transport.erl-176-185 (1)

176-185: ⚠️ Potential issue | 🟠 Major

Blocking receive inside handle_call stalls the gen_server.

When the message queue is empty (line 178), the handle_call for recv enters a raw receive block that suspends the entire gen_server process. While blocked, no other handle_call, handle_cast, or handle_info can be processed — including the {inject_message, Data} cast that would unblock the receive. This creates a deadlock: recv waits for data that can only arrive via a cast the server can't process.

For a test mock this may not surface if recv is never called on an empty queue, but it's a latent deadlock.

Proposed fix — reply asynchronously or queue the caller

One approach: store the pending caller and reply from handle_cast when data arrives.

-handle_call({recv, _Length, _Timeout}, _From, State) ->
-    %% Wait for a message to be injected
-    receive
-        {inject_message, Data} ->
-            {reply, {ok, Data}, State}
-    after Timeout ->
-        {reply, {error, timeout}, State}
-    end;
+handle_call({recv, _Length, Timeout}, From, State) ->
+    %% No data available — park the caller and reply later
+    TRef = case Timeout of
+        infinity -> undefined;
+        T -> erlang:send_after(T, self(), {recv_timeout, From})
+    end,
+    {noreply, State#{pending_recv => {From, TRef}}};

Then in handle_cast({inject_message, ...}), check for pending_recv and reply to the parked caller.

🟡 Minor comments (22)
justfile-41-41 (1)

41-41: ⚠️ Potential issue | 🟡 Minor

Overly broad doc directory deletion may remove source documentation.

find ... -name "doc" will match any directory named doc in the entire project tree, including potential source documentation directories under apps/*/doc. Consider scoping this to the known output location (e.g., rm -rf {{justfile_directory()}}/doc) or excluding apps/ paths.

justfile-44-45 (1)

44-45: ⚠️ Potential issue | 🟡 Minor

Broadened glob patterns could delete user-provided certificates.

Switching from specific filenames (cert.pem, key.pem) to *.pem / *.key means any PEM or key file anywhere in the tree will be removed, including manually placed or third-party certificates. Consider limiting the search to specific directories or using more specific filename patterns.

apps/mumble_protocol/src/mumble_cert.erl-72-84 (1)

72-84: ⚠️ Potential issue | 🟡 Minor

code:priv_dir/1 will fail if the application isn't loaded.

If mumble_protocol isn't yet loaded/started when ensure_auto_certs/2 is called, code:priv_dir(mumble_protocol) returns {error, bad_name} (not a string), which will crash downstream at filename:join/2. Consider handling this error case.

AGENTS.md-7-7 (1)

7-7: ⚠️ Potential issue | 🟡 Minor

just dev target doesn't exist — should be just shell.

The justfile defines a shell target, not dev. This will confuse developers following the guide.

Proposed fix
-just dev       # Start development shell
+just shell     # Start development shell
AGENTS.md-65-65 (1)

65-65: ⚠️ Potential issue | 🟡 Minor

Incorrect Erlang syntax: try...of...else doesn't exist.

Erlang's exception handling is try Expr of Pattern -> Body catch Class:Reason -> Handler end. There is no else clause. This could mislead developers.

Proposed fix
-- Use `try...of...else` for exceptions that need cleanup
+- Use `try...of...catch...after...end` for exceptions that need cleanup
apps/mumble_protocol/src/mumble_cert.erl-127-133 (1)

127-133: ⚠️ Potential issue | 🟡 Minor

file:make_dir/1 is not recursive — nested directories will fail.

If the parent of the certificate directory doesn't exist, file:make_dir/1 will return {error, enoent}. Consider using filelib:ensure_dir/1 (which creates all parent directories for a file path) or filelib:ensure_path/1 (OTP 25+) for the directory itself.

Proposed fix
 ensure_cert_dir(CertFile) ->
-    Dir = filename:dirname(CertFile),
-    case file:make_dir(Dir) of
-        ok -> ok;
-        {error, eexist} -> ok;
-        {error, Reason} -> {error, {dir_creation_failed, Reason}}
-    end.
+    case filelib:ensure_dir(CertFile) of
+        ok -> ok;
+        {error, Reason} -> {error, {dir_creation_failed, Reason}}
+    end.
rebar.config-2-2 (1)

2-2: ⚠️ Potential issue | 🟡 Minor

Remove hardcoded profile name from GPB include path.

The path _build/default/plugins/gpb/include explicitly references the default profile. If the project is built with a different profile (e.g., test), this path becomes invalid. While the test profile currently overrides erl_opts entirely and doesn't use this path, the GPB-generated headers are already available locally at apps/mumble_protocol/include, making the hardcoded path potentially redundant. For correctness and to avoid future issues, consider either removing this path if the local include is sufficient, or using a profile-independent approach.

apps/mumble_protocol/src/mumble_udp_proto.erl-9-21 (1)

9-21: ⚠️ Potential issue | 🟡 Minor

Documentation contradicts itself on message types.

Line 12 says "0=Ping, 1=Voice" but lines 19–20 say "Type 1: Ping" and "Type 0, 2-7: Voice". The code treats Type 1 as Ping (line 34), which aligns with the Mumble protocol spec.

Fix the doc
-- **Type (3 bits)**: Message type (0=Ping, 1=Voice, etc.)
+- **Type (3 bits)**: Message type (1=Ping, 0/2-7=Voice codecs)
apps/mumble_protocol/src/mumble_client_conn.erl-130-134 (1)

130-134: ⚠️ Potential issue | 🟡 Minor

Stats field names are misleading — tracking received data but naming it from_client_*.

In a client module, from_client_tcp_packets and from_client_tcp_bytes are incremented when data is received from the server. The naming suggests these track outbound client traffic. Consider using field names that reflect the direction correctly (e.g., received_tcp_packets), or check whether the #stats{} record fields are designed for the server's perspective.

Also applies to: 154-159

apps/mumble_protocol/src/mumble_client_conn.erl-217-224 (1)

217-224: ⚠️ Potential issue | 🟡 Minor

UDP send functions are completely stubbed — udp_verified can never become true.

send_udp_ping/1 and send_udp/2 are no-ops (just log). No UDP socket is created anywhere in this module, so the established(info, {udp, ...}, ...) handler on line 185 is unreachable, and udp_verified will never become true. The voice-over-UDP path in do_send_voice is dead code.

Consider either implementing the UDP socket lifecycle or removing the dead branches to avoid confusion.

Do you want me to open an issue to track UDP implementation for the client connection?

apps/erlmur/src/erlmur_server_handler.erl-78-86 (1)

78-86: ⚠️ Potential issue | 🟡 Minor

erlang:system_info(otp_release) returns a charlist, not a binary.

The other values in the caps map (release, os) are binaries. otp_release returns a string like "27", which may cause encoding failures if downstream code expects all values to be binaries.

🐛 Proposed fix
     #{
         major => 1,
         minor => 2,
         patch => 4,
         release => <<"Erlmur MVP">>,
         os => <<"Erlang/OTP">>,
-        os_version => erlang:system_info(otp_release)
+        os_version => list_to_binary(erlang:system_info(otp_release))
     }.
apps/mumble_protocol/test/mumble_cert_tests.erl-114-117 (1)

114-117: ⚠️ Potential issue | 🟡 Minor

Overly weak fallback assertion — "test" will match almost anything.

The fallback string:find(Subject, "test") =/= nomatch is too broad; it would match the word "test" in any context (e.g., directory names, other fields). This makes the assertion nearly vacuous when the primary check fails.

🐛 Proposed fix — tighten the assertion
         ?assert(
-            (string:find(Subject, "test.example.com") =/= nomatch) orelse
-            (string:find(Subject, "test") =/= nomatch)
+            (string:find(Subject, "test.example.com") =/= nomatch) orelse
+            (string:find(Subject, "CN = test.example.com") =/= nomatch)
         )
apps/mumble_protocol/test/mumble_cert_tests.erl-138-158 (1)

138-158: ⚠️ Potential issue | 🟡 Minor

reuse_existing_cert_test doesn't actually test reuse — it only asserts the file is unchanged during a sleep.

The test generates a cert, reads its mtime, sleeps 100ms, then reads mtime again. It never calls generate_cert/2 a second time, so it doesn't verify that an existing certificate is reused rather than regenerated. The test will always pass trivially.

To test reuse, call generate_cert(CertFile, KeyFile) again after the sleep and then assert that the mtime is unchanged.

🐛 Proposed fix
         %% First generation
         generate_cert(CertFile, KeyFile),
         {ok, FileInfo1} = file:read_file_info(CertFile),
         MTime1 = FileInfo1#file_info.mtime,
         
         timer:sleep(1100),
         
-        %% Check files still exist
+        %% Second generation — should reuse existing cert
+        generate_cert(CertFile, KeyFile),
         {ok, FileInfo2} = file:read_file_info(CertFile),
         MTime2 = FileInfo2#file_info.mtime,
         
         ?assertEqual(MTime1, MTime2)

Note: The local generate_cert/2 helper always overwrites the file unconditionally (it just calls openssl req). If the intent is to test reuse logic, this test should call mumble_cert:generate_self_signed_cert/4 or mumble_cert:ensure_auto_certs/2 instead, which presumably has the skip-if-exists logic.

apps/erlmur/src/erlmur_user_manager.erl-81-86 (1)

81-86: ⚠️ Potential issue | 🟡 Minor

Add wrapping or validation for next_session_id to respect Mumble's uint32 constraint.

The Mumble protocol requires session IDs to be uint32 (max value 0xFFFFFFFF or 4,294,967,295). Currently, next_session_id grows unboundedly as a pos_integer() without any wrapping or overflow guards. After approximately 4.3 billion registrations, the value will exceed the valid uint32 range, potentially causing serialization failures or collisions when clients receive out-of-range session IDs.

Implement either:

  • Wrap-around logic: (SessionId rem 16#FFFFFFFF) + 1 (ensuring non-zero IDs), or
  • Guard clause with server restart/reset, or
  • Modulo arithmetic to keep values within uint32 bounds
apps/mumble_protocol/README.md-31-31 (1)

31-31: ⚠️ Potential issue | 🟡 Minor

Fix spelling errors.

  • Line 31: "recieved" → "received"
  • Line 37: "reciveing" → "receiving"
  • Line 45: "explicitely" → "explicitly"
📝 Proposed fix
-If no `Ping` is recieved for 30sec the client will be disconnected from the server.
+If no `Ping` is received for 30 seconds, the client will be disconnected from the server.
-If reciveing a `UDPTunnel` message the connection is marked "UDP verified" false, until new UDP verification is done.
+If receiving a `UDPTunnel` message, the connection is marked "UDP verified" false, until new UDP verification is done.
-- **Automatic Fallback**: If a server hasn't received a UDP packet from a client recently (or if the client explicitely
+- **Automatic Fallback**: If a server hasn't received a UDP packet from a client recently (or if the client explicitly

Also applies to: 37-37, 45-45

apps/mumble_protocol/docs/establishing_connection.md-79-81 (1)

79-81: ⚠️ Potential issue | 🟡 Minor

Minor: CodecVersion ordering differs between prose and diagram.

In the prose (sections 4.x), CodecVersion is described after UserState (line 79–81). In the Mermaid diagram (line 116), it appears before ChannelState/UserState under "Cryptographic & Config Setup." If the server can send these in any order, consider adding a note; otherwise, align the two for consistency.

Also applies to: 112-130

apps/erlmur/src/erlmur_sup.erl-23-32 (1)

23-32: ⚠️ Potential issue | 🟡 Minor

Log on line 31 is misleading — user manager hasn't started yet.

init/1 only returns the child specification; the supervisor starts children after init/1 returns. The log "User manager started" fires before the child process actually starts. Consider removing it or changing to "Supervisor initialized with user_manager child spec".

apps/mumble_protocol/test/mock_mumble_handler.erl-4-6 (1)

4-6: ⚠️ Potential issue | 🟡 Minor

start_link/1 returns {ok, self()} — does not start a new process.

This returns the caller's pid rather than spawning and linking a new process. For a mock this may be acceptable if callers don't rely on the returned pid being a distinct, linked process. If any caller monitors or links to this pid expecting independent lifecycle management, it will behave incorrectly.

apps/mumble_protocol/src/mumble_server_sup.erl-61-61 (1)

61-61: ⚠️ Potential issue | 🟡 Minor

Default handler is mock_mumble_handler — risky for production.

If server_handler is accidentally omitted from the options, production will silently use the mock handler. Consider requiring this option (crash if absent) or defaulting to a production handler.

Proposed fix — require the option
-    ServerHandler = proplists:get_value(server_handler, Options, mock_mumble_handler),
+    ServerHandler = case proplists:get_value(server_handler, Options) of
+        undefined -> error({missing_required_option, server_handler});
+        H -> H
+    end,
apps/mumble_protocol/src/mumble_udp_server.erl-38-40 (1)

38-40: ⚠️ Potential issue | 🟡 Minor

send/3 spec promises {error, term()} but gen_server:cast always returns ok.

The caller never sees an error — any send failure is logged inside handle_cast but swallowed. The spec should be ok only, or switch to gen_server:call if the caller needs error feedback.

apps/mumble_protocol/test/mock_transport.erl-190-202 (1)

190-202: ⚠️ Potential issue | 🟡 Minor

setopts active-mode simulation sends unexpected messages.

When {active, true} is set, line 194 sends {ssl_passive, self()} followed by {ssl, self(), <<>>}. Real SSL sockets don't emit ssl_passive when activating active mode, and sending an empty binary <<>> has no analog in real behavior. This could confuse tests that assert on message patterns. The ssl_passive message is only sent by real SSL when the active-N counter reaches zero.

apps/mumble_protocol/test/mumble_SUITE.erl-77-82 (1)

77-82: ⚠️ Potential issue | 🟡 Minor

OpenSSL availability check is not portable.

which is not available on Windows and may behave differently across Unix variants. Additionally, os:cmd/1 returns a non-empty error string (e.g., "which: no openssl...") rather than [] on some systems when the command is not found.

Consider using os:find_executable("openssl") which returns false when the binary isn't found, and works across platforms.

Proposed fix
-    case os:cmd("which openssl") of
-        [] -> 
-            ct:fail(openssl_not_available);
-        _ -> 
-            ok
-    end,
+    case os:find_executable("openssl") of
+        false ->
+            ct:fail(openssl_not_available);
+        _ ->
+            ok
+    end,
🧹 Nitpick comments (34)
AGENTS.md (1)

20-93: Markdown lint: missing blank lines around headings and fenced code blocks.

Multiple headings (Lines 20, 25, 31, 37, 45, 57, 63, 69, 76, 82, 88) and code fences (Lines 46, 99, 106) lack required surrounding blank lines per MD022/MD031. Adding blank lines before each ### heading and around fenced code blocks would fix these.

rebar.config (1)

34-35: All dependencies are pinned to branch heads — builds are not reproducible.

ranch, eqwalizer_support, meck, and proper all reference {branch, "master"} or {branch, "main"}. A breaking upstream commit will silently break your build. Consider pinning to a specific tag or commit SHA. The rebar.lock file provides some protection, but only if it's consistently committed and not regenerated carelessly.

Also applies to: 40-45

apps/mumble_protocol/src/mumble_varint.erl (1)

1-59: No catch-all / error clause for malformed input in decode/1.

If decode/1 is called with a binary that doesn't match any clause (e.g., truncated data), the caller gets an uninformative function_clause crash. Consider adding a final catch-all clause that returns a tagged error for better diagnostics, especially since this processes network input.

apps/mumble_protocol/src/mumble_client.erl (2)

43-62: Consider guarding against mismatched cert/key arguments.

When only one of CertFile / KeyFile is undefined and the other is not, this falls into the {CF, KF} branch and relies on mumble_cert:validate_cert_files/2 to reject it. An explicit clause or early guard would make the contract clearer and give a better error message at this layer.

Example
     case {CertFile, KeyFile} of
         {undefined, undefined} ->
             start_client_impl(Host, Port, Options);
+        {undefined, _} ->
+            {error, {missing_cert_file, "cert_file required when key_file is provided"}};
+        {_, undefined} ->
+            {error, {missing_key_file, "key_file required when cert_file is provided"}};
         {CF, KF} ->

131-141: start_link couples the client lifetime to the caller.

mumble_client_conn:start_link/3 links the client to the calling process. If the caller exits (e.g., a short-lived shell or test process), the client dies silently. If this is intentional supervision, it's fine; otherwise consider start/3 or start_monitor/3 for callers that aren't supervisors.

apps/mumble_protocol/src/mumble_udp_proto.erl (1)

52-56: split_voice_positional_speex_celt/1 crashes on truncated input.

The function only matches <<Cont:1, Len:7, V:Len/binary, Rest/binary>>. If the binary is truncated mid-frame (common with UDP), this produces an uninformative function_clause error instead of a graceful failure.

apps/mumble_protocol/rebar.config (1)

26-27: Inconsistent indentation: tabs on lines 26–27, spaces elsewhere.

Lines 26–27 use tab indentation while the rest of the file uses spaces. Minor formatting nit.

apps/mumble_protocol/test/mumble_test_callback.erl (1)

6-14: messages field in state is initialized but never updated.

init/1 sets messages => [] (line 8) but handle_msg/2 never appends to it — messages are forwarded to the parent pid instead. If accumulation isn't needed, consider dropping the field to avoid confusion.

apps/mumble_protocol/src/mumble_version.erl (1)

36-46: decode/1 auto-detects V1 vs V2 by value range — fragile for small versions.

The threshold V1 =< 16#FFFFFFFF vs V2 > 16#FFFFFFFF works for real-world Mumble versions (≥ 1.0.0), but a V2-encoded version like 0.0.1 (0x0000000000010000 = 65536) falls into the V1 branch and decodes as 0.1.0. Consider either:

  • Documenting the assumption that Major ≥ 1 for V2, or
  • Providing separate decode_v1/1 / decode_v2/1 for callers that know the source field format.
apps/mumble_protocol/test/mumble_cert_tests.erl (2)

56-68: Tests exercise local OpenSSL helpers instead of the mumble_cert module under test.

All tests call the local generate_cert/2,4 helpers (which shell out to openssl req), but the AI summary indicates the production module mumble_cert exposes validate_cert_files/2, ensure_auto_certs/2, and generate_self_signed_cert/4. These tests verify that OpenSSL works, not that the application's certificate management logic is correct.

Consider adding tests that exercise the actual mumble_cert API to ensure coverage of validation, auto-generation, and edge-case handling in the production code.

Also applies to: 70-83, 85-102, 104-120, 122-136, 160-182, 184-201


48-50: which openssl is not portable to all platforms.

which is not POSIX-mandated and may not exist on some systems (e.g., certain minimal containers). Consider using os:find_executable("openssl") which is Erlang-native and cross-platform.

♻️ Proposed fix
 openssl_available_test() ->
-    Result = os:cmd("which openssl"),
-    ?assert(length(Result) > 0).
+    ?assertNotEqual(false, os:find_executable("openssl")).
apps/erlmur/src/erlmur_user_manager.erl (2)

157-181: handle_info DOWN handler performs O(n) full-table scan to find user by PID.

ets:foldl iterates the entire users table to find the user matching the downed PID. This is acceptable for an MVP with a small user count, but consider adding a PID-indexed secondary ETS table (or ets:match/2 which at least stops internally) if you expect scaling.


40-50: The #user{} record type is opaque to callers outside this module.

get_user/1 returns {ok, #user{}} in its spec, but since the record is defined locally (not in a header), callers cannot pattern-match on the result. Consider either moving the record to a shared header, exporting a map representation, or defining accessor functions.

apps/erlmur/src/erlmur_server_handler.erl (2)

61-68: All permissions granted unconditionally — flag for hardening before production.

16#FFFFFFFF grants every permission to every user. This is acknowledged as MVP, but should be tracked as a TODO to implement proper permission checking.

Do you want me to open an issue to track implementing proper permission management?


26-34: No validation or rate-limiting on authentication — all users accepted.

authenticate/2 unconditionally accepts every Authenticate message. While the %% MVP comment acknowledges this, there's no guard against resource exhaustion (e.g., a client repeatedly connecting and registering). Consider adding at minimum a TODO comment to track this for hardening.

apps/mumble_protocol/src/mumble_client_conn.erl (1)

192-195: UDP ping timer starts immediately on entering established with a 1-second delay, firing every 5 seconds thereafter.

The first timer fires after 1 second (line 194), but subsequent ones fire every 5 seconds (line 183). Since send_udp_ping is a no-op stub, this timer just creates recurring log noise. If UDP is not yet implemented, consider deferring the timer start until UDP support is in place.

apps/ocb128_crypto/src/ocb128_crypto.erl (1)

423-436: repeat and drop classifications don't update stats.

When a packet is classified as repeat or drop, the state is returned without incrementing any stats counter. Consider tracking these events (e.g., adding repeat and drop fields to crypto_stats) for observability — this data is valuable for diagnosing network issues.

apps/mumble_protocol/test/client_logic_tests.erl (4)

312-317: Placeholder tests provide false confidence.

established_transition_test, client_close_test, and invalid_msg_test all just assert true. They pass unconditionally and give the appearance of coverage without testing anything. Consider either implementing them with mock transports or removing them and tracking the gaps with TODO comments / issues.

Would you like me to open an issue to track these placeholder tests for future implementation?

Also applies to: 344-350, 425-430


407-415: network_error_test doesn't test error handling.

Iterating over a list of atoms and asserting is_atom/1 is tautologically true and verifies nothing about how the client handles these errors.


417-423: ssl_close_test only reconstructs and matches a tuple it just created.

This does not exercise the client's handle_common handler for {ssl_closed, _} messages.


328-342: established_receive_test tests Erlang mailbox mechanics, not client logic.

Sending {mumble_msg, Msg} to self() and receiving it verifies message passing, not that mumble_client_conn actually forwards messages to the parent in the established state.

apps/mumble_protocol/test/protocol_varint_tests.erl (1)

8-12: Negative varint tests will break the test suite if decode truly lacks negative support.

The comment on line 10 acknowledges that decode may not handle negatives, yet the tests use ?_assertEqual which will produce failures (not skips). If this is intentional to track a known gap, consider guarding with a try/catch and using ?debugMsg to document the skip, or exclude these from the default test suite until the fix lands. Otherwise every CI run will report failures for a known limitation.

apps/erlmur/src/erlmur_app.erl (1)

18-21: list_to_atom/1 on environment variable input is an atom table pollution risk.

list_to_atom(LevelStr) on an arbitrary env var value will create atoms that are never garbage-collected. Consider using list_to_existing_atom/1 with a fallback, or validate against a known set of log levels.

Proposed fix
-        LevelStr -> list_to_atom(LevelStr)
+        LevelStr ->
+            try list_to_existing_atom(LevelStr)
+            catch error:badarg ->
+                logger:warning("[erlmur] Invalid log level '~s', defaulting to info", [LevelStr]),
+                info
+            end
apps/mumble_protocol/src/mumble_msg.erl (2)

38-42: No catch-all clause for unknown message types — function_clause crash.

Both raw_to_map/1 and from_map/1 lack a fallback clause. If an unrecognized record or a map with an unknown message_type is passed, this will crash with function_clause. Consider adding a catch-all that returns {error, {unknown_message, ...}} or at least a clear error.

Suggested fallback for from_map
 from_map(#{message_type := 'PluginDataTransmission'} = Map) ->
     #'PluginDataTransmission'{
         senderSession = maps:get(senderSession, Map, undefined),
         receiverSessions = maps:get(receiverSessions, Map, []),
         data = maps:get(data, Map, undefined),
         dataID = maps:get(dataID, Map, undefined)
-    }.
+    };
+from_map(#{message_type := Type}) ->
+    error({unknown_message_type, Type}).

519-532: Side-effectful default for timestamp in Ping conversion.

Line 521 uses erlang:system_time(second) as a default value. This means calling from_map(#{message_type => 'Ping'}) silently inserts the current time, making this conversion function impure. If this is intentional, a comment would help; otherwise, undefined may be a safer default to let the caller decide.

apps/mumble_protocol/src/mumble_tcp_proto.erl (1)

137-144: find_msg_by_tag doesn't short-circuit on match.

lists:foldl continues scanning the entire list even after finding the matching tag. Consider using lists:keyfind/3 or lists:search/2 for an early exit, consistent with how find_msg_by_record/1 already uses lists:search/2.

Suggested refactor
 find_msg_by_tag(Tag) ->
-    lists:foldl(fun ({T, Record}, _Acc) when T =:= Tag ->
-                        {ok, Tag, Record};
-                    (_, Acc) ->
-                        Acc
-                end,
-                error,
-                ?MESSAGE_TABLE).
+    case lists:keyfind(Tag, 1, ?MESSAGE_TABLE) of
+        {Tag, Record} -> {ok, Tag, Record};
+        false -> error
+    end.
apps/mumble_protocol/src/mumble_server_conn.erl (1)

447-457: notice-level logging on every sent message is too verbose for production.

Line 450 logs at notice level for every message sent. In a busy server, this will flood logs. Consider debug level.

Suggested fix
-            logger:notice("Sending ~p (~p bytes)", [maps:get(message_type, Map), byte_size(Bin)]),
+            logger:debug("Sending ~p (~p bytes)", [maps:get(message_type, Map), byte_size(Bin)]),
apps/mumble_protocol/test/test_utils.erl (1)

68-74: wait_for_message/2 only supports exact-value matching, not pattern matching.

Since Pattern is a bound variable, receive Pattern = Msg -> matches only messages exactly equal to Pattern. It cannot match partial patterns (e.g., tuples with wildcards). If flexible matching is needed, consider a predicate-based approach: wait_for_message(fun(Msg) -> ... end, Timeout).

This may be sufficient for current test usage, but worth noting as a limitation.

apps/mumble_protocol/test/mumble_SUITE.erl (4)

93-96: Fragile error detection for certificate generation.

Matching only "Error" ++ _ won't catch OpenSSL failures that begin with other prefixes (e.g., "Can't", "unable to", "problems making", etc.). Relying on filelib:is_file checks on lines 99–100 is a more reliable indicator. Consider checking the exit status or just relying on the file-existence assertions below.


237-240: Brittle positional access into opaque server state.

element(3, State) will silently break if the #state{} record in mumble_udp_server is reordered or extended. Prefer using sys:get_state/1 with a record pattern, or expose a dedicated API (e.g., mumble_udp_server:get_port/1) to query the port.


518-576: Dead helper functions: get_server_port/1, get_ranch_port/1, find_port_in_listeners/1, and wait_for_client_connection/2 are unused.

These helpers are never called from any test case. get_server_port/1 always returns a hardcoded 64738, and wait_for_client_connection/2 is a no-op placeholder. They add maintenance burden without providing value.

Either remove them or, if they're planned for future tests, annotate them clearly (e.g., with a TODO and suppress unused-function warnings).


380-413: auto_cert_reuse_test implicitly depends on auto_cert_basic_test having created the cert files.

If auto_cert_basic_test fails or is skipped, this test will crash at line 387 with {error, enoent} instead of a clear skip/failure. While the [sequence] group property will stop after a failure, the error message won't be obvious. Consider adding a guard:

case file:read_file_info(CertFile) of
    {ok, FileInfo1} -> ...;
    {error, _} -> ct:fail(prerequisite_auto_cert_basic_test_did_not_run)
end
apps/mumble_protocol/test/mock_transport.erl (2)

87-88: recv/3 passes Timeout as the gen_server:call timeout, but the server may block internally for the same duration.

If Timeout is finite, both the gen_server:call and the internal receive (line 180) race on the same deadline. The gen_server:call timeout could fire first, leaving an orphaned reply in the gen_server's mailbox. Consider using a slightly larger call timeout or infinity for the call and letting only the server-side timeout govern.


40-42: start_link/0 starts an unregistered gen_server — ownership tracking is absent.

The init/1 sets owner => undefined, and owner is only set via controlling_process. If controlling_process is never called (which is the case for sockets created in handshake/2, connect/4, and mock_handshake/1), the owner stays undefined and active-mode notifications are silently dropped. This may cause tests to silently pass when they shouldn't receive data.

Also applies to: 167-174

Port = application:get_env(erlmur, listen_port, 64738),
CertFile = application:get_env(erlmur, cert_pem, filename:join(PrivDir, "server.pem")),
KeyFile = application:get_env(erlmur, key_pem, filename:join(PrivDir, "server.key")),
logger:info("PrivDir Port CertFile KeyFile",[PrivDir, Port,CertFile,KeyFile]),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Logger format string has no placeholders — will crash or produce a logger error at runtime.

The format string "PrivDir Port CertFile KeyFile" contains no ~p/~s specifiers, but a 4-element list is passed as the second argument. This will cause a logger formatting error.

Proposed fix
-		logger:info("PrivDir Port CertFile KeyFile",[PrivDir, Port,CertFile,KeyFile]),
+		logger:info("PrivDir: ~p Port: ~p CertFile: ~p KeyFile: ~p", [PrivDir, Port, CertFile, KeyFile]),
🤖 Prompt for AI Agents
In `@apps/erlmur/src/erlmur_app.erl` at line 30, Logger call in erlmur_app.erl
passes a four-element list (PrivDir, Port, CertFile, KeyFile) but the format
string has no specifiers; update the logger:info call so the format string
contains four matching placeholders (e.g. ~p or ~s) and retain the existing
argument list so each of PrivDir, Port, CertFile, KeyFile is interpolated;
locate the logger:info invocation in erlmur_app.erl and replace the current
plain string with one that includes four specifiers.

Comment on lines 101 to 109
generate_self_signed_cert(CertFile, KeyFile, Subject, Days) ->
%% Ensure directory exists (ignore eexist error)
case ensure_cert_dir(CertFile) of
ok ->
Cmd = io_lib:format(
"openssl req -x509 -newkey rsa:2048 -keyout ~s -out ~s -days ~B -nodes -subj '~s' 2>&1",
[KeyFile, CertFile, Days, Subject]
),

_Output = os:cmd(Cmd),
%% Check if files were created (OpenSSL outputs progress to stderr)
case {filelib:is_file(CertFile), filelib:is_file(KeyFile)} of
{true, true} ->
{ok, CertFile, KeyFile};
{false, _} ->
{error, {cert_generation_failed, "Cert file not created"}};
{_, false} ->
{error, {cert_generation_failed, "Key file not created"}}
end;
{error, Reason} ->
{error, Reason}
end.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Command injection vulnerability via unsanitized shell arguments.

Subject, KeyFile, and CertFile are interpolated directly into a shell command string passed to os:cmd/1. If any parameter contains shell metacharacters (e.g., a Subject like "/CN=test; rm -rf /"), arbitrary commands can be executed. This is especially concerning since Subject originates from caller input in ensure_auto_certs/2.

Consider either:

  1. Sanitizing/validating all inputs before interpolation (reject or escape shell-special characters), or
  2. Using Erlang's open_port/2 with {args, [...]} to avoid shell interpretation entirely.
Sketch using open_port to avoid shell injection
-            Cmd = io_lib:format(
-                "openssl req -x509 -newkey rsa:2048 -keyout ~s -out ~s -days ~B -nodes -subj '~s' 2>&1",
-                [KeyFile, CertFile, Days, Subject]
-            ),
-
-            _Output = os:cmd(Cmd),
+            DaysStr = integer_to_list(Days),
+            Args = ["req", "-x509", "-newkey", "rsa:2048",
+                    "-keyout", KeyFile, "-out", CertFile,
+                    "-days", DaysStr, "-nodes", "-subj", Subject],
+            Port = open_port({spawn_executable, os:find_executable("openssl")},
+                            [{args, Args}, exit_status, stderr_to_stdout]),
+            collect_port_output(Port),
🤖 Prompt for AI Agents
In `@apps/mumble_protocol/src/mumble_cert.erl` around lines 101 - 122, The
generate_self_signed_cert function currently builds a shell string with
CertFile, KeyFile, Days, and Subject and calls os:cmd/1, creating a
command-injection risk; fix by replacing the os:cmd usage with an exec-style
call (open_port/2 or erl's port with {spawn_executable, "openssl"} and {args,
[...]}) to pass CertFile, KeyFile, Days and Subject as separate argv entries
(or, if you must keep os:cmd, strictly validate/escape shell metacharacters for
Subject/CertFile/KeyFile and reject unsafe input); update
generate_self_signed_cert (and callers like ensure_auto_certs/2) to either
validate inputs or use open_port with explicit args so no shell interpolation
occurs and then keep the existing file existence checks.

Comment on lines +191 to +195
authenticating(Type, Msg, StateData) ->
logger:warning("State: authenticating~nUnhandled ~p~n~p~n~p", [
Type, Msg, StateData
]),
{stop, unhandled}.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Timeout during authentication is treated as an unhandled event.

The catch-all clause at line 191 matches all event types including timeout. When the ?TIMEOUT timer fires during authentication, this will {stop, unhandled} instead of a clean shutdown. Add an explicit timeout clause before the catch-all.

Suggested fix
+authenticating(timeout, _Msg, _StateData) ->
+    logger:warning("Authentication timeout"),
+    {stop, normal};
 authenticating(Type, Msg, StateData) ->
     logger:warning("State: authenticating~nUnhandled ~p~n~p~n~p", [
         Type, Msg, StateData
     ]),
     {stop, unhandled}.
🤖 Prompt for AI Agents
In `@apps/mumble_protocol/src/mumble_server_conn.erl` around lines 191 - 195, The
catch-all clause in function authenticating/3 currently matches every event
(including the ?TIMEOUT timer) and returns {stop, unhandled}; add an explicit
clause that matches the timeout event (e.g., authenticating(?TIMEOUT, Msg,
StateData) or authenticating(timeout, Msg, StateData) depending on how ?TIMEOUT
is delivered) placed before the catch-all so the timeout case performs a clean
shutdown (for example returning {stop, timeout} or {stop, normal}) and does not
fall through to the unhandled logger/return in authenticating/3.

Comment on lines +34 to +35
handle(Session, <<1:3, Timestamp/bits>>) ->
handle_ping(Session, Timestamp, false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

cd /tmp && find . -name "mumble_udp_proto.erl" -type f 2>/dev/null | head -5

Repository: freke/erlmur

Length of output: 38


🏁 Script executed:

git ls-files | grep -i mumble | head -20

Repository: freke/erlmur

Length of output: 945


🏁 Script executed:

# Search for the file in the repository
find . -name "*.erl" -type f 2>/dev/null | head -20

Repository: freke/erlmur

Length of output: 944


🏁 Script executed:

git ls-files | grep -E "\.erl$"

Repository: freke/erlmur

Length of output: 1836


🏁 Script executed:

# List the repository structure
ls -la

Repository: freke/erlmur

Length of output: 1445


🏁 Script executed:

# Find the mumble protocol file
find . -type f -name "*mumble*" 2>/dev/null

Repository: freke/erlmur

Length of output: 1170


🏁 Script executed:

wc -l apps/mumble_protocol/src/mumble_udp_proto.erl

Repository: freke/erlmur

Length of output: 104


🏁 Script executed:

cat -n apps/mumble_protocol/src/mumble_udp_proto.erl | head -100

Repository: freke/erlmur

Length of output: 3415


🏁 Script executed:

# Search for handle_ping function
ast-grep --pattern $'handle_ping($_, $_, $_)'

Repository: freke/erlmur

Length of output: 38


🏁 Script executed:

cat apps/mumble_protocol/proto/MumbleUDP.proto

Repository: freke/erlmur

Length of output: 4256


🏁 Script executed:

# Check if there are any tests that would reveal the actual packet format
grep -r "1:3.*Timestamp\|handle_ping\|Ping" apps/mumble_protocol/test/ --include="*.erl" -A 3 | head -40

Repository: freke/erlmur

Length of output: 2996


The pattern at line 34 captures the 5-bit target field in Timestamp, causing a match error at line 69.

When the first 3 bits match type 1, Timestamp/bits captures everything after those 3 bits. For a standard 9-byte ping packet (3 bits type + 5 bits target + 64 bits timestamp = 72 bits), Timestamp becomes 69 bits. The v1.5 branch then fails at line 69 with <<T:64>> = Timestamp because 69 ≠ 64.

Fix both occurrences:

Proposed changes
-handle(Session, <<1:3, Timestamp/bits>>) ->
-    handle_ping(Session, Timestamp, false);
+handle(Session, <<1:3, _Target:5, Timestamp/binary>>) ->
+    handle_ping(Session, Timestamp, false);
             _ ->
-                <<1:3, Timestamp/bits>>
+                <<1:3, 0:5, Timestamp/binary>>
🤖 Prompt for AI Agents
In `@apps/mumble_protocol/src/mumble_udp_proto.erl` around lines 34 - 35, The
handler pattern currently captures the 5-bit target into Timestamp (<<1:3,
Timestamp/bits>>) which makes Timestamp 69 bits and breaks the later v1.5 unpack
(<<T:64>> = Timestamp); update the pattern in handle/2 (and the other similar
occurrence the comment mentions) to explicitly extract the 5-bit target and the
64-bit timestamp (e.g., match <<1:3, Target:5, T:64, _Rest/bits>> or at minimum
<<1:3, Target:5, Timestamp:64>>) so the v1.5 branch can safely do <<T:64>> =
Timestamp or use T directly; ensure any other clause that previously used
Timestamp/bits is adjusted consistently to bind Target and a 64-bit timestamp.

Comment on lines +44 to +45
encode(Num) when -4 < Num andalso Num < 0 ->
<<2#111111:6, (-Num):2>>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Mumble protocol varint encoding specification negative numbers

💡 Result:

In Mumble’s custom voice-channel varint (not Protobuf varint), “negative numbers” are represented by encoding the bitwise NOT of the value and using special leading-byte patterns:

Decode rules (what the first byte means)

Let b0 be the first byte:

  • If (b0 & 0xFC) == 0xF8 (i.e. 111110xx), then the value is negative and is decoded as:
    value = ~read_varint() (bitwise NOT of the following varint). [1]
  • If (b0 & 0xFC) == 0xFC (i.e. 111111xx), then this is an inline “small negative” and decodes as:
    value = ~(b0 & 0x03) which yields exactly -1, -2, -3, -4 for b0 = 0xFC, 0xFD, 0xFE, 0xFF. [1]

Encode rules (how to emit negative values)

When encoding a u64 that you intend to interpret as signed (two’s-complement):

  • For -1..-4 (i.e. value in 0xFFFF...FFFC to 0xFFFF...FFFF), encode as a single byte:
    0xFC | (~value as u8)-1→FC, -2→FD, -3→FE, -4→FF. [1]
  • For any other negative (i.e. top bit set, value & 0x8000_0000_0000_0000 != 0), encode:
    0xF8 followed by write_varint(~value).
    Example: -5 as u64 is ...FFFB, ~value = 4, so it encodes as F8 04. [1]

Source: This behavior is implemented (and thus precisely specified) by Mumble varint read/write logic here. [1]

References
[1] https://docs.rs/mumble-protocol/latest/src/mumble_protocol/varint.rs.html


Small-negative varint encoding violates Mumble protocol specification and breaks interoperability.

Lines 44–45 and 79–80 use an incorrect convention for encoding/decoding small negatives (−3…−1) instead of the standard Mumble range (−4…−1). The official spec defines these as:

  • Encode: −1 → 0xFC, −2 → 0xFD, −3 → 0xFE, −4 → 0xFF
  • Decode: 0xFC → −1, 0xFD → −2, 0xFE → −3, 0xFF → −4

The current code instead produces −1 → 0xFD, −3 → 0xFF, and never encodes −4 or 0xFC. This causes:

  1. Incompatibility with standard Mumble clients/servers: packets sent by or to compliant implementations will be misinterpreted.
  2. −4 inefficiently encoded: uses 9 bytes instead of 1 byte.
  3. Crash on receipt of spec-compliant packets: when decoding 0xFC (which spec clients send for −1), the guard I > 0 on line 79 excludes the match, falling through to line 81, which attempts to read 64 trailing bits and crashes if the packet is malformed or ends.

Fix by using the standard ~ (bitwise NOT) convention:

  • Line 44: change guard to Num >= -4 andalso Num < 0 and use (-(Num + 1)):2
  • Line 79: remove guard when I > 0 and use -(I + 1) for decoding
🤖 Prompt for AI Agents
In `@apps/mumble_protocol/src/mumble_varint.erl` around lines 44 - 45, The
small-negative varint handling in encode/decode is off-spec: update encode(Num)
to accept Num >= -4 andalso Num < 0 and encode using the bitwise NOT convention
by emitting (-(Num + 1)) into the 2-bit field (so −1..−4 map to 0xFC..0xFF), and
change the corresponding decode branch (the clause matching the 2-bit prefix
currently guarded with I > 0) to remove the I > 0 guard and return -(I + 1) when
decoding that 2-bit prefix; adjust the guards and arithmetic in the encode and
decode clauses that reference encode/Num and the decode variable I accordingly
so −1..−4 are encoded/decoded to 0xFC..0xFF per the Mumble spec.

Comment on lines +125 to +158
start_server(Options) when is_map(Options) ->
CertFile = maps:get(cert_file, Options, undefined),
KeyFile = maps:get(key_file, Options, undefined),
Port = maps:get(port, Options, ?DEFAULT_PORT),
AutoCreate = maps:get(auto_create_cert, Options, false),
ServerHandler = maps:get(server_handler, Options, mock_mumble_handler),

case {CertFile, KeyFile, AutoCreate} of
{undefined, undefined, true} ->
%% Auto-generate certificates
CertSubject = maps:get(cert_subject, Options, ?DEFAULT_CERT_SUBJECT),
CertDays = maps:get(cert_days, Options, ?DEFAULT_CERT_DAYS),
case mumble_cert:ensure_auto_certs(CertSubject, CertDays) of
{ok, AutoCertFile, AutoKeyFile} ->
ServerOpts = #{
cert_file => AutoCertFile,
key_file => AutoKeyFile,
port => Port,
server_handler => ServerHandler
},
mumble_server:start_server(ServerOpts);
{error, Reason} ->
{error, Reason}
end;
_ ->
%% Use provided files
ServerOpts = #{
cert_file => CertFile,
key_file => KeyFile,
port => Port,
server_handler => ServerHandler
},
mumble_server:start_server(ServerOpts)
end.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Default server_handler is mock_mumble_handler — a test module.

Line 130 defaults ServerHandler to mock_mumble_handler, and the positional start_server/2,3 variants on lines 168 and 178 hard-code this same default. A production API module should not default to a test mock. If no real handler exists yet, consider requiring the handler explicitly or raising an error when it's absent, rather than silently wiring in a mock.

Additionally, the catch-all branch on line 149 doesn't validate that both CertFile and KeyFile are non-undefined. If only one is provided (or neither, with auto_create_cert defaulting to false), undefined will be passed to mumble_server:start_server/1, likely causing a confusing failure downstream.

Proposed fix for validation
         _ ->
-            %% Use provided files
-            ServerOpts = #{
-                cert_file => CertFile,
-                key_file => KeyFile,
-                port => Port,
-                server_handler => ServerHandler
-            },
-            mumble_server:start_server(ServerOpts)
+            %% Validate both cert files are provided
+            case {CertFile, KeyFile} of
+                {undefined, _} ->
+                    {error, {missing_option, cert_file}};
+                {_, undefined} ->
+                    {error, {missing_option, key_file}};
+                _ ->
+                    ServerOpts = #{
+                        cert_file => CertFile,
+                        key_file => KeyFile,
+                        port => Port,
+                        server_handler => ServerHandler
+                    },
+                    mumble_server:start_server(ServerOpts)
+            end
     end.
🤖 Prompt for AI Agents
In `@apps/mumble_protocol/src/mumble.erl` around lines 125 - 158, The
start_server/1 function improperly defaults ServerHandler to the test module
mock_mumble_handler and fails to validate that both cert_file and key_file are
present when auto_create_cert is false; update the maps:get(server_handler, ...)
call to require an explicit handler (returning an error like {error,
missing_server_handler} or raising) instead of defaulting to
mock_mumble_handler, and add validation before calling
mumble_server:start_server/1 to ensure either AutoCreate=true or both CertFile
and KeyFile are not undefined (if one or both are missing return a clear {error,
missing_cert_or_key}), and apply the same handler-default/validation policy to
the other start_server/2 and start_server/3 variants that currently hard-code
the mock.

Comment on lines +68 to +70
accept(_Timeout, ListenSocket) ->
%% Mock implementation
{ok, {mock_accepted, ListenSocket}}.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# First, let's locate and examine the mock_transport.erl file
find . -name "mock_transport.erl" -type f

Repository: freke/erlmur

Length of output: 102


🌐 Web query:

Ranch transport callback accept signature Erlang

💡 Result:

In Ranch, the transport behavior callback for accepting connections is accept/2:

accept(LSocket :: socket(), Timeout :: timeout()) ->
    {ok, Socket :: socket()}
  | {error, closed | timeout | atom()}.

You call it with the listening socket (returned by listen/1) and a timeout (ms); it returns the newly accepted connection socket (or an error). [1]

Concrete implementations follow the same signature, e.g. ranch_etls:accept/2 and ranch_ssl2:accept/2. [2][3]

Sources: [1] [2] [3]


🏁 Script executed:

#!/bin/bash
# Search for ranch transport callback specifications in the codebase
rg -n "callback accept" --type erlang
# Also search for any documentation or specs about accept
rg -n "accept.*listener.*timeout" --type erlang
# Look for other implementations or usages of accept/2
rg -n "accept\(" --type erlang -B 1 -A 2 | head -50

Repository: freke/erlmur

Length of output: 350


🏁 Script executed:

#!/bin/bash
# Check for usages of the accept function being called
rg -n "accept\(" --type erlang -B 2 -A 2 | grep -E "(mock_transport|accept\()" | head -30
# Also check if mock_transport module is used anywhere
rg -n "mock_transport" --type erlang

Repository: freke/erlmur

Length of output: 2718


🏁 Script executed:

#!/bin/bash
# Check how mumble_server_conn uses the transport module
find . -name "mumble_server_conn.erl" -type f
cat apps/mumble_protocol/src/mumble_server_conn.erl | head -100

Repository: freke/erlmur

Length of output: 3026


🏁 Script executed:

#!/bin/bash
# Search for where accept is called with the transport module
rg -n "Transport:accept\|ranch.*accept\|accept\(.*Timeout" --type erlang -B 2 -A 2

Repository: freke/erlmur

Length of output: 38


🏁 Script executed:

#!/bin/bash
# Search for where Transport module's accept is called
rg -n "Transport:" apps/mumble_protocol/src/mumble_server_conn.erl
# Also look for ranch protocol callback invocations
rg -n "init\|handle_continue" apps/mumble_protocol/src/mumble_server_conn.erl -A 5

Repository: freke/erlmur

Length of output: 256


🏁 Script executed:

#!/bin/bash
# Let's see the init function of mumble_server_conn to understand Ranch protocol flow
sed -n '70,150p' apps/mumble_protocol/src/mumble_server_conn.erl

Repository: freke/erlmur

Length of output: 2722


🏁 Script executed:

#!/bin/bash
# Look at the full init function and see if there's any Transport:accept call
sed -n '130,200p' apps/mumble_protocol/src/mumble_server_conn.erl
# Also check if Ranch automatically calls accept before starting the protocol
rg -n "accept\|ranch_server\|ranch:add_connection" --type erlang

Repository: freke/erlmur

Length of output: 2517


🏁 Script executed:

#!/bin/bash
# Check the Ranch documentation or look for how the mock_transport.setup_ranch_mock works
cat apps/mumble_protocol/test/mock_transport.erl

Repository: freke/erlmur

Length of output: 7331


Fix accept/2 parameter order to match Ranch transport callback signature.

Ranch's ranch_transport behavior specifies accept(listener(), timeout()), but the current implementation has parameters reversed as accept(_Timeout, ListenSocket). This violates the callback contract and will cause pattern-match failures if the callback is invoked by Ranch or used with production code.

Proposed fix
-accept(_Timeout, ListenSocket) ->
+accept(ListenSocket, _Timeout) ->
     %% Mock implementation
     {ok, {mock_accepted, ListenSocket}}.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
accept(_Timeout, ListenSocket) ->
%% Mock implementation
{ok, {mock_accepted, ListenSocket}}.
accept(ListenSocket, _Timeout) ->
%% Mock implementation
{ok, {mock_accepted, ListenSocket}}.
🤖 Prompt for AI Agents
In `@apps/mumble_protocol/test/mock_transport.erl` around lines 68 - 70, The
accept/2 function currently has its parameters reversed (accept(_Timeout,
ListenSocket)), which violates Ranch's ranch_transport callback signature
accept(Listener, Timeout); update the accept/2 definition to
accept(ListenSocket, Timeout) (and rename internal variables accordingly) so it
matches the expected order and return the same {ok, {mock_accepted,
ListenSocket}} tuple; also search for and adjust any internal or test calls to
accept/2 to use the corrected parameter order to avoid pattern-match failures.

Comment on lines +91 to 94
fun({{_S, Stats}, I}) ->
1 = Stats#crypto_stats.good,
I = Stats#crypto_stats.late,
(N - 1 - I) =:= Stats#crypto_stats.lost
end,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Bug: =:= does not assert — use = for pattern match assertion.

Line 94 uses =:= (exact equality comparison) which evaluates to a boolean that is silently discarded. Unlike = on lines 92–93, this does not crash on mismatch, so the lost stat is never actually verified.

🐛 Proposed fix
             fun({{_S, Stats}, I}) ->
                 1 = Stats#crypto_stats.good,
                 I = Stats#crypto_stats.late,
-                    (N - 1 - I) =:= Stats#crypto_stats.lost
+                    (N - 1 - I) = Stats#crypto_stats.lost
             end,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fun({{_S, Stats}, I}) ->
1 = Stats#crypto_stats.good,
I = Stats#crypto_stats.late,
(N - 1 - I) =:= Stats#crypto_stats.lost
end,
fun({{_S, Stats}, I}) ->
1 = Stats#crypto_stats.good,
I = Stats#crypto_stats.late,
(N - 1 - I) = Stats#crypto_stats.lost
end,
🤖 Prompt for AI Agents
In `@apps/ocb128_crypto/test/prop_ocb128_crypto.erl` around lines 91 - 95, The
anonymous function verifying crypto stats uses the equality operator =:= which
returns a boolean and doesn't fail the test; change the comparison "(N - 1 - I)
=:= Stats#crypto_stats.lost" to a pattern match using "=" so mismatches raise an
error and actually assert the lost value (update the expression involving N, I
and Stats#crypto_stats.lost inside the fun({{_S, Stats}, I}) -> ... end).

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/ocb128_crypto/test/ocb128_crypto_benchmark_SUITE.erl (1)

3-11: ⚠️ Potential issue | 🔴 Critical

Bug: Individual benchmark test functions are not exported — CT will fail when RUN_BENCHMARKS=true.

all/0 returns atoms like encrypt_16b, decrypt_16b, etc. via maps:keys(test_params()), but the corresponding functions (Lines 78–96) are not in the -export list. Common Test requires test case functions to be exported to invoke them.

🐛 Proposed fix — add missing exports
 -export([
     all/0,
     groups/0,
     init_per_suite/1,
     end_per_suite/1,
     init_per_testcase/2,
     end_per_testcase/2,
-    run_benchmark/2
+    run_benchmark/2,
+    encrypt_16b/1, encrypt_32b/1, encrypt_128b/1, encrypt_1kb/1,
+    encrypt_4kb/1, encrypt_16kb/1, encrypt_128kb/1, encrypt_1mb/1, encrypt_4mb/1,
+    decrypt_16b/1, decrypt_32b/1, decrypt_128b/1, decrypt_1kb/1,
+    decrypt_4kb/1, decrypt_16kb/1, decrypt_128kb/1, decrypt_1mb/1, decrypt_4mb/1
 ]).

Also applies to: 78-96

🤖 Fix all issues with AI agents
In `@apps/erlmur/src/erlmur_server_handler.erl`:
- Around line 61-68: The PermissionQuery handler in function handle_msg
currently grants full admin rights by sending permissions => 16#FFFFFFFF; change
this to a minimal safe permission bitmask (e.g. only read/speak/text bits)
before sending via mumble_server_conn:send in the handle_msg clause that matches
#{message_type := 'PermissionQuery'} so clients are not given
kick/ban/manage/channel rights; update the permissions value and add a short
comment explaining the reduced default.

In `@apps/erlmur/src/erlmur_user_manager.erl`:
- Around line 48-54: The specs for get_user/1 and get_all_users/0 reference the
non-exported record `#user`{}, making them unusable to callers; update the
implementation and specs to either export the record or (preferred) return maps:
change the spec types to {ok, map()} | {error, not_found} for get_user/1 and
[map()] for get_all_users/0, and convert any returned `#user`{} values to maps
before returning (or replace internal usage of `#user`{} with maps). Update any
pattern matches or helper functions that construct/return `#user`{} (e.g., the
gen_server handlers that return {get_user, SessionId} and get_all_users) to
build maps so the public API and specs remain consistent.

In `@apps/mumble_protocol/include/mumble_protocol.hrl`:
- Around line 73-79: Remove the unused record field listening_volume_adjust from
the record definition in mumble_protocol.hrl (it is redundant with the active
field listening_volume_adjustment); update the record tuple so only
listening_volume_adjustment remains, run a project-wide search for
listening_volume_adjust to ensure there are no remaining references, and update
any comments or docs that mention the removed field.

In `@apps/mumble_protocol/src/mumble_server_conn.erl`:
- Around line 447-457: send_msg/2 can return {error, Reason} but all its callers
ignore that result, so send failures (especially during handshake messages like
CryptSetup or ServerSync) leave the connection inconsistent; update send_msg
callers to check its return and propagate or handle errors: where functions such
as the handshake/auth routines invoke send_msg, change their code to
pattern-match on ok vs {error, Reason} and on error either log and close the
socket/terminate the connection or return {error, Reason} up the call chain so
the connection state is not advanced; ensure send_msg remains returning ok |
{error, Reason} and that the calling functions propagate the error (or perform
cleanup) rather than discarding it.
- Around line 244-250: Replace direct calls to
erlmur_user_manager:register_udp_addr/2 with an invocation on the configurable
handler module so mumble_protocol has no hard dependency on erlmur_user_manager;
specifically, in mumble_server_conn.erl (NewStateData block) and the two places
in mumble_udp_server.erl, call the handler module stored on the connection state
(e.g. StateData#state.handler_mod or StateData.handler_mod) like
HandlerMod:register_udp_addr(SessionId, Addr) (or invoke via apply/3 if needed)
and remove the direct erlmur_user_manager reference so user-manager behavior is
routed through the handler_mod callback.

In `@apps/mumble_protocol/src/mumble_server_sup.erl`:
- Line 61: The supervisor currently defaults ServerHandler to the test-only mock
via proplists:get_value(server_handler, Options, mock_mumble_handler); change
this so production can't silently use the mock—either remove the default (use
proplists:get_value(server_handler, Options) and explicitly handle the undefined
case) or replace the default with a sentinel (e.g. undefined_server_handler) and
make the supervisor's init/start function validate ServerHandler and crash with
a clear error if it's not provided or is the sentinel; update the code paths
that reference ServerHandler so they rely on the validated value.

In `@apps/mumble_protocol/src/mumble_udp_server.erl`:
- Around line 141-143: The handler handle_info/2 currently logs every incoming
UDP packet with logger:info("UDP Message ~p", [Msg]) which will flood production
logs (especially for voice packets) and also uses a tab for indentation; change
the log level to debug or remove the per-packet log (e.g., use logger:debug/2)
and only log at info on notable events or errors, keep message variable Msg for
context if needed, and normalize the line indentation to match the file’s spaces
style so handle_info/2 and the Addr = {IP, Port} line use consistent spacing.

In `@apps/mumble_protocol/src/mumble_varint.erl`:
- Around line 17-20: The moduledoc and implementation incorrectly document and
use the large-negative varint prefix 0xFC (11111100) which overlaps the
small-negative range 111111xx; update the documentation and any constants/logic
in mumble_varint.erl to use 0xF8 (11111000) as the large-negative prefix and
make sure the encode/decode paths (the varint handling functions in this module)
implement the protocol: when encountering 111110xx, follow with the recursive
varint of the bitwise-NOT value (not a fixed 64-bit payload), and adjust any
pattern-matching, prefix constants, and tests to reflect 0xF8/111110xx for large
negatives and 111111xx (0xFC–0xFF) for the small-negative two-bit values.

In `@apps/mumble_protocol/test/mumble_cert_tests.erl`:
- Around line 1-202: Add real unit tests that call the public API in mumble_cert
instead of only the local OpenSSL helpers: write tests that (1) call
mumble_cert:generate_self_signed_cert(TestDirCertPath, TestDirKeyPath, Subject,
Days) and assert the cert/key files are created and OpenSSL reports valid X.509
dates/subject, (2) call mumble_cert:validate_cert_files(CertPath, KeyPath) to
assert it returns ok for valid files and an error tuple when files are missing
or malformed, and (3) call mumble_cert:ensure_auto_certs(Dir, Options) to assert
it creates cert/key when absent, reuses existing files (mtime unchanged) and
regenerates missing ones; use the existing
get_test_dir/ensure_test_dir/cleanup_test_dir helpers to manage temp dirs and
filelib:is_file/file:read_file_info to check existence and mtimes, referencing
the functions mumble_cert:generate_self_signed_cert/4,
mumble_cert:validate_cert_files/2, and mumble_cert:ensure_auto_certs/2 in your
new tests.

In `@rebar.config`:
- Line 2: The test profile's erl_opts is overwriting the base erl_opts and
losing include paths; update the test profile's {erl_opts, ...} to merge base
options by including debug_info and the include tuple {i,
"apps/mumble_protocol/include"} (instead of the hardcoded _build path) so gpb
headers generated per gpb_opts are found, or remove the redundant {i,
"_build/default/plugins/gpb/include"} from the base erl_opts; change references
to erl_opts in rebar.config and ensure the test profile keeps export_all and
nowarn_export_all plus debug_info and {i, "apps/mumble_protocol/include"}.
🟡 Minor comments (22)
apps/erlmur/src/erlmur_sup.erl-24-31 (1)

24-31: ⚠️ Potential issue | 🟡 Minor

Misleading log: "User manager started" is emitted before the child is actually started.

init/1 only builds the child spec and returns it to the supervisor framework. The actual child process is started after init/1 returns. Line 31 will log "started" prematurely — or even if the child subsequently fails to start.

Proposed fix — remove the misleading log
     },
-    logger:info("[erlmur_sup] User manager started"),
     {ok, {{one_for_one, 5, 10}, [UserManager]}}.
apps/erlmur/src/erlmur_server_handler.erl-78-86 (1)

78-86: ⚠️ Potential issue | 🟡 Minor

erlang:system_info(otp_release) returns a charlist, not a binary.

All other fields in the map (release, os) are binaries, and the protocol expects consistent types. Wrap with list_to_binary/1.

Proposed fix
-        os_version => erlang:system_info(otp_release)
+        os_version => list_to_binary(erlang:system_info(otp_release))
apps/mumble_protocol/test/mumble_cert_tests.erl-138-158 (1)

138-158: ⚠️ Potential issue | 🟡 Minor

reuse_existing_cert_test does not test any reuse logic.

This test generates a certificate, reads its mtime, sleeps 100ms, then reads the mtime again and asserts it hasn't changed. Since nothing attempted to regenerate the certificate in between, this assertion is trivially true. To meaningfully test reuse, call mumble_cert:ensure_auto_certs/2 twice and verify the second call returns the same files without regeneration.

AGENTS.md-65-65 (1)

65-65: ⚠️ Potential issue | 🟡 Minor

Incorrect Erlang syntax: try...of...else does not exist.

Erlang's try expression uses try Expr of Pattern -> Body catch Class:Reason -> Handler end. There is no else clause. This should read try...of...catch (or try...catch...after...end).

Proposed fix
-- Use `try...of...else` for exceptions that need cleanup
+- Use `try...of...catch...after...end` for exceptions that need cleanup
apps/mumble_protocol/src/mumble_udp_server.erl-38-40 (1)

38-40: ⚠️ Potential issue | 🟡 Minor

send/3 spec claims {error, term()} but gen_server:cast/2 always returns ok.

The spec -spec send(...) -> ok | {error, term()}. is misleading — gen_server:cast is fire-and-forget and always returns ok. The actual send error is logged inside handle_cast but never propagated to the caller. Either update the spec to -> ok or switch to gen_server:call if the caller needs error feedback.

Proposed fix (if fire-and-forget is intentional)
--spec send(IP :: inet:ip_address(), inet:port_number(), binary()) -> ok | {error, term()}.
+-spec send(IP :: inet:ip_address(), inet:port_number(), binary()) -> ok.
apps/mumble_protocol/src/mumble_cert.erl-127-133 (1)

127-133: ⚠️ Potential issue | 🟡 Minor

file:make_dir/1 won't create intermediate directories.

If the parent path doesn't exist (e.g., priv dir not yet created), file:make_dir/1 will fail with {error, enoent}. Use filelib:ensure_dir/1 which creates all intermediate directories for a given file path.

Proposed fix
 ensure_cert_dir(CertFile) ->
-    Dir = filename:dirname(CertFile),
-    case file:make_dir(Dir) of
-        ok -> ok;
-        {error, eexist} -> ok;
-        {error, Reason} -> {error, {dir_creation_failed, Reason}}
+    case filelib:ensure_dir(CertFile) of
+        ok -> ok;
+        {error, Reason} -> {error, {dir_creation_failed, Reason}}
     end.
AGENTS.md-20-93 (1)

20-93: ⚠️ Potential issue | 🟡 Minor

Fix markdown formatting: add blank lines around headings and fenced code blocks.

The markdownlint tool flags multiple MD022 and MD031 violations throughout the file. Each ### heading needs a blank line both above and below it, and fenced code blocks need blank lines surrounding them. For example, lines 20, 25, 31, 37, 45–46, 57, 63, 69, 76, 82, 88 all need an extra blank line inserted.

Example fix for one section
 ### Imports and Includes
+
 - Use `-include("module.hrl")` for local records

Apply the same pattern (blank line after heading, blank lines around fenced code blocks) to all flagged headings.

apps/mumble_protocol/src/mumble_client.erl-133-141 (1)

133-141: ⚠️ Potential issue | 🟡 Minor

Document the ownership model: client processes are linked to their parent and will terminate if the parent exits.

The mumble_client_conn:start_link/3 creates a bidirectional link between the calling process and the client connection process. This coupling is intentional—the client stores the parent PID and sends messages back to it throughout its lifecycle. However, this behavior is not documented in the public API. Callers must understand that:

  • If the caller process crashes, the client connection terminates with it
  • If the client connection crashes, it propagates to the caller

Add documentation to mumble_client.erl:start_client/5 clarifying this ownership requirement, or consider using gen_statem:start/4 instead of start_link if looser coupling is preferred.

apps/mumble_protocol/include/mumble_protocol.hrl-85-85 (1)

85-85: ⚠️ Potential issue | 🟡 Minor

Confusing type spec: regular | type — is type the intended atom?

The type annotation :: regular | type uses the atom type as a variant, which is both confusing (looks like a keyword) and likely not the intended domain value. If type is genuinely a valid session kind in the Mumble protocol, consider renaming it to something more descriptive (e.g., server_loopback, bot, etc.) to avoid ambiguity.

apps/mumble_protocol/README.md-37-37 (1)

37-37: ⚠️ Potential issue | 🟡 Minor

Typo: "reciveing" → "receiving".

✏️ Proposed fix
-If reciveing a `UDPTunnel` message the connection is marked "UDP verified" false, until new UDP verification is done.
+If receiving a `UDPTunnel` message the connection is marked "UDP verified" false, until new UDP verification is done.
apps/mumble_protocol/README.md-31-31 (1)

31-31: ⚠️ Potential issue | 🟡 Minor

Typo: "recieved" → "received".

Also, "30sec" should be "30 seconds" or "30 s" for readability.

✏️ Proposed fix
-If no `Ping` is recieved for 30sec the client will be disconnected from the server.
+If no `Ping` is received for 30 seconds the client will be disconnected from the server.
apps/mumble_protocol/README.md-45-45 (1)

45-45: ⚠️ Potential issue | 🟡 Minor

Typo: "explicitely" → "explicitly".

✏️ Proposed fix
-- **Automatic Fallback**: If a server hasn't received a UDP packet from a client recently (or if the client explicitely
+- **Automatic Fallback**: If a server hasn't received a UDP packet from a client recently (or if the client explicitly
apps/mumble_protocol/src/mumble_server_sup.erl-57-79 (1)

57-79: ⚠️ Potential issue | 🟡 Minor

No validation of required options — undefined values will cause child crashes.

If port, cert_file, or key_file are missing from Options, proplists:get_value returns undefined, which will propagate to mumble_udp_server:start_link(undefined) and ranch:start_listener with undefined cert/key paths. This produces confusing downstream errors. Validate required options upfront in init/1.

apps/mumble_protocol/src/mumble_msg.erl-519-532 (1)

519-532: ⚠️ Potential issue | 🟡 Minor

from_map for 'Ping' injects erlang:system_time(second) as a default — hidden side effect.

A conversion function shouldn't have time-dependent side effects. This means calling from_map(#{message_type => 'Ping'}) twice yields records with different timestamps, and round-tripping to_map → from_map won't preserve undefined — it silently fills in "now". Use undefined as the default and let the caller set the timestamp explicitly.

-        timestamp = maps:get(timestamp, Map, erlang:system_time(second)),
+        timestamp = maps:get(timestamp, Map, undefined),
apps/mumble_protocol/test/server_logic_tests.erl-16-29 (1)

16-29: ⚠️ Potential issue | 🟡 Minor

Missing process_flag(trap_exit, true) — linked process crash will take down test.

start_link links the spawned gen_statem to the test process. If the server crashes (e.g., mock setup issue), the test process dies with it instead of producing a clear test failure. The PR objectives mention adding trap_exit to fix EUnit failures, but this file doesn't include it.

Proposed fix (apply to all test funs)
 {"Server connection initialization", fun() ->
+    process_flag(trap_exit, true),
     mock_transport:setup_ranch_mock(),
apps/mumble_protocol/test/mumble_SUITE.erl-238-241 (1)

238-241: ⚠️ Potential issue | 🟡 Minor

element(3, State) is fragile — breaks silently if mumble_udp_server's record changes.

Accessing internal gen_server state by tuple position is brittle. If a field is added before Port in the #state{} record of mumble_udp_server, this index becomes wrong without any compile-time warning.

Consider using a dedicated API on mumble_udp_server to retrieve the port, or using sys:get_state/1 with a record-aware pattern if the record is accessible.

apps/mumble_protocol/test/mock_transport.erl-176-185 (1)

176-185: ⚠️ Potential issue | 🟡 Minor

Blocking receive inside handle_call freezes the gen_server.

Lines 180-184 perform a raw receive inside a handle_call callback, which blocks the entire gen_server process until a message arrives or the timeout expires. During this time, all other calls (e.g., send, setopts, get_sent_messages) will queue and potentially time out. This can cause hard-to-debug test hangs.

Consider using a continuation pattern: if no message is in the queue, return {error, empty} or store the caller's From and reply asynchronously when a message is injected.

apps/mumble_protocol/test/mumble_SUITE.erl-287-302 (1)

287-302: ⚠️ Potential issue | 🟡 Minor

Multiple test cases are stubs that don't test their stated purpose.

client_connect_disconnect_test, client_with_callback_test, client_server_communication_test, and multiple_clients_test only start and stop the server — they never create a client, send messages, or test callbacks. These pass trivially and give a false sense of coverage.

Consider either implementing the actual test logic or renaming/marking them as {skip, "not yet implemented"} to be honest about coverage.

Also applies to: 305-320, 456-471, 474-489

apps/mumble_protocol/src/mumble_client_conn.erl-180-184 (1)

180-184: ⚠️ Potential issue | 🟡 Minor

UDP ping timer runs every 5 seconds but send_udp_ping/1 is a no-op stub.

The timer at line 183 re-arms every 5 seconds and at line 194 starts immediately on entering established, but send_udp_ping/1 (line 217) and send_udp/2 (line 222) only log debug messages. This creates unnecessary timer overhead and misleading udp_verified state (line 188 would set udp_verified = true if a UDP packet arrives, but none can since nothing is sent).

Consider either implementing the UDP send path or removing the timer until it's functional.

Also applies to: 217-224

apps/mumble_protocol/test/test_utils.erl-85-95 (1)

85-95: ⚠️ Potential issue | 🟡 Minor

Negative timeout causes badargTimeout - Elapsed can go below zero.

If processing a matched message takes longer than the remaining Timeout, Timeout - Elapsed becomes negative. Erlang's receive ... after clause requires a non-negative integer, so a negative value will crash with badarg.

Proposed fix
 wait_for_messages(Pattern, Timeout, Acc) ->
     Start = erlang:monotonic_time(millisecond),
     receive
         Pattern = Msg ->
             Elapsed = erlang:monotonic_time(millisecond) - Start,
-            wait_for_messages(Pattern, Timeout - Elapsed, [Msg | Acc])
+            wait_for_messages(Pattern, max(0, Timeout - Elapsed), [Msg | Acc])
     after Timeout ->
         lists:reverse(Acc)
     end.
apps/mumble_protocol/test/mock_transport.erl-223-239 (1)

223-239: ⚠️ Potential issue | 🟡 Minor

SSL notifications use self() (gen_server pid) instead of #mock_socket{} — tests may fail to pattern-match.

Real SSL sends {ssl, SslSocket, Data} where SslSocket is the socket reference. Here, self() returns the raw gen_server pid, but callers (like mumble_server_conn) likely expect the #mock_socket{pid = Pid} record or a reference that matches what handshake returned. This mismatch can cause unhandled message warnings or silent message drops in tests.

Proposed fix
 handle_cast({inject_message, Data}, State = #{owner := Owner, opts := Opts}) ->
     NewQueue = case State of
         #{message_queue := Queue} -> Queue ++ [Data];
         _ -> [Data]
     end,
+    Socket = `#mock_socket`{pid = self()},
     
     %% Check if we should notify the owner (active mode)
     case lists:keyfind(active, 1, Opts) of
         {active, true} when Owner =/= undefined ->
-            Owner ! {ssl, self(), Data};
+            Owner ! {ssl, Socket, Data};
         {active, once} when Owner =/= undefined ->
-            Owner ! {ssl, self(), Data};
+            Owner ! {ssl, Socket, Data};
         _ ->
             ok
     end,
apps/mumble_protocol/src/mumble_server.erl-87-95 (1)

87-95: ⚠️ Potential issue | 🟡 Minor

Convert os_version to a binary to match other string fields in the version record.

erlang:system_info(otp_release) returns a charlist, but release and os fields are binaries. All test fixtures and documentation examples expect os_version as a binary. This inconsistency will cause type mismatches during encoding.

Fix
-        os_version = erlang:system_info(otp_release)
+        os_version = list_to_binary(erlang:system_info(otp_release))
🧹 Nitpick comments (30)
.github/workflows/erlang.yml (1)

17-18: Consider pinning the Erlang container image to a specific OTP version.

Using erlang:latest means CI builds can break silently when a new OTP major version is released with breaking changes. Pinning (e.g., erlang:27) gives reproducible builds while still allowing controlled upgrades.

apps/erlmur/src/erlmur_server_handler.erl (1)

26-34: MVP authentication accepts all users with no validation — ensure this is tracked.

No password check, no certificate validation, no rate limiting. The logger:warning on invalid auth is good, but there's no mechanism to reject or limit connections. Make sure there's a follow-up task to add real authentication before any non-local deployment.

apps/erlmur/src/erlmur.erl (1)

14-15: register_user/1 is a no-op stub — consider removing or wiring it up.

This function returns ok without doing anything. The actual user registration is handled by erlmur_user_manager:register_user/2. Keeping this dead code in the public API module is misleading.

apps/erlmur/src/erlmur_user_manager.erl (3)

81-86: Monitor reference is discarded — consider storing it for cleanup.

monitor(process, Pid) returns a reference that's dropped. If you ever need to demonitor (e.g., on explicit unregister via cast), you won't be able to cancel the monitor, potentially causing a spurious DOWN handling after the user is already removed in unregister_user.

Sketch: store monitor ref in the user record
 -record(user, {
     session_id :: pos_integer(),
     pid :: pid(),
     username :: binary(),
-    udp_addr :: {inet:ip_address(), inet:port_number()} | undefined
+    udp_addr :: {inet:ip_address(), inet:port_number()} | undefined,
+    mon_ref :: reference() | undefined
 }).

Then in register_user:

+    MonRef = monitor(process, Pid),
-    User = `#user`{session_id = SessionId, pid = Pid, username = Username},
+    User = `#user`{session_id = SessionId, pid = Pid, username = Username, mon_ref = MonRef},

And in unregister_user, call demonitor(MonRef, [flush]) before deleting.


107-115: unregister_user cast doesn't demonitor — may trigger a spurious DOWN handler.

After explicitly unregistering, the monitor is still active. When the process eventually exits, handle_info('DOWN', ...) will fire, scan the ETS table (user already deleted), and silently succeed — but this is wasted work and could be confusing during debugging. Pair this with storing the monitor ref (see previous comment).


157-181: DOWN handler scans entire ETS table via foldl — O(n) per disconnect.

For an MVP with a small number of users this is fine, but it won't scale. A secondary ETS index on pid (or storing the monitor ref mapped to session_id) would give O(1) cleanup.

apps/ocb128_crypto/include/ocb128_crypto.hrl (1)

1-6: Add an include guard to prevent duplicate record definitions.

If this header is included both directly and transitively (e.g., via another .hrl), the compiler will error on the duplicate -record definition. Standard Erlang practice is to wrap headers with -ifndef/-define/-endif.

Proposed fix
+-ifndef(OCB128_CRYPTO_HRL).
+-define(OCB128_CRYPTO_HRL, true).
+
 -record(crypto_stats, {
     good = 0 :: non_neg_integer(),
     late = 0 :: non_neg_integer(),
     lost = 0 :: integer()
 }).
 -type crypto_stats() :: `#crypto_stats`{}.
+
+-endif.
apps/mumble_protocol/src/mumble_client_behaviour.erl (1)

44-68: Consider adding a terminate/1 callback for connection-close cleanup.

Currently there's no way for the handler to be notified when the connection closes (gracefully or due to error). A callback like terminate(Reason, State) -> ok would let implementations release resources or log disconnection. This can be deferred, but it's a common pattern in OTP-style behaviours.

rebar.config (1)

34-46: All dependencies use unpinned branch references — builds are not reproducible.

ranch, meck, proper, and eqwalizer_support all point to {branch, "master"} or {branch, "main"}. A breaking change or force-push upstream will silently break your build. Pin to a specific tag or commit hash for reproducibility.

Example
-    {ranch, {git, "https://github.com/ninenines/ranch.git", {branch, "master"}}},
+    {ranch, {git, "https://github.com/ninenines/ranch.git", {tag, "2.1.0"}}},
apps/mumble_protocol/src/mumble_version.erl (1)

37-46: Ambiguity in V1/V2 auto-detection for low version numbers.

The guard-based heuristic (V1 =< 16#FFFFFFFF vs V2 > 16#FFFFFFFF) works for real Mumble versions (1.x+) but would misinterpret a V2-encoded version like 0.0.1 (value 65536, within 32-bit range) as a V1 value, decoding it as 1.0.0 instead.

If this function is only ever called with known-format inputs, consider either:

  • Splitting into decode_v1/1 and decode_v2/1, or
  • Adding a @doc note that the heuristic assumes Major ≥ 1.

This is unlikely to matter in practice for the Mumble protocol.

apps/mumble_protocol/test/client_logic_tests.erl (4)

312-351: Multiple tests are no-ops that always pass — they provide no coverage.

established_transition_test, client_close_test, and invalid_msg_test all just ?assert(true). Similarly, session_id_test extracts a value from a map it constructs locally, and established_receive_test sends a message to itself. None of these exercise mumble_client_conn behavior.

Consider either implementing these with a mock transport or removing them and tracking them as TODOs in an issue, so they don't give a false sense of coverage.

Would you like me to open an issue to track implementing these stub tests with proper mock transport support?


407-431: Error-handling tests don't test error handling.

network_error_test asserts that atoms are atoms, ssl_close_test pattern-matches a tuple it constructs, and invalid_msg_test asserts true. These do not exercise any mumble_client_conn error paths.


130-211: Connection tests always pass regardless of outcome — no assertions on behavior.

Tests like init_test, init_with_options_test, and connection_success_test accept both {ok, _} and {error, _} results and return ok for either case. They exercise the code path but never assert expected behavior, so they can't catch regressions.


365-375: send_voice_test only destructures a local tuple — no module code is exercised.

This test creates a voice_data tuple and asserts the values it just set. It doesn't call any mumble_client_conn or mumble_udp_proto function.

apps/ocb128_crypto/test/ocb128_crypto_benchmark_SUITE.erl (1)

18-22: Inconsistent indentation — tabs used inside all/0 while the rest of the file uses spaces.

✏️ Proposed fix
 all() ->
-    case os:getenv("RUN_BENCHMARKS") of
-			"true" -> maps:keys(test_params());
-			_ -> []
-		end.
+    case os:getenv("RUN_BENCHMARKS") of
+        "true" -> maps:keys(test_params());
+        _ -> []
+    end.
apps/ocb128_crypto/test/prop_ocb128_crypto.erl (1)

47-47: Nit: Typo in variable name EncyptedMsgs — should be EncryptedMsgs.

The variable is consistently named (also on Line 57, 67), so it compiles fine, but it's a readability nit.

apps/mumble_protocol/test/server_logic_tests.erl (1)

101-119: Test name "Invalid state transitions" doesn't test an invalid transition.

The test sends a voice_data message and asserts the process is still alive — this is the same as voice_data_test_/0. There's no actual invalid state transition being triggered (e.g., sending a message that's invalid for the current state). Consider either renaming to reflect what it actually tests or adding a genuinely invalid event.

apps/mumble_protocol/src/mumble_server_sup.erl (2)

77-78: Log messages are premature — children haven't started yet.

These log lines execute during init/1, before the supervisor has actually started the children. They're misleading. The supervisor starts children after init/1 returns. Consider removing them or moving the logging into the child start functions.


81-96: start_tcp_listener/5 lacks TLS hardening options.

The TLS socket_opts only specify port, certfile, and keyfile. For a production Mumble server, consider adding TLS version constraints and cipher suite restrictions (e.g., {versions, ['tlsv1.2', 'tlsv1.3']}) to prevent downgrade attacks. This can be deferred, but is worth noting.

apps/mumble_protocol/test/mumble_api_tests.erl (1)

9-15: Fragile OpenSSL detection — which output varies by platform.

os:cmd("which openssl") may return an error message (not empty) on systems where which is available but OpenSSL is not, and which doesn't exist on Windows. A more reliable check would be to run openssl version and check the exit code or output.

Suggested improvement
 setup() ->
-    case os:cmd("which openssl") of
-        [] -> error(openssl_not_available);
-        _ -> ok
-    end,
+    case os:cmd("openssl version") of
+        "OpenSSL" ++ _ -> ok;
+        "LibreSSL" ++ _ -> ok;
+        _ -> error(openssl_not_available)
+    end,
     ok.
apps/mumble_protocol/src/mumble_tcp_proto.erl (2)

137-144: find_msg_by_tag/1 doesn't short-circuit — lists:foldl always traverses the full list.

After finding a match, the fold continues iterating the rest of MESSAGE_TABLE. Use lists:search (as find_msg_by_record already does) or lists:keyfind for early termination.

Proposed fix
 find_msg_by_tag(Tag) ->
-    lists:foldl(fun ({T, Record}, _Acc) when T =:= Tag ->
-                        {ok, Tag, Record};
-                    (_, Acc) ->
-                        Acc
-                end,
-                error,
-                ?MESSAGE_TABLE).
+    case lists:keyfind(Tag, 1, ?MESSAGE_TABLE) of
+        {Tag, Record} -> {ok, Tag, Record};
+        false -> error
+    end.

98-108: pack/1 spec claims binary() return but raises on unknown type.

The -spec pack(map()) -> binary() doesn't reflect the error({unknown_type, Record}) crash path. Consider either returning {ok, binary()} | {error, term()} or documenting the crash behavior in the doc string.

apps/mumble_protocol/src/mumble_msg.erl (1)

499-499: No catch-all clause in from_map/1 — unknown message_type crashes with function_clause.

If a map with an unrecognized message_type is passed, the crash provides no context. Consider adding a catch-all that returns a clear error.

Suggested addition after line 774
from_map(#{message_type := Type}) ->
    error({unknown_message_type, Type});
from_map(Map) ->
    error({missing_message_type, Map}).
apps/mumble_protocol/src/mumble_server.erl (1)

113-130: Supervisor started via start_link is not part of a supervision tree.

mumble_server_sup:start_link(SupOptions) links the new supervisor to the calling process. If that process exits (e.g., a shell session or a short-lived caller), the entire server supervision tree is torn down. Consider whether start_link is the right call here, or whether the supervisor should be started under a top-level application supervisor.

apps/mumble_protocol/src/mumble_client_conn.erl (2)

52-57: Redundant case expression — gen_statem:start_link already returns the matched tuples.

Proposed simplification
 start_link(Host, Port, Opts) ->
     Parent = self(),
-    case gen_statem:start_link(?MODULE, {Host, Port, Opts, Parent}, []) of
-        {ok, Pid} -> {ok, Pid};
-        {error, Reason} -> {error, Reason}
-    end.
+    gen_statem:start_link(?MODULE, {Host, Port, Opts, Parent}, []).

130-134: Stats field names from_client_tcp_* are misleading in a client module.

These fields track packets received from the server by the client, but the field names suggest traffic originating from the client. This appears to be a reuse of the server-side #stats{} record without adapting the semantics.

Also applies to: 154-158

apps/mumble_protocol/test/test_utils.erl (1)

68-74: wait_for_message/2 only supports exact term equality, not Erlang pattern matching.

Since Pattern is a bound variable in the receive clause, it performs exact value comparison (not structural pattern matching with wildcards). For example, wait_for_message({mumble_msg, '_'}, 1000) would only match the literal atom '_', not any message. The doc and function name suggest pattern matching, which is misleading.

This is an inherent limitation of Erlang's receive with bound variables — just worth noting in the doc or function name (e.g., wait_for_exact_message).

apps/ocb128_crypto/src/ocb128_crypto.erl (1)

14-17: encrypt_ocb/3 and decrypt_ocb/3 are exported but appear to be internal primitives.

These are low-level OCB block cipher operations without public documentation. If they're exported only for testing or benchmarking, consider moving them to a test-only export or documenting them. Exporting them widens the public API surface that must remain stable.

apps/mumble_protocol/test/mumble_SUITE.erl (2)

520-576: Unused helper functions — dead code.

get_server_port/1, get_ranch_port/1, find_port_in_listeners/1, and wait_for_client_connection/2 are defined but never called by any test case. They add maintenance burden and clutter. Remove them or wire them into actual tests.


89-96: Fragile OpenSSL error detection — only matches "Error" prefix.

OpenSSL command failures may produce output starting with "unable to", "Can't open", "problems making Certificate Request", etc. Checking only for "Error" ++ _ misses most failure modes. A more reliable approach is to check the file existence (which you already do at lines 99-100) and drop this fragile prefix check.

Proposed fix — rely on file existence check
     Cmd = io_lib:format(
         "openssl req -x509 -newkey rsa:2048 -keyout ~s -out ~s -days 1 -nodes -subj '/CN=localhost' 2>&1",
         [KeyFile, CertFile]
     ),
-    case os:cmd(Cmd) of
-        "Error" ++ _ -> ct:fail(cert_generation_failed);
-        _ -> ok
-    end,
+    os:cmd(Cmd),
     
     %% Verify certs were created
     true = filelib:is_file(CertFile),
     true = filelib:is_file(KeyFile),

Comment on lines +61 to +68
handle_msg(#{message_type := 'PermissionQuery'}, State) ->
%% Respond with default permissions for MVP
mumble_server_conn:send(self(), #{
message_type => 'PermissionQuery',
channel_id => 0,
permissions => 16#FFFFFFFF %% All permissions
}),
{ok, State};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Security: granting all permissions unconditionally.

16#FFFFFFFF gives every client full admin rights (kick, ban, move, manage channels, etc.). Even for an MVP, this is risky if exposed to untrusted networks. Consider returning a minimal read/speak/text permission set now and expanding later.

🤖 Prompt for AI Agents
In `@apps/erlmur/src/erlmur_server_handler.erl` around lines 61 - 68, The
PermissionQuery handler in function handle_msg currently grants full admin
rights by sending permissions => 16#FFFFFFFF; change this to a minimal safe
permission bitmask (e.g. only read/speak/text bits) before sending via
mumble_server_conn:send in the handle_msg clause that matches #{message_type :=
'PermissionQuery'} so clients are not given kick/ban/manage/channel rights;
update the permissions value and add a short comment explaining the reduced
default.

Comment on lines +48 to +54
-spec get_user(SessionId :: pos_integer()) -> {ok, #user{}} | {error, not_found}.
get_user(SessionId) ->
gen_server:call(?MODULE, {get_user, SessionId}).

-spec get_all_users() -> [#user{}].
get_all_users() ->
gen_server:call(?MODULE, get_all_users).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

#user{} record is not exported — specs referencing it are unusable by callers.

get_user/1 and get_all_users/0 specs return #user{}, but the record is private to this module. External callers have no way to type-match or destructure the result. Either export the record (or a map-based type), or return a map from these functions.

🤖 Prompt for AI Agents
In `@apps/erlmur/src/erlmur_user_manager.erl` around lines 48 - 54, The specs for
get_user/1 and get_all_users/0 reference the non-exported record `#user`{}, making
them unusable to callers; update the implementation and specs to either export
the record or (preferred) return maps: change the spec types to {ok, map()} |
{error, not_found} for get_user/1 and [map()] for get_all_users/0, and convert
any returned `#user`{} values to maps before returning (or replace internal usage
of `#user`{} with maps). Update any pattern matches or helper functions that
construct/return `#user`{} (e.g., the gen_server handlers that return {get_user,
SessionId} and get_all_users) to build maps so the public API and specs remain
consistent.

Comment on lines +73 to +79
listening_volume_adjust = 0,
codec_version,
texture_hash,
plugin_context,
plugin_identity,
temporary_access_tokens,
listening_volume_adjustment,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for usage of both fields to determine if they serve different purposes
echo "=== listening_volume_adjust ==="
rg -n 'listening_volume_adjust[^i]' --type=erlang
echo ""
echo "=== listening_volume_adjustment ==="
rg -n 'listening_volume_adjustment' --type=erlang

Repository: freke/erlmur

Length of output: 999


Remove the unused field listening_volume_adjust (line 73).

The field listening_volume_adjust is defined in the record but never referenced anywhere in the codebase. The active field is listening_volume_adjustment (line 79), which is used in mumble_msg.erl at multiple locations. The unused field should be removed to avoid confusion and dead code.

🤖 Prompt for AI Agents
In `@apps/mumble_protocol/include/mumble_protocol.hrl` around lines 73 - 79,
Remove the unused record field listening_volume_adjust from the record
definition in mumble_protocol.hrl (it is redundant with the active field
listening_volume_adjustment); update the record tuple so only
listening_volume_adjustment remains, run a project-wide search for
listening_volume_adjust to ensure there are no remaining references, and update
any comments or docs that mention the removed field.

Comment on lines +244 to +250
NewStateData = case StateData#state.udp_addr of
undefined ->
erlmur_user_manager:register_udp_addr(SessionId, Addr),
StateData#state{crypto_state = NewCryptoState, udp_addr = Addr};
_ ->
StateData#state{crypto_state = NewCryptoState}
end,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if erlmur_user_manager exists in the mumble_protocol app or is in a separate app
fd "erlmur_user_manager" --type f

# Also check all erlmur_ references in mumble_protocol
rg -n 'erlmur_' apps/mumble_protocol/src/ --type erlang

Repository: freke/erlmur

Length of output: 442


Remove hard dependency on erlmur_user_manager — breaks standalone library goal.

erlmur_user_manager is located in apps/erlmur/src/ (separate app), yet mumble_protocol calls it directly at three locations: mumble_server_conn.erl:246, mumble_udp_server.erl:144, and mumble_udp_server.erl:151. This cross-app coupling prevents mumble_protocol from functioning as a standalone library. Route user manager operations through the handler_mod callback instead.

🤖 Prompt for AI Agents
In `@apps/mumble_protocol/src/mumble_server_conn.erl` around lines 244 - 250,
Replace direct calls to erlmur_user_manager:register_udp_addr/2 with an
invocation on the configurable handler module so mumble_protocol has no hard
dependency on erlmur_user_manager; specifically, in mumble_server_conn.erl
(NewStateData block) and the two places in mumble_udp_server.erl, call the
handler module stored on the connection state (e.g. StateData#state.handler_mod
or StateData.handler_mod) like HandlerMod:register_udp_addr(SessionId, Addr) (or
invoke via apply/3 if needed) and remove the direct erlmur_user_manager
reference so user-manager behavior is routed through the handler_mod callback.

Comment on lines +447 to +457
send_msg(#state{socket = Socket, transport = Transport}, Map) ->
case mumble_tcp_proto:pack(Map) of
Bin when is_binary(Bin) ->
logger:notice("Sending ~p (~p bytes)", [maps:get(message_type, Map), byte_size(Bin)]),
case Transport:send(Socket, Bin) of
ok -> ok;
{error, Reason} ->
logger:error("Failed to send ~p: ~p", [maps:get(message_type, Map), Reason]),
{error, Reason}
end
end.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

send_msg/2 error return is silently discarded by all callers.

send_msg can return {error, Reason} (line 455), but every call site (lines 178, 200, 353, 364, 373, 379, 387, 394, 400) ignores the return value. A send failure during authentication handshake (e.g., CryptSetup, ServerSync) would leave the connection in an inconsistent state — the server thinks it sent the messages but the client never received them.

🤖 Prompt for AI Agents
In `@apps/mumble_protocol/src/mumble_server_conn.erl` around lines 447 - 457,
send_msg/2 can return {error, Reason} but all its callers ignore that result, so
send failures (especially during handshake messages like CryptSetup or
ServerSync) leave the connection inconsistent; update send_msg callers to check
its return and propagate or handle errors: where functions such as the
handshake/auth routines invoke send_msg, change their code to pattern-match on
ok vs {error, Reason} and on error either log and close the socket/terminate the
connection or return {error, Reason} up the call chain so the connection state
is not advanced; ensure send_msg remains returning ok | {error, Reason} and that
the calling functions propagate the error (or perform cleanup) rather than
discarding it.

Port = proplists:get_value(port, Options),
CertFile = proplists:get_value(cert_file, Options),
KeyFile = proplists:get_value(key_file, Options),
ServerHandler = proplists:get_value(server_handler, Options, mock_mumble_handler),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Production default server_handler is the test mock.

mock_mumble_handler is a test-only module. If a caller omits the server_handler option, the supervisor silently falls back to a mock, which will misbehave in production. Consider removing the default or using a sentinel that fails fast.

-    ServerHandler = proplists:get_value(server_handler, Options, mock_mumble_handler),
+    case proplists:get_value(server_handler, Options) of
+        undefined -> error(missing_server_handler);
+        ServerHandler -> ServerHandler
+    end,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ServerHandler = proplists:get_value(server_handler, Options, mock_mumble_handler),
case proplists:get_value(server_handler, Options) of
undefined -> error(missing_server_handler);
ServerHandler -> ServerHandler
end,
🤖 Prompt for AI Agents
In `@apps/mumble_protocol/src/mumble_server_sup.erl` at line 61, The supervisor
currently defaults ServerHandler to the test-only mock via
proplists:get_value(server_handler, Options, mock_mumble_handler); change this
so production can't silently use the mock—either remove the default (use
proplists:get_value(server_handler, Options) and explicitly handle the undefined
case) or replace the default with a sentinel (e.g. undefined_server_handler) and
make the supervisor's init/start function validate ServerHandler and crash with
a clear error if it's not provided or is the sentinel; update the code paths
that reference ServerHandler so they rely on the validated value.

Comment on lines +141 to +143
handle_info({udp, _Socket, IP, Port, Data} = Msg, State) ->
logger:info("UDP Message ~p", [Msg]),
Addr = {IP, Port},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Logging every UDP packet at info level will be very noisy.

Line 142 logs every incoming UDP message (including voice packets) at info level. Voice traffic is high-frequency; this will flood logs in production. Additionally, line 142 uses tab indentation inconsistent with the rest of the file.

Proposed fix
 handle_info({udp, _Socket, IP, Port, Data} = Msg, State) ->
-		logger:info("UDP Message ~p", [Msg]),
+    logger:debug("[mumble_udp_server] UDP packet from ~p:~p (~B bytes)", [IP, Port, byte_size(Data)]),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
handle_info({udp, _Socket, IP, Port, Data} = Msg, State) ->
logger:info("UDP Message ~p", [Msg]),
Addr = {IP, Port},
handle_info({udp, _Socket, IP, Port, Data} = Msg, State) ->
logger:debug("[mumble_udp_server] UDP packet from ~p:~p (~B bytes)", [IP, Port, byte_size(Data)]),
Addr = {IP, Port},
🤖 Prompt for AI Agents
In `@apps/mumble_protocol/src/mumble_udp_server.erl` around lines 141 - 143, The
handler handle_info/2 currently logs every incoming UDP packet with
logger:info("UDP Message ~p", [Msg]) which will flood production logs
(especially for voice packets) and also uses a tab for indentation; change the
log level to debug or remove the per-packet log (e.g., use logger:debug/2) and
only log at info on notable events or errors, keep message variable Msg for
context if needed, and normalize the line indentation to match the file’s spaces
style so handle_info/2 and the Addr = {IP, Port} line use consistent spacing.

Comment on lines +17 to +20
- `11110000` - 5 bytes (32 bits value)
- `11110100` - 9 bytes (64 bits value)
- `111111xx` - 1 byte (2 bits value, negative numbers -4 to -1)
- `11111100` - 9 bytes (64 bits, negative numbers)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Moduledoc prefix 11111100 on line 20 overlaps with 111111xx on line 19.

11111100 (0xFC) falls within the 111111xx (0xFC–0xFF) range documented on line 19 for small negatives. Per the Mumble protocol, large negative numbers use prefix 111110xx (0xF8–0xFB), followed by a recursive varint of the bitwise-NOT value — not a fixed 64-bit payload. The doc and code should both use 0xF8 (11111000) for the large-negative prefix.

🤖 Prompt for AI Agents
In `@apps/mumble_protocol/src/mumble_varint.erl` around lines 17 - 20, The
moduledoc and implementation incorrectly document and use the large-negative
varint prefix 0xFC (11111100) which overlaps the small-negative range 111111xx;
update the documentation and any constants/logic in mumble_varint.erl to use
0xF8 (11111000) as the large-negative prefix and make sure the encode/decode
paths (the varint handling functions in this module) implement the protocol:
when encountering 111110xx, follow with the recursive varint of the bitwise-NOT
value (not a fixed 64-bit payload), and adjust any pattern-matching, prefix
constants, and tests to reflect 0xF8/111110xx for large negatives and 111111xx
(0xFC–0xFF) for the small-negative two-bit values.

Comment on lines +1 to +202
-module(mumble_cert_tests).
-include_lib("eunit/include/eunit.hrl").
-include_lib("kernel/include/file.hrl").
-include("mumble_protocol.hrl").

%%%%%%%%%%%%%%%%%%%%%%%
%% Helper Functions
%%%%%%%%%%%%%%%%%%%%%%%

get_test_dir() ->
"/tmp/mumble_test_certs_" ++ integer_to_list(erlang:phash2(make_ref())).

ensure_test_dir() ->
TestDir = get_test_dir(),
case file:make_dir(TestDir) of
ok -> ok;
{error, eexist} -> ok
end,
TestDir.

cleanup_test_dir(TestDir) ->
case filelib:is_dir(TestDir) of
true ->
Files = filelib:wildcard(filename:join(TestDir, "*")),
[file:delete(F) || F <- Files],
file:del_dir(TestDir);
false -> ok
end.

generate_cert(CertFile, KeyFile) ->
Cmd = io_lib:format(
"openssl req -x509 -newkey rsa:2048 -keyout ~s -out ~s -days 1 -nodes -subj '/CN=localhost' 2>&1",
[KeyFile, CertFile]
),
os:cmd(Cmd).

generate_cert(CertFile, KeyFile, Subject, Days) ->
Cmd = io_lib:format(
"openssl req -x509 -newkey rsa:2048 -keyout ~s -out ~s -days ~B -nodes -subj '~s' 2>&1",
[KeyFile, CertFile, Days, Subject]
),
os:cmd(Cmd).

%%%%%%%%%%%%%%%%%%%%%%%
%% Setup Check
%%%%%%%%%%%%%%%%%%%%%%%

openssl_available_test() ->
Result = os:cmd("which openssl"),
?assert(length(Result) > 0).

%%%%%%%%%%%%%%%%%%%%%%%
%% Certificate Generation Tests
%%%%%%%%%%%%%%%%%%%%%%%

generate_cert_creates_files_test() ->
TestDir = ensure_test_dir(),
try
CertFile = filename:join(TestDir, "test.pem"),
KeyFile = filename:join(TestDir, "test.key"),

generate_cert(CertFile, KeyFile),

?assert(filelib:is_file(CertFile)),
?assert(filelib:is_file(KeyFile))
after
cleanup_test_dir(TestDir)
end.

generate_cert_creates_valid_x509_test() ->
TestDir = ensure_test_dir(),
try
CertFile = filename:join(TestDir, "test.pem"),
KeyFile = filename:join(TestDir, "test.key"),

generate_cert(CertFile, KeyFile),

VerifyCmd = io_lib:format("openssl x509 -in ~s -text -noout 2>&1", [CertFile]),
Output = os:cmd(VerifyCmd),
?assert(string:find(Output, "Certificate") =/= nomatch)
after
cleanup_test_dir(TestDir)
end.

generate_cert_with_default_subject_test() ->
TestDir = ensure_test_dir(),
try
CertFile = filename:join(TestDir, "test.pem"),
KeyFile = filename:join(TestDir, "test.key"),

generate_cert(CertFile, KeyFile),

SubjectCmd = io_lib:format("openssl x509 -in ~s -subject -noout 2>&1", [CertFile]),
Subject = os:cmd(SubjectCmd),
%% OpenSSL format can be "subject=CN = localhost" or "CN=localhost"
?assert(
(string:find(Subject, "CN = localhost") =/= nomatch) orelse
(string:find(Subject, "CN=localhost") =/= nomatch)
)
after
cleanup_test_dir(TestDir)
end.

generate_cert_with_custom_subject_test() ->
TestDir = ensure_test_dir(),
try
CertFile = filename:join(TestDir, "test.pem"),
KeyFile = filename:join(TestDir, "test.key"),

generate_cert(CertFile, KeyFile, "/CN=test.example.com/O=TestOrg", 1),

SubjectCmd = io_lib:format("openssl x509 -in ~s -subject -noout 2>&1", [CertFile]),
Subject = os:cmd(SubjectCmd),
?assert(
(string:find(Subject, "test.example.com") =/= nomatch) orelse
(string:find(Subject, "test") =/= nomatch)
)
after
cleanup_test_dir(TestDir)
end.

generate_cert_with_custom_days_test() ->
TestDir = ensure_test_dir(),
try
CertFile = filename:join(TestDir, "test.pem"),
KeyFile = filename:join(TestDir, "test.key"),

generate_cert(CertFile, KeyFile, "/CN=localhost", 30),

DatesCmd = io_lib:format("openssl x509 -in ~s -dates -noout 2>&1", [CertFile]),
Dates = os:cmd(DatesCmd),
?assert(string:find(Dates, "notBefore") =/= nomatch),
?assert(string:find(Dates, "notAfter") =/= nomatch)
after
cleanup_test_dir(TestDir)
end.

reuse_existing_cert_test() ->
TestDir = ensure_test_dir(),
try
CertFile = filename:join(TestDir, "test.pem"),
KeyFile = filename:join(TestDir, "test.key"),

%% First generation
generate_cert(CertFile, KeyFile),
{ok, FileInfo1} = file:read_file_info(CertFile),
MTime1 = FileInfo1#file_info.mtime,

timer:sleep(100),

%% Check files still exist
{ok, FileInfo2} = file:read_file_info(CertFile),
MTime2 = FileInfo2#file_info.mtime,

?assertEqual(MTime1, MTime2)
after
cleanup_test_dir(TestDir)
end.

regenerate_missing_cert_test() ->
TestDir = ensure_test_dir(),
try
CertFile = filename:join(TestDir, "test.pem"),
KeyFile = filename:join(TestDir, "test.key"),

%% Generate both
generate_cert(CertFile, KeyFile),
?assert(filelib:is_file(CertFile)),
?assert(filelib:is_file(KeyFile)),

%% Delete cert only
ok = file:delete(CertFile),
?assertNot(filelib:is_file(CertFile)),

%% Regenerate
generate_cert(CertFile, KeyFile),

?assert(filelib:is_file(CertFile)),
?assert(filelib:is_file(KeyFile))
after
cleanup_test_dir(TestDir)
end.

cert_cleanup_test() ->
TestDir = ensure_test_dir(),
try
CertFile = filename:join(TestDir, "test.pem"),
KeyFile = filename:join(TestDir, "test.key"),

generate_cert(CertFile, KeyFile),
?assert(filelib:is_file(CertFile)),
?assert(filelib:is_file(KeyFile)),

%% Cleanup
Files = filelib:wildcard(filename:join(TestDir, "*")),
[file:delete(F) || F <- Files],
file:del_dir(TestDir),

?assertNot(filelib:is_dir(TestDir))
after
cleanup_test_dir(TestDir)
end.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Tests don't exercise the mumble_cert module — only local OpenSSL helpers.

Despite the module name mumble_cert_tests, none of these tests call mumble_cert:validate_cert_files/2, mumble_cert:ensure_auto_certs/2, or mumble_cert:generate_self_signed_cert/4. They only test the locally defined generate_cert/2,4 helpers which shell out to OpenSSL directly. The production code in mumble_cert.erl (including its directory-creation logic, error handling, and file validation) remains untested.

Consider adding tests that exercise the actual public API of mumble_cert.

Would you like me to generate a set of tests that exercise mumble_cert:validate_cert_files/2, mumble_cert:ensure_auto_certs/2, and mumble_cert:generate_self_signed_cert/4?

🤖 Prompt for AI Agents
In `@apps/mumble_protocol/test/mumble_cert_tests.erl` around lines 1 - 202, Add
real unit tests that call the public API in mumble_cert instead of only the
local OpenSSL helpers: write tests that (1) call
mumble_cert:generate_self_signed_cert(TestDirCertPath, TestDirKeyPath, Subject,
Days) and assert the cert/key files are created and OpenSSL reports valid X.509
dates/subject, (2) call mumble_cert:validate_cert_files(CertPath, KeyPath) to
assert it returns ok for valid files and an error tuple when files are missing
or malformed, and (3) call mumble_cert:ensure_auto_certs(Dir, Options) to assert
it creates cert/key when absent, reuses existing files (mtime unchanged) and
regenerates missing ones; use the existing
get_test_dir/ensure_test_dir/cleanup_test_dir helpers to manage temp dirs and
filelib:is_file/file:read_file_info to check existence and mtimes, referencing
the functions mumble_cert:generate_self_signed_cert/4,
mumble_cert:validate_cert_files/2, and mumble_cert:ensure_auto_certs/2 in your
new tests.

@freke freke force-pushed the umbrella branch 3 times, most recently from 691a2ba to bc7392b Compare February 11, 2026 05:01
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/ocb128_crypto/test/ocb128_crypto_benchmark_SUITE.erl (1)

3-11: ⚠️ Potential issue | 🟠 Major

Bug: Individual benchmark test functions are not exported.

all/0 returns atoms like encrypt_16b, decrypt_16b, etc., but the corresponding functions (lines 78–96) are missing from the -export list. When RUN_BENCHMARKS=true, Common Test will fail with undef errors because it cannot call unexported functions.

🐛 Proposed fix — add exports for all benchmark functions
 -export([
     all/0,
     groups/0,
     init_per_suite/1,
     end_per_suite/1,
     init_per_testcase/2,
     end_per_testcase/2,
-    run_benchmark/2
+    run_benchmark/2,
+    encrypt_16b/1, encrypt_32b/1, encrypt_128b/1,
+    encrypt_1kb/1, encrypt_4kb/1, encrypt_16kb/1,
+    encrypt_128kb/1, encrypt_1mb/1, encrypt_4mb/1,
+    decrypt_16b/1, decrypt_32b/1, decrypt_128b/1,
+    decrypt_1kb/1, decrypt_4kb/1, decrypt_16kb/1,
+    decrypt_128kb/1, decrypt_1mb/1, decrypt_4mb/1
 ]).

Also applies to: 78-96

rebar.config (1)

30-38: ⚠️ Potential issue | 🔴 Critical

Test profile erl_opts must include the base include paths to avoid compilation failures.

The test profile on line 32 only contains [export_all, nowarn_export_all] without the include paths from the base config. Multiple test files include "Mumble_gpb.hrl" and "mumble_protocol.hrl", which require the include paths "apps/mumble_protocol/include" and "_build/default/plugins/gpb/include" to resolve. Since rebar3 replaces (rather than merges) profile erl_opts, test compilation will fail.

Update the test profile erl_opts to include:

{erl_opts, [export_all, nowarn_export_all, {i, "apps/mumble_protocol/include"}, {i, "_build/default/plugins/gpb/include"}]}
🤖 Fix all issues with AI agents
In `@apps/mumble_protocol/README.md`:
- Around line 92-124: Update the README examples to use maps (the actual client
API) instead of Erlang record syntax: replace occurrences of #'TextMessage'{...}
and #'ServerSync'{...} in the client examples with map literals and map
pattern-matching, and update the send/send_voice calls and the parent loop()
receive clauses to expect {mumble_msg, Map} and match map keys (e.g.,
<<"message">> or atom keys used by the API) rather than record fields;
specifically adjust the examples around mumble_client_conn:start_link,
mumble_client_conn:send, mumble_client_conn:send_voice and the loop() receive
patterns so they show sending/receiving maps consistent with the
e2e_connection_SUITE tests.
- Around line 68-83: The README examples use Erlang record syntax
(#'TextMessage'{...}) but the runtime passes and expects maps
(mumble_tcp_proto:decode returns a map and mumble_server_conn:send forwards maps
to mumble_tcp_proto:pack), so update the handler example in
my_handler_mod:handle_msg to construct and match on maps (e.g., %{"message" =>
<<"ping">>} and %{"message" => <<"pong">>}) instead of records, and ensure the
call to mumble_server_conn:send/2 sends a map; change any pattern matches and
sent payloads accordingly so examples match the map-based API.

In `@apps/mumble_protocol/src/mumble_client_conn.erl`:
- Around line 217-224: The send_udp_ping/1 and send_udp/2 functions are
currently no-ops while established(enter, ...) starts a recurring 5s timer,
causing needless timers and an unused udp_timer state field; either implement
real UDP sending or remove the timer and related stubs. To fix, choose one: (A)
implement UDP support by opening a gen_udp socket (or using existing socket
helper), replace logger:debug in send_udp_ping/1 and send_udp/2 with actual
gen_udp:send calls, ensure the socket is stored in the state and udp_timer is
set to the timer reference created in established(enter, ...), and cancel the
timer on teardown; or (B) remove the recurring timer setup in established(enter,
...) and delete or noop the send_udp_ping/1 and send_udp/2 stubs (and remove
udp_timer from the state) so no timer churn occurs until UDP is implemented.
Ensure changes reference the functions send_udp_ping/1, send_udp/2, the
established(enter, ...) clause, and the udp_timer field so reviewers can find
and validate the fix.
- Around line 103-108: The call to ssl:connect/3 in the gen_statem state
transition can block indefinitely; update the call in the connect branch to use
ssl:connect/4 with an explicit timeout value (e.g. a configured connect_timeout
from the state or a reasonable default like 5000 ms) so the process cannot hang
forever. Locate the ssl:connect(Host, Port, SslOptsList) invocation and replace
it with ssl:connect(Host, Port, SslOptsList, Timeout) (pass the Timeout from
State such as State#state.connect_timeout or add one to the state), keeping the
same {ok, Socket} and {error, Reason} handling and return values ({next_state,
authenticating, State#state{socket = Socket}} and {stop, Reason}). Ensure any
new field introduced (connect_timeout) is initialized where State is built.

In `@apps/mumble_protocol/src/mumble_msg.erl`:
- Around line 519-532: The from_map clause for 'Ping' currently uses
erlang:system_time(second) as a default for timestamp which introduces a side
effect and uses seconds (likely wrong unit); change the default to undefined
(consistent with other optional fields) so from_map('Ping') is pure and callers
must set timestamp explicitly, updating the pattern in the from_map/1 function
and the #'Ping'{timestamp = ...} construction accordingly.

In `@apps/mumble_protocol/src/mumble_server_conn.erl`:
- Around line 492-496: notify_status/4 currently discards any return from
Mod:handle_msg/2 that isn't {ok, NewState}, which swallows {stop, Reason,
NewState} signals; update notify_status to explicitly pattern-match and
propagate a {stop, Reason, NewState} return (return it directly) and keep the
existing {ok, NewState} -> {ok, NewState} branch, falling back to {ok, State}
only for truly unexpected results—locate the notify_status function and the call
to Mod:handle_msg to add the explicit {stop, Reason, NewState} clause so stop
signals from the handler are not lost.

In `@apps/mumble_protocol/src/mumble_server_sup.erl`:
- Around line 57-62: The init/1 function uses proplists:get_value for Port,
CertFile, and KeyFile without validation, letting undefined propagate; add a
validation helper (e.g., require_opt/2 or require_opt/1 as suggested) and call
it for port, cert_file, and key_file inside init/1 so missing options return a
clear error ({error, {missing_required_option, Key}}) or crash fast with a
descriptive reason instead of passing undefined into
mumble_udp_server:start_link or ranch; update any callers or specs as needed to
handle the explicit error.
- Around line 71-76: Replace the manual RanchListener map and the indirect
start_tcp_listener approach in mumble_server_sup:init/1 with a proper ranch
child spec by calling ranch:child_spec/5 so the listener is supervised under
mumble_server_sup; specifically, remove or stop using start_tcp_listener and
instead add ranch:child_spec(mumble_tcp_listener, TransportModule,
TransportOpts, ServerHandler, ListenerOptions) (using the correct
TransportModule and TransportOpts that include Port/CertFile/KeyFile) into the
children list so crashes propagate to the supervisor and restarts won’t
conflict.

In `@apps/mumble_protocol/src/mumble_server.erl`:
- Around line 34-46: The current start_server/1 function defaults server_handler
to test-only mock_mumble_handler which will be undefined in releases; update
start_server to require the server_handler option (i.e. remove the mock default)
or change the default to a production handler (e.g. prod_mumble_handler) instead
of mock_mumble_handler so it won’t crash at runtime — locate the
maps:get(server_handler, Options, mock_mumble_handler) in start_server/1 and
either remove the third argument and return a clear {error,
missing_server_handler} when absent, or replace the default symbol with your
production handler module name.

In `@apps/mumble_protocol/src/mumble_udp_server.erl`:
- Around line 141-160: The current handler in handle_info/2 logs every UDP
packet at info level and iterates AllUsers using element(3, User), which is
fragile and the suggested element(`#user.pid`, User) would reference a different
`#user` record; fix by changing logger:info("UDP Message ~p", [Msg]) to
logger:debug("UDP Message ~p", [Msg]) and replace the anonymous function in
lists:foreach to use pattern matching over the erlmur user tuple returned by
erlmur_user_manager:get_all_users(), e.g. fun({user, _SessionId, Pid, _Username,
_UdpAddr}) -> mumble_server_conn:udp_packet(Pid, Data, Addr) end so you
explicitly extract Pid (alternatively add
-include_lib("erlmur/include/erlmur.hrl") if that header exists or keep
element(3, User) but add a clear comment documenting the assumed tuple layout).

In `@apps/mumble_protocol/test/client_logic_tests.erl`:
- Around line 312-317: The three tests established_transition_test,
client_close_test, and invalid_msg_test are only ?assert(true) placeholders and
provide no coverage; replace each placeholder with a real test or remove and
mark TODO: for established_transition_test implement sending a ServerSync-like
mock message to the client logic and assert the state becomes 'established' (use
the same test harness helpers used elsewhere in the file), for client_close_test
simulate a server/client close sequence and assert proper cleanup/closed state
and expected messages, and for invalid_msg_test send an invalid/garbage message
and assert the client drops it or logs an error (matching existing error/assert
helpers); if you cannot implement now, delete the placeholder tests and add a
TODO comment referencing these test names so they are not silently passing.

In `@apps/mumble_protocol/test/mock_transport.erl`:
- Around line 176-185: The handle_call clause for recv must not perform a raw
receive (it will never match the {'$gen_cast',...} wrapper and blocks the
gen_server); instead remove the blocking receive in handle_call({recv, ...},
...) and either (a) immediately return {reply, {error, timeout}, State} when
message_queue is empty, or (b) implement asynchronous waiting: store the caller
From (and timeout metadata) in State (e.g. pending_recv key) and return
{noreply, StateUpdated}, then in handle_cast({inject_message, Data}, State) (the
handler for send_message/2 casts) check for a pending_recv and call
gen_server:reply(From, {ok, Data}) (or enqueue if none) before updating State —
ensure handle_cast matches the inject_message tuple and updates message_queue
accordingly.

In `@apps/mumble_protocol/test/mumble_SUITE.erl`:
- Around line 287-302: The tests named client_connect_disconnect_test,
client_with_callback_test, client_server_communication_test, and
multiple_clients_test currently only start/stop the server and do not exercise
the client behaviors their names promise; update each test to either implement
the intended behavior (e.g., use mumble:connect/3 or equivalent to create a
client, perform connect/disconnect flows, register and assert callback
invocations in client_with_callback_test, send/receive messages in
client_server_communication_test, and spawn/connect multiple clients and assert
concurrency in multiple_clients_test) or explicitly mark the test as skipped by
returning {skip, "Not yet implemented"} (matching the pattern used by
server_multiple_instances_test); locate and edit the test functions
(client_connect_disconnect_test, client_with_callback_test,
client_server_communication_test, multiple_clients_test) to apply one of these
fixes so the suite no longer gives false confidence.
🟡 Minor comments (21)
justfile-48-49 (1)

48-49: ⚠️ Potential issue | 🟡 Minor

Wildcard *.pem / *.key deletion is broader than before — verify intent.

The previous version deleted specific files (cert.pem, key.pem). The new wildcards will remove all .pem and .key files found recursively under the project root, which could catch files you want to keep (e.g., manually provisioned certs outside _build). If the intent is only to clean generated test certificates, consider restricting the search path or using more specific patterns.

apps/erlmur/src/erlmur_sup.erl-23-32 (1)

23-32: ⚠️ Potential issue | 🟡 Minor

Misleading log messages — children haven't started yet during init/1.

The supervisor starts its children after init/1 returns. Line 31's "User manager started" log fires before the child is actually started, which can be confusing when debugging startup issues.

Proposed fix
 init([]) ->
-    logger:info("[erlmur_sup] Starting user manager..."),
     UserManager = #{
         id => erlmur_user_manager,
         start => {erlmur_user_manager, start_link, []},
         restart => permanent,
         type => worker
     },
-    logger:info("[erlmur_sup] User manager started"),
+    logger:info("[erlmur_sup] Initializing supervisor with children: [erlmur_user_manager]"),
     {ok, {{one_for_one, 5, 10}, [UserManager]}}.
apps/mumble_protocol/src/mumble_version.erl-37-46 (1)

37-46: ⚠️ Potential issue | 🟡 Minor

decode/1 has an ambiguity: V2-encoded values that fit in 32 bits are decoded as V1.

The range-based dispatch (V1 =< 16#FFFFFFFF vs V2 > 16#FFFFFFFF) can misclassify a V2-encoded integer if the version components are small enough to fit in 32 bits. For example, version 0.0.1 encodes to V2 = 16#0000000000010000 (65536), which falls in the V1 range and would be decoded as {1, 0, 0} instead of {0, 0, 1}.

In practice, real Mumble versions (≥1.x) always produce V2 > 32-bit max, so this is safe today. Consider adding a two-arity variant decode(Format, Value) if this ever needs to be robust for edge cases, or at minimum document the assumption.

apps/mumble_protocol/src/mumble_udp_proto.erl-9-21 (1)

9-21: ⚠️ Potential issue | 🟡 Minor

Documentation inconsistency: Type field description contradicts actual code.

Line 12 states "Type (3 bits): Message type (0=Ping, 1=Voice, etc.)" but Line 19 says "Type 1: Ping" and the code at Line 34 matches <<1:3, ...>> as ping. The Mumble protocol uses Type 1 for ping in the legacy UDP format. The doc header at Line 12 should be corrected.

📝 Proposed fix
-- **Type (3 bits)**: Message type (0=Ping, 1=Voice, etc.)
+- **Type (3 bits)**: Message type (1=Ping, 0/2-7=Voice with various codecs)
apps/mumble_protocol/src/mumble_client_conn.erl-130-134 (1)

130-134: ⚠️ Potential issue | 🟡 Minor

Rename counter fields or clarify stats semantics for client-side data tracking.

In mumble_client_conn.erl (lines 132-133 and 157-158), from_client_tcp_packets and from_client_tcp_bytes are incremented when receiving data from the server. The stats record's from_client_* fields are semantically intended for data originating from clients (as confirmed by mumble_server_conn.erl which correctly uses them for incoming client data). Either rename these fields to from_server_tcp_* for the client context, or implement separate tracking fields with clear semantics to distinguish client-received vs. server-received data.

apps/mumble_protocol/docs/establishing_connection.md-94-97 (1)

94-97: ⚠️ Potential issue | 🟡 Minor

Ping timeout value disagrees with the implementation.

The doc says "If the server doesn't receive a Ping for 30 seconds, it will drop the connection", but mumble_server_conn.erl defines ?TIMEOUT as 60000 ms (60 seconds).

apps/mumble_protocol/test/mumble_client_api_tests.erl-7-11 (1)

7-11: ⚠️ Potential issue | 🟡 Minor

Only 2 of 7 test functions are included in the test group — 5 are orphaned.

client_start_no_certs, client_send_message, client_send_voice, client_get_state, and client_stop are defined but never referenced by mumble_client_api_test_/0. They won't run during rebar3 eunit. Either add them to the group (with proper test infrastructure) or remove them to avoid confusion.

apps/mumble_protocol/docs/establishing_connection.md-83-91 (1)

83-91: ⚠️ Potential issue | 🟡 Minor

Minor discrepancy: doc says ServerSync includes permissions, code doesn't send it.

Line 89 states ServerSync fields include "permissions for the root channel", but the implementation at mumble_server_conn.erl:394-399 only sends session, max_bandwidth, and welcome_text.

apps/mumble_protocol/docs/establishing_connection.md-58-63 (1)

58-63: ⚠️ Potential issue | 🟡 Minor

Documentation mentions ServerConfig and SuggestConfig but they're not sent by the implementation.

The doc states the server sends ServerConfig and SuggestConfig during state synchronization (also shown in the Mermaid diagram at lines 114-115). However, mumble_server_conn.erl lines 355-408 only sends CryptSetup, CodecVersion, ChannelState, UserState, and ServerSync — neither ServerConfig nor SuggestConfig is sent. Either update the code to send them or revise the docs to match the current implementation.

apps/mumble_protocol/test/mumble_client_api_tests.erl-13-17 (1)

13-17: ⚠️ Potential issue | 🟡 Minor

setup/0 and cleanup/1 are defined but never wired into the test group.

The test generator at line 7 is a plain list, not a {setup, ...} fixture. These functions are dead code.

apps/mumble_protocol/src/mumble_server_conn.erl-459-464 (1)

459-464: ⚠️ Potential issue | 🟡 Minor

send_voice TCP fallback discards send_msg result.

When udp_verified is false, send_msg is called at line 464, but its return value (which may be {error, Reason}) is discarded. Since send_voice is called from established(cast, {voice_data, ...}) which expects {keep_state_and_data, ...}, a send failure will go unnoticed.

apps/mumble_protocol/README.md-44-48 (1)

44-48: ⚠️ Potential issue | 🟡 Minor

Typo: "explicitely" → "explicitly" (line 45).

📝 Proposed fix
-- **Automatic Fallback**: If a server hasn't received a UDP packet from a client recently (or if the client explicitely
+- **Automatic Fallback**: If a server hasn't received a UDP packet from a client recently (or if the client explicitly
apps/mumble_protocol/README.md-31-37 (1)

31-37: ⚠️ Potential issue | 🟡 Minor

Fix spelling errors flagged by static analysis.

  • Line 31: "recieved" → "received"
  • Line 37: "reciveing" → "receiving"
📝 Proposed fix
-If no `Ping` is recieved for 30sec the client will be disconnected from the server.
+If no `Ping` is received for 30 seconds the client will be disconnected from the server.
-If reciveing a `UDPTunnel` message the connection is marked "UDP verified" false, until new UDP verification is done.
+If receiving a `UDPTunnel` message the connection is marked "UDP verified" false, until new UDP verification is done.
apps/mumble_protocol/test/mumble_cert_tests.erl-114-117 (1)

114-117: ⚠️ Potential issue | 🟡 Minor

Weak assertion in custom subject test — string:find(Subject, "test") matches almost anything.

The fallback check string:find(Subject, "test") =/= nomatch is too permissive. It would pass even if the subject contained none of the expected fields, as long as any word containing "test" appears in the output.

🐛 Use a more specific match
         ?assert(
-            (string:find(Subject, "test.example.com") =/= nomatch) orelse
-            (string:find(Subject, "test") =/= nomatch)
+            (string:find(Subject, "test.example.com") =/= nomatch) orelse
+            (string:find(Subject, "CN = test.example.com") =/= nomatch)
         )
apps/mumble_protocol/test/mumble_cert_tests.erl-138-158 (1)

138-158: ⚠️ Potential issue | 🟡 Minor

reuse_existing_cert_test doesn't actually test reuse — it only proves the file isn't spontaneously modified.

The test generates a cert, sleeps 100ms, then re-reads the same file's mtime. Since generate_cert is never called a second time, this asserts nothing about reuse behavior. To test reuse, call generate_cert(CertFile, KeyFile) again after the sleep and verify the mtime is unchanged.

🐛 Proposed fix
         %% First generation
         generate_cert(CertFile, KeyFile),
         {ok, FileInfo1} = file:read_file_info(CertFile),
         MTime1 = FileInfo1#file_info.mtime,
         
         timer:sleep(100),
         
-        %% Check files still exist
+        %% Second generation — should reuse existing
+        generate_cert(CertFile, KeyFile),
         {ok, FileInfo2} = file:read_file_info(CertFile),
         MTime2 = FileInfo2#file_info.mtime,
         
-        ?assertEqual(MTime1, MTime2)
+        %% NOTE: This will FAIL because generate_cert always overwrites.
+        %% If the intent is to test mumble_cert:ensure_auto_certs reuse,
+        %% call that function instead of the local generate_cert helper.
+        ?assertNotEqual(MTime1, MTime2)
apps/mumble_protocol/src/mumble_udp_server.erl-38-40 (1)

38-40: ⚠️ Potential issue | 🟡 Minor

send/3 spec claims {error, term()} but gen_server:cast always returns ok.

The -spec says ok | {error, term()}, but gen_server:cast/2 always returns ok immediately. The error handling happens asynchronously inside handle_cast (Line 67) and is only logged — never returned to the caller. The spec should be ok.

Proposed fix
--spec send(IP :: inet:ip_address(), inet:port_number(), binary()) -> ok | {error, term()}.
+-spec send(IP :: inet:ip_address(), inet:port_number(), binary()) -> ok.
apps/mumble_protocol/src/mumble_server.erl-86-95 (1)

86-95: ⚠️ Potential issue | 🟡 Minor

erlang:system_info(otp_release) returns a charlist, not a binary.

os_version in the #version{} record will be a charlist while other string fields are binaries. If downstream encoding (e.g., protobuf) expects binaries, this will fail.

Proposed fix
-        os_version = erlang:system_info(otp_release)
+        os_version = list_to_binary(erlang:system_info(otp_release))
apps/erlmur/src/erlmur_server_handler.erl-78-86 (1)

78-86: ⚠️ Potential issue | 🟡 Minor

Convert erlang:system_info(otp_release) to binary for protobuf compatibility.

erlang:system_info(otp_release) returns a charlist (e.g., "27"), but the protobuf schema defines os_version as a string field. The map contains binaries for release and os, creating a type inconsistency. When protobuf encoding serializes this map, the charlist will cause encoding errors.

Proposed fix
-        os_version => erlang:system_info(otp_release)
+        os_version => list_to_binary(erlang:system_info(otp_release))

Note: The same issue exists in apps/mumble_protocol/src/mumble_server.erl:94.

apps/mumble_protocol/test/mumble_SUITE.erl-169-174 (1)

169-174: ⚠️ Potential issue | 🟡 Minor

end_per_testcase comment promises cleanup but body is a no-op.

The comment on line 173 says "Ensure no lingering processes" but no cleanup is performed. If a test fails after starting a server but before stopping it, the server process leaks and may cause subsequent tests to fail (e.g., port conflicts, registered name clashes with mumble_udp_server).

Consider stopping any leftover server references stored in Config, or at minimum removing the misleading comment.

apps/mumble_protocol/src/mumble.erl-247-250 (1)

247-250: ⚠️ Potential issue | 🟡 Minor

Type spec doesn't permit undefined for cert/key, but the doc example passes it.

Line 36 of the moduledoc shows mumble:start_client(undefined, undefined, "localhost", 64738), but the spec on line 247 declares both parameters as file:filename_all(), which does not include undefined. Dialyzer would flag callers passing undefined. Either update the spec to file:filename_all() | undefined or fix the doc example.

Suggested spec fix
--spec start_client(file:filename_all(), file:filename_all(), string(), inet:port_number()) ->
+-spec start_client(file:filename_all() | undefined, file:filename_all() | undefined, string(), inet:port_number()) ->
     {ok, client_ref()} | {error, term()}.

And correspondingly for start_client/5:

--spec start_client(file:filename_all(), file:filename_all(), string(), inet:port_number(), client_options()) ->
+-spec start_client(file:filename_all() | undefined, file:filename_all() | undefined, string(), inet:port_number(), client_options()) ->
     {ok, client_ref()} | {error, term()}.
apps/mumble_protocol/test/mumble_SUITE.erl-237-241 (1)

237-241: ⚠️ Potential issue | 🟡 Minor

Use the public get_port/0 API instead of accessing internal state.

The mumble_udp_server module already exports get_port/0 which safely retrieves the port via a proper gen_server:call. Accessing internal state via sys:get_state() and element(3, State) is brittle and unnecessary. Replace with mumble_udp_server:get_port().

🧹 Nitpick comments (33)
justfile (1)

45-45: Broad find -name "doc" may delete unintended directories.

This will recursively remove any directory named doc under the project root, including those inside dependencies or vendored code. Consider scoping the search to only the top-level generated doc directory.

Proposed fix
-	find {{justfile_directory()}} -name "doc" -exec rm -rf {} +
+	rm -rf {{justfile_directory()}}/doc
apps/ocb128_crypto/include/ocb128_crypto.hrl (1)

1-6: Consider adding an include guard.

If this header is included from multiple paths (directly and transitively), the record redefinition will cause a compilation error. A standard Erlang include guard prevents this.

♻️ Proposed fix
+-ifndef(OCB128_CRYPTO_HRL).
+-define(OCB128_CRYPTO_HRL, true).
+
 -record(crypto_stats, {
     good = 0 :: non_neg_integer(),
     late = 0 :: non_neg_integer(),
     lost = 0 :: integer()
 }).
 -type crypto_stats() :: `#crypto_stats`{}.
+
+-endif.
apps/ocb128_crypto/src/ocb128_crypto.erl (2)

14-17: Exported encrypt_ocb/3 and decrypt_ocb/3 lack -spec and -doc annotations.

All other exported functions have specs and docs. These two are public API surface (used by tests and property tests) but have neither, which will cause Dialyzer to infer less precise types and leave gaps in generated documentation.

♻️ Proposed addition above the private section
+-doc """
+Low-level OCB encryption. Returns a map with `tag` and `ciphertext` keys.
+""".
+-spec encrypt_ocb(binary(), key(), nonce()) -> #{tag := binary(), ciphertext := binary()}.
 encrypt_ocb(Plain, Key, IV) ->
     ...

+-doc """
+Low-level OCB decryption. Returns a map with `tag` and `text` keys.
+""".
+-spec decrypt_ocb(binary(), key(), nonce()) -> #{tag := binary(), text := binary()}.
 decrypt_ocb(Encrypted, Key, IV) ->
     ...

251-263: Debug logging exposes plaintext and packet contents.

Lines 253, 257, 261, and 263 log the full packet, internal history state, and decrypted plaintext at debug level. If debug logging is accidentally enabled in production, this leaks sensitive data. Consider removing or reducing the verbosity of these statements, especially the plaintext log on Line 263.

apps/ocb128_crypto/test/ocb128_crypto_benchmark_SUITE.erl (1)

19-22: Inconsistent indentation — tabs mixed with spaces.

Lines 20–22 use tab indentation while the rest of the file uses spaces. This is a minor cosmetic issue but may trip up linters or editors with different tab-width settings.

rebar.config (1)

40-45: Dependencies pinned to branch heads — no reproducible builds.

ranch and eqwalizer_support are pulled from master/main branches, meaning builds are not reproducible across time. The same applies to test deps meck and proper on lines 34–35. Consider pinning to a tag or commit SHA.

apps/mumble_protocol/src/mumble_tcp_proto.erl (2)

137-144: find_msg_by_tag/1 doesn't short-circuit on match.

lists:foldl always traverses the full MESSAGE_TABLE even after finding the matching tag. Consider using lists:keyfind/3 or lists:search/2 for early termination. The table is small, so this is a minor efficiency nit.

Proposed fix
 find_msg_by_tag(Tag) ->
-    lists:foldl(fun ({T, Record}, _Acc) when T =:= Tag ->
-                        {ok, Tag, Record};
-                    (_, Acc) ->
-                        Acc
-                end,
-                error,
-                ?MESSAGE_TABLE).
+    case lists:keyfind(Tag, 1, ?MESSAGE_TABLE) of
+        {Tag, Record} -> {ok, Tag, Record};
+        false -> error
+    end.

98-108: pack/1 spec doesn't reflect error return.

The spec says -spec pack(map()) -> binary() but line 107 calls error({unknown_type, Record}) which raises an exception rather than returning a value. The spec is technically correct (it never returns a non-binary), but the @doc on line 88 says "Output: Binary encoded for TCP transmission" without mentioning the exception. Consider documenting the error case.

apps/mumble_protocol/src/mumble_udp_proto.erl (1)

36-41: No handle/2 clause for unrecognized or malformed packets.

If the binary doesn't match either the ping pattern or the <<Type:3, Target:5, Rest/binary>> pattern (e.g., a packet shorter than 1 byte), this will crash with a function_clause error. Consider adding a catch-all clause that logs and discards malformed packets to improve resilience.

🛡️ Proposed addition
 handle(Session, <<Type:3, Target:5, Rest/binary>>) ->
     logger:debug("DataMsg~nType ~p~nTarget ~p", [Type, Target]),
     {Counter, R} = mumble_varint:decode(Rest),
     {Voice, Positional} = split_voice_positional(Type, R),
     mumble_server_conn:voice_data(Session#session.session_pid,
-                              {voice_data, Type, Target, Counter, Voice, Positional}).
+                              {voice_data, Type, Target, Counter, Voice, Positional});
+handle(_Session, Packet) ->
+    logger:warning("Ignoring malformed UDP packet: ~p bytes", [byte_size(Packet)]),
+    ok.
apps/mumble_protocol/src/mumble_msg.erl (1)

38-39: No catch-all clause for unrecognized message_type values.

Both raw_to_map/1 and from_map/1 will crash with a function_clause error if an unknown record or map with an unrecognized message_type is passed. While this may be intentional (fail-fast), consider whether a more descriptive error (e.g., error({unknown_message_type, Type})) would aid debugging in production.

Also applies to: 499-499

apps/mumble_protocol/src/mumble_client_conn.erl (4)

82-84: get_state/1 exposes the internal #state{} record to callers.

The #state{} record is private to this module (not exported via .hrl). Callers cannot pattern-match on the returned value. Consider returning a map with relevant fields instead, or exporting the record definition if intentional.


52-57: start_link/3 case expression is a no-op wrapper.

The case gen_statem:start_link(...) just re-wraps {ok, Pid} and {error, Reason} unchanged. This can be simplified to a direct return.

♻️ Proposed fix
 start_link(Host, Port, Opts) ->
     Parent = self(),
-    case gen_statem:start_link(?MODULE, {Host, Port, Opts, Parent}, []) of
-        {ok, Pid} -> {ok, Pid};
-        {error, Reason} -> {error, Reason}
-    end.
+    gen_statem:start_link(?MODULE, {Host, Port, Opts, Parent}, []).

151-153: No guard against sending when not yet in established state.

send/2 and send_voice/2 use gen_statem:cast which is fire-and-forget. If called before the FSM reaches established, the cast will hit handle_common and be silently logged as "unhandled". Consider either documenting this limitation or returning an error via gen_statem:call for send/2.


96-102: TLS connection uses {verify, verify_none} — no server certificate verification.

This is common for Mumble (self-signed certs), but worth documenting that the client trusts any server certificate. Consider allowing callers to pass custom verify options via Opts for deployments that require certificate validation.

apps/mumble_protocol/src/mumble_server_conn.erl (1)

449-450: logger:notice on every sent message is too verbose for production.

Line 450 logs at notice level for every outgoing message including routine pings. This will flood logs under normal operation. Consider using debug level.

♻️ Proposed change
-            logger:notice("Sending ~p (~p bytes)", [maps:get(message_type, Map), byte_size(Bin)]),
+            logger:debug("Sending ~p (~p bytes)", [maps:get(message_type, Map), byte_size(Bin)]),
apps/mumble_protocol/test/mumble_cert_tests.erl (1)

48-50: which openssl is platform-specific and doesn't fail gracefully.

On minimal containers or Windows, which may not exist or return unexpected output. If openssl is absent, this test fails but all subsequent tests will also fail with unhelpful errors. Consider making the test group conditional on openssl availability.

apps/mumble_protocol/test/client_logic_tests.erl (4)

407-423: network_error_test and ssl_close_test test only Erlang primitives, not client behavior.

network_error_test asserts that atoms are atoms. ssl_close_test pattern-matches a literal tuple it just constructed. Neither exercises any code from mumble_client_conn. These are false-positive coverage signals.


328-342: established_receive_test tests Erlang's receive, not the client.

Sending a message to self() and receiving it proves only that Erlang message passing works. This doesn't validate that mumble_client_conn forwards messages to its parent.


182-211: Test named "connection_success_test" always expects connection failure.

The test connects to 127.0.0.1:64738 without starting a server, so it always takes the {error, econnrefused} path. The name is misleading. Consider renaming to connection_attempt_without_server_test or similar.


268-285: version_sent_test and authenticate_sent_test test protocol utilities, not client state transitions.

These are valid protocol roundtrip tests, but they're grouped under "authenticating state" which implies they test the client's authenticating state behavior. Consider moving them to a protocol utility test group for clarity.

apps/mumble_protocol/test/e2e_connection_SUITE.erl (1)

13-33: os:cmd("openssl ...") lacks error checking — silent failure if openssl is unavailable.

If openssl is not installed, os:cmd returns an empty string or error text, and the suite proceeds with missing cert files. This would cause a confusing failure later in application:ensure_all_started(erlmur). Add a check for the generated files or openssl exit code.

♻️ Add cert file validation after generation
     os:cmd("openssl req -x509 -newkey rsa:2048 -keyout "
            ++ KeyFile
            ++ " -out "
            ++ CertFile
            ++ " -days 1 -nodes -subj '/CN=localhost'"),
+    true = filelib:is_file(CertFile),
+    true = filelib:is_file(KeyFile),
 		ct:log("Key and Cert file ~p ~p",[KeyFile, CertFile]),
apps/mumble_protocol/src/mumble_server_sup.erl (1)

77-78: Log messages claim children are starting, but they're emitted before init/1 returns.

Lines 77-78 log "Starting UDP server..." and "Starting TCP listener..." before the supervisor has actually started any children. The children are started by the supervisor framework after init/1 returns. This is misleading during debugging. Consider removing these or moving to debug level with "will start" phrasing.

apps/mumble_protocol/test/mumble_api_tests.erl (1)

9-15: OpenSSL availability check is fragile.

os:cmd("which openssl") is Unix-only and on some systems may return a "not found" message string rather than []. Consider using os:find_executable("openssl") which returns false when not found—portable and unambiguous.

Proposed fix
 setup() ->
-    %% Ensure OpenSSL is available
-    case os:cmd("which openssl") of
-        [] -> error(openssl_not_available);
-        _ -> ok
-    end,
+    case os:find_executable("openssl") of
+        false -> error(openssl_not_available);
+        _ -> ok
+    end,
     ok.
apps/mumble_protocol/src/mumble_server.erl (1)

111-130: start_link couples the supervisor lifetime to the caller.

Same concern as with the client: mumble_server_sup:start_link/1 links the supervision tree to whatever process calls start_server/1. If that process exits, the entire server tree goes down. If called from an application supervisor, this is fine; if called from a shell or one-off process, it's risky.

apps/erlmur/src/erlmur_user_manager.erl (1)

157-181: O(n) full table scan in DOWN handler to find user by PID.

ets:foldl scans every user record to find the one matching the downed PID. With many concurrent users, this runs on every disconnect inside the gen_server process, blocking other calls. Consider maintaining a secondary pid -> session_id ETS table or map for O(1) lookup.

Acceptable for an MVP with a small user count, but will become a bottleneck at scale.

apps/mumble_protocol/src/mumble_client.erl (1)

131-141: gen_statem:start_link creates an implicit link that couples caller lifetime to the connection process.

mumble_client_conn:start_link/3 links the connection to the calling process via gen_statem:start_link. If the caller isn't trapping exits, an unexpected connection crash will terminate the caller (and vice versa). Since the public API (mumble:start_client) is designed for direct application-level use and there is no supervisor managing client connections, callers should be given more control. Consider using gen_statem:start/3 with explicit monitor/2 instead, allowing callers to decide how to handle connection failures.

apps/mumble_protocol/test/test_utils.erl (1)

85-95: Minor: Timeout - Elapsed can go negative, bypassing the 0 guard clause.

When Elapsed > Timeout, the recursive call passes a negative value. While Erlang's after with a negative timeout fires immediately (so no bug), the explicit _Pattern, 0, Acc clause on line 85 is only reachable via a direct wait_for_messages(P, 0) call, never from the recursion. Consider using a when Timeout =< 0 guard instead for clarity.

Suggested fix
-wait_for_messages(_Pattern, 0, Acc) ->
+wait_for_messages(_Pattern, Timeout, Acc) when Timeout =< 0 ->
     lists:reverse(Acc);
apps/mumble_protocol/test/mumble_SUITE.erl (4)

75-96: OpenSSL availability check is fragile and Unix-specific.

Line 77: os:cmd("which openssl") returns empty only on Unix when not found; on Windows it would return an error string, not []. Additionally, line 94 only matches errors starting with "Error" — OpenSSL failures may produce different prefixes (e.g., "Can't", "unable to").

Consider using os:find_executable("openssl") which returns false when not found, and checking that the generated files exist (which you already do on lines 99-100) rather than parsing stderr.

Suggested fix for OpenSSL check
-    case os:cmd("which openssl") of
-        [] -> 
-            ct:fail(openssl_not_available);
-        _ -> 
-            ok
-    end,
+    case os:find_executable("openssl") of
+        false -> 
+            ct:fail(openssl_not_available);
+        _ -> 
+            ok
+    end,

380-413: auto_cert_reuse_test depends on prior test's side effects.

This test assumes auto_cert_basic_test ran successfully and left certs on disk (line 387 reads file info). While [sequence] ordering guarantees execution order, if auto_cert_basic_test fails, this test will crash with {error, enoent} at file:read_file_info/1 rather than producing a clear skip or descriptive error.

Consider guarding with a file existence check or adding {require, auto_cert_basic_test} semantics.


566-571: cleanup_auto_generated_certs deletes all .pem and .key files in the app's priv_dir.

This wildcard deletion is aggressive — it removes every .pem and .key file, not just auto-generated ones. If the priv_dir contains other legitimate certificate files (e.g., shipped CA bundles), they'd be lost. Consider targeting only the known auto-generated filenames (e.g., auto_server.pem, auto_server.key).

Suggested fix
 cleanup_auto_generated_certs() ->
     PrivDir = code:priv_dir(mumble_protocol),
-    Files = filelib:wildcard(filename:join(PrivDir, "*.pem")) ++
-            filelib:wildcard(filename:join(PrivDir, "*.key")),
+    Files = [filename:join(PrivDir, "auto_server.pem"),
+             filename:join(PrivDir, "auto_server.key")],
     [file:delete(F) || F <- Files],
     ok.

518-563: Remove unused helper functions get_server_port/1, get_ranch_port/1, and find_port_in_listeners/1.

These internal helper functions are never called within the test suite. get_server_port/1 only returns a hardcoded value, and get_ranch_port/1 contains complex logic that is never exercised. Dead code should be removed to reduce maintenance burden.

apps/mumble_protocol/test/mock_transport.erl (2)

87-88: recv/3 uses the same Timeout for both the gen_server call and the internal recv — potential double-timeout.

gen_server:call(Pid, {recv, Length, Timeout}, Timeout) sets Timeout as both the gen_server call deadline and the value forwarded to the handler. If the handler blocks (current code), the gen_server:call will raise {timeout, ...} at roughly the same time the internal receive after fires, creating a race between a crash and a normal {error, timeout} reply.

Even after fixing the blocking recv (per the comment above), consider whether the gen_server:call timeout should be slightly larger than the logical recv timeout, or use infinity for the call timeout since the handler now replies immediately.


40-42: Each handshake, connect, and mock_handshake call spawns an unlinked, unsupervised gen_server.

handshake/2 (line 77), connect/4 (line 84), and mock_handshake/1 (line 152) all call gen_server:start_link/3, linking the new process to the caller. In test scenarios where the caller exits or crashes before the mock socket is explicitly closed, these processes will be cleaned up by the link. However, start_link here links to whatever process happens to call it (often the test process), which may or may not be the intended owner. This is probably fine for tests but worth noting.

Also applies to: 72-78, 83-85, 150-153

Comment on lines +68 to +83
```erlang
-module(my_handler_mod).
-behaviour(mumble_server_behaviour).

% ... init and other callbacks ...

handle_msg(#'TextMessage'{message = <<"ping">>}, State) ->
% Send a message back to the client
% The server connection process (self() in this context) can be reached by
% storing it in your state or using a known registered name if applicable,
% but usually you have the connection PID.
mumble_server_conn:send(self(), #'TextMessage'{message = <<"pong">>}),
{ok, State};
handle_msg(_Msg, State) ->
{ok, State}.
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Code examples use Erlang records but the API expects maps — these examples will crash.

The server handler example at lines 74 and 79 uses record syntax (#'TextMessage'{...}), but mumble_server_conn passes decoded messages as maps (see mumble_server_conn.erl line 441 where Msg is a map from mumble_tcp_proto:decode). Similarly, send/2 at line 79 expects a map for packing via mumble_tcp_proto:pack. Users following this example will get runtime errors.

📝 Corrected example
-handle_msg(#'TextMessage'{message = <<"ping">>}, State) ->
-    % Send a message back to the client
-    % The server connection process (self() in this context) can be reached by 
-    % storing it in your state or using a known registered name if applicable, 
-    % but usually you have the connection PID.
-    mumble_server_conn:send(self(), #'TextMessage'{message = <<"pong">>}),
-    {ok, State};
+handle_msg(#{message_type := 'TextMessage', message := <<"ping">>}, State) ->
+    mumble_server_conn:send(self(), #{message_type => 'TextMessage', message => <<"pong">>}),
+    {ok, State};
🤖 Prompt for AI Agents
In `@apps/mumble_protocol/README.md` around lines 68 - 83, The README examples use
Erlang record syntax (#'TextMessage'{...}) but the runtime passes and expects
maps (mumble_tcp_proto:decode returns a map and mumble_server_conn:send forwards
maps to mumble_tcp_proto:pack), so update the handler example in
my_handler_mod:handle_msg to construct and match on maps (e.g., %{"message" =>
<<"ping">>} and %{"message" => <<"pong">>}) instead of records, and ensure the
call to mumble_server_conn:send/2 sends a map; change any pattern matches and
sent payloads accordingly so examples match the map-based API.

Comment on lines +92 to +124
```erlang
% Example: Starting a client
Opts = [{certfile, "client_cert.pem"}, {keyfile, "client_key.pem"}],
{ok, ClientPid} = mumble_client_conn:start_link({127, 0, 0, 1}, 64738, Opts).

% Sending a TCP message
mumble_client_conn:send(ClientPid, #'TextMessage'{message = <<"Hello Server!">>}).

% Sending voice data (binary)
% Packet: {voice_data, Type, Target, Counter, VoiceBin, PositionalBin}
Voice = {voice_data, 0, 0, 1, <<"audio bytes">>, undefined},
mumble_client_conn:send_voice(ClientPid, Voice).
```

**Receiving Messages:**

The client forwards all decoded messages to the process that started it (its parent) as `{mumble_msg, Record}`.

```erlang
% Example: Receiving in the parent process
loop() ->
receive
{mumble_msg, #'TextMessage'{message = Msg}} ->
io:format("Received text: ~s~n", [Msg]),
loop();
{mumble_msg, #'ServerSync'{} = Sync} ->
io:format("Logged in. My session ID is ~p~n",
[Sync#'ServerSync'.session]),
loop();
_Other ->
loop()
end.
```
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Client example also uses records; should use maps for consistency with the actual API.

Lines 98, 102–103, and 114–119 all reference record syntax. The client API appears to use maps as well (the e2e test at e2e_connection_SUITE.erl:63,100 uses maps). Update the examples accordingly.

🤖 Prompt for AI Agents
In `@apps/mumble_protocol/README.md` around lines 92 - 124, Update the README
examples to use maps (the actual client API) instead of Erlang record syntax:
replace occurrences of #'TextMessage'{...} and #'ServerSync'{...} in the client
examples with map literals and map pattern-matching, and update the
send/send_voice calls and the parent loop() receive clauses to expect
{mumble_msg, Map} and match map keys (e.g., <<"message">> or atom keys used by
the API) rather than record fields; specifically adjust the examples around
mumble_client_conn:start_link, mumble_client_conn:send,
mumble_client_conn:send_voice and the loop() receive patterns so they show
sending/receiving maps consistent with the e2e_connection_SUITE tests.

Comment on lines +103 to +108
case ssl:connect(Host, Port, SslOptsList) of
{ok, Socket} ->
{next_state, authenticating, State#state{socket = Socket}};
{error, Reason} ->
logger:error("Client failed to connect to ~p:~p reason: ~p", [Host, Port, Reason]),
{stop, Reason}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

ssl:connect/3 called without a timeout — can block indefinitely.

If the remote host is unreachable or firewalled, this call can hang the gen_statem process with no upper bound. Use ssl:connect/4 with an explicit timeout.

🐛 Proposed fix
-    case ssl:connect(Host, Port, SslOptsList) of
+    Timeout = maps:get(connect_timeout, Opts, 10000),
+    case ssl:connect(Host, Port, SslOptsList, Timeout) of
🤖 Prompt for AI Agents
In `@apps/mumble_protocol/src/mumble_client_conn.erl` around lines 103 - 108, The
call to ssl:connect/3 in the gen_statem state transition can block indefinitely;
update the call in the connect branch to use ssl:connect/4 with an explicit
timeout value (e.g. a configured connect_timeout from the state or a reasonable
default like 5000 ms) so the process cannot hang forever. Locate the
ssl:connect(Host, Port, SslOptsList) invocation and replace it with
ssl:connect(Host, Port, SslOptsList, Timeout) (pass the Timeout from State such
as State#state.connect_timeout or add one to the state), keeping the same {ok,
Socket} and {error, Reason} handling and return values ({next_state,
authenticating, State#state{socket = Socket}} and {stop, Reason}). Ensure any
new field introduced (connect_timeout) is initialized where State is built.

Comment on lines +217 to +224
send_udp_ping(_State) ->
%% NOTE: Actual UDP send should happen here.
%% Mumble UDP Ping is <<1:3, 0:5, (64-bit timestamp)>> or similar.
logger:debug("Client would send UDP Ping here").

send_udp(_State, _Payload) ->
%% NOTE: Actual UDP send should happen here.
logger:debug("Client would send UDP packet here").
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

UDP send functions are stubs — send_udp_ping and send_udp are no-ops.

Both functions only log a debug message and never actually send any UDP traffic. Meanwhile, established(enter, ...) starts a recurring 5-second timer (Line 183) that calls send_udp_ping indefinitely. This creates unnecessary timer churn with no actual functionality.

Consider either implementing the UDP path or removing the timer and stubs until UDP support is ready. The udp_timer field in the state record is also never populated.

🤖 Prompt for AI Agents
In `@apps/mumble_protocol/src/mumble_client_conn.erl` around lines 217 - 224, The
send_udp_ping/1 and send_udp/2 functions are currently no-ops while
established(enter, ...) starts a recurring 5s timer, causing needless timers and
an unused udp_timer state field; either implement real UDP sending or remove the
timer and related stubs. To fix, choose one: (A) implement UDP support by
opening a gen_udp socket (or using existing socket helper), replace logger:debug
in send_udp_ping/1 and send_udp/2 with actual gen_udp:send calls, ensure the
socket is stored in the state and udp_timer is set to the timer reference
created in established(enter, ...), and cancel the timer on teardown; or (B)
remove the recurring timer setup in established(enter, ...) and delete or noop
the send_udp_ping/1 and send_udp/2 stubs (and remove udp_timer from the state)
so no timer churn occurs until UDP is implemented. Ensure changes reference the
functions send_udp_ping/1, send_udp/2, the established(enter, ...) clause, and
the udp_timer field so reviewers can find and validate the fix.

Comment on lines +519 to +532
from_map(#{message_type := 'Ping'} = Map) ->
#'Ping'{
timestamp = maps:get(timestamp, Map, erlang:system_time(second)),
good = maps:get(good, Map, undefined),
late = maps:get(late, Map, undefined),
lost = maps:get(lost, Map, undefined),
resync = maps:get(resync, Map, undefined),
udp_packets = maps:get(udp_packets, Map, undefined),
tcp_packets = maps:get(tcp_packets, Map, undefined),
udp_ping_avg = maps:get(udp_ping_avg, Map, undefined),
udp_ping_var = maps:get(udp_ping_var, Map, undefined),
tcp_ping_avg = maps:get(tcp_ping_avg, Map, undefined),
tcp_ping_var = maps:get(tcp_ping_var, Map, undefined)
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Side-effectful default for timestamp in from_map for 'Ping'.

Line 521 uses erlang:system_time(second) as the default when the timestamp key is missing. This introduces a side effect in a pure conversion function, making it non-deterministic and harder to test. Additionally, Mumble ping timestamps are typically in microseconds (or at least milliseconds), not seconds — this default could produce unexpected values.

Consider using undefined as the default (consistent with other optional fields) and letting callers set the timestamp explicitly.

♻️ Proposed fix
 from_map(#{message_type := 'Ping'} = Map) ->
     #'Ping'{
-        timestamp = maps:get(timestamp, Map, erlang:system_time(second)),
+        timestamp = maps:get(timestamp, Map, undefined),
         good = maps:get(good, Map, undefined),
🤖 Prompt for AI Agents
In `@apps/mumble_protocol/src/mumble_msg.erl` around lines 519 - 532, The from_map
clause for 'Ping' currently uses erlang:system_time(second) as a default for
timestamp which introduces a side effect and uses seconds (likely wrong unit);
change the default to undefined (consistent with other optional fields) so
from_map('Ping') is pure and callers must set timestamp explicitly, updating the
pattern in the from_map/1 function and the #'Ping'{timestamp = ...} construction
accordingly.

Comment on lines +34 to +46
start_server(Options) ->
CertFile = maps:get(cert_file, Options, undefined),
KeyFile = maps:get(key_file, Options, undefined),
Port = maps:get(port, Options, ?DEFAULT_PORT),
ServerHandler = maps:get(server_handler, Options, mock_mumble_handler),

%% Validate certificate files
case mumble_cert:validate_cert_files(CertFile, KeyFile) of
ok ->
start_server_with_certs(CertFile, KeyFile, Port, ServerHandler);
{error, Reason} ->
{error, Reason}
end.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Defaulting server_handler to mock_mumble_handler is unsafe for production.

mock_mumble_handler lives in the test directory and won't be available in a release build. If a caller omits server_handler from the options map, this will crash with undef at runtime. Either require the option or default to a production-ready handler.

Proposed fix — require the option explicitly
-    ServerHandler = maps:get(server_handler, Options, mock_mumble_handler),
+    case maps:find(server_handler, Options) of
+        {ok, ServerHandler} ->
+            ok;
+        error ->
+            error({missing_required_option, server_handler})
+    end,

Or alternatively, if you want to keep the map extraction pattern:

-    ServerHandler = maps:get(server_handler, Options, mock_mumble_handler),
+    ServerHandler = maps:get(server_handler, Options, undefined),
+    ServerHandler =:= undefined andalso error({missing_required_option, server_handler}),
🤖 Prompt for AI Agents
In `@apps/mumble_protocol/src/mumble_server.erl` around lines 34 - 46, The current
start_server/1 function defaults server_handler to test-only mock_mumble_handler
which will be undefined in releases; update start_server to require the
server_handler option (i.e. remove the mock default) or change the default to a
production handler (e.g. prod_mumble_handler) instead of mock_mumble_handler so
it won’t crash at runtime — locate the maps:get(server_handler, Options,
mock_mumble_handler) in start_server/1 and either remove the third argument and
return a clear {error, missing_server_handler} when absent, or replace the
default symbol with your production handler module name.

Comment on lines +312 to +317
established_transition_test() ->
%% Test that receiving ServerSync triggers established state
%% This requires a mock server

%% For now, verify the test structure
?assert(true).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Multiple tests are ?assert(true) placeholders providing zero coverage.

established_transition_test, client_close_test, and invalid_msg_test all just assert true. They give the impression of coverage while testing nothing. They'll silently pass even if the code they claim to test is completely broken. Either implement them with real assertions or remove them and track as TODO.

Also applies to: 344-350, 425-430

🤖 Prompt for AI Agents
In `@apps/mumble_protocol/test/client_logic_tests.erl` around lines 312 - 317, The
three tests established_transition_test, client_close_test, and invalid_msg_test
are only ?assert(true) placeholders and provide no coverage; replace each
placeholder with a real test or remove and mark TODO: for
established_transition_test implement sending a ServerSync-like mock message to
the client logic and assert the state becomes 'established' (use the same test
harness helpers used elsewhere in the file), for client_close_test simulate a
server/client close sequence and assert proper cleanup/closed state and expected
messages, and for invalid_msg_test send an invalid/garbage message and assert
the client drops it or logs an error (matching existing error/assert helpers);
if you cannot implement now, delete the placeholder tests and add a TODO comment
referencing these test names so they are not silently passing.

Comment on lines +176 to +185
handle_call({recv, _Length, _Timeout}, _From, State = #{message_queue := [Msg | Rest]}) ->
{reply, {ok, Msg}, State#{message_queue := Rest}};
handle_call({recv, _Length, Timeout}, _From, State) ->
%% Wait for a message to be injected
receive
{inject_message, Data} ->
{reply, {ok, Data}, State}
after Timeout ->
{reply, {error, timeout}, State}
end;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Blocking receive inside handle_call will never match send_message/2 injections — deadlock risk.

When the message queue is empty, handle_call({recv, ...}) enters a raw receive waiting for {inject_message, Data}. However, send_message/2 uses gen_server:cast/2, which delivers the message as {'$gen_cast', {inject_message, Data}}. The raw receive will never match this wrapped form, so recv will always time out when the queue is empty.

Furthermore, this raw receive blocks the gen_server process, preventing it from processing any other calls or casts until the timeout fires. This means even if the inject arrives via a different path, the process is stuck.

Suggested fix — only serve from the queue, don't block
 handle_call({recv, _Length, _Timeout}, _From, State = #{message_queue := [Msg | Rest]}) ->
     {reply, {ok, Msg}, State#{message_queue := Rest}};
-handle_call({recv, _Length, Timeout}, _From, State) ->
-    %% Wait for a message to be injected
-    receive
-        {inject_message, Data} ->
-            {reply, {ok, Data}, State}
-    after Timeout ->
-        {reply, {error, timeout}, State}
-    end;
+handle_call({recv, _Length, _Timeout}, _From, State) ->
+    {reply, {error, timeout}, State};

If blocking recv semantics are truly needed, consider restructuring to reply asynchronously: store the From reference in state, return {noreply, State}, and reply from handle_cast({inject_message, ...}) when data arrives.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
handle_call({recv, _Length, _Timeout}, _From, State = #{message_queue := [Msg | Rest]}) ->
{reply, {ok, Msg}, State#{message_queue := Rest}};
handle_call({recv, _Length, Timeout}, _From, State) ->
%% Wait for a message to be injected
receive
{inject_message, Data} ->
{reply, {ok, Data}, State}
after Timeout ->
{reply, {error, timeout}, State}
end;
handle_call({recv, _Length, _Timeout}, _From, State = #{message_queue := [Msg | Rest]}) ->
{reply, {ok, Msg}, State#{message_queue := Rest}};
handle_call({recv, _Length, _Timeout}, _From, State) ->
{reply, {error, timeout}, State};
🤖 Prompt for AI Agents
In `@apps/mumble_protocol/test/mock_transport.erl` around lines 176 - 185, The
handle_call clause for recv must not perform a raw receive (it will never match
the {'$gen_cast',...} wrapper and blocks the gen_server); instead remove the
blocking receive in handle_call({recv, ...}, ...) and either (a) immediately
return {reply, {error, timeout}, State} when message_queue is empty, or (b)
implement asynchronous waiting: store the caller From (and timeout metadata) in
State (e.g. pending_recv key) and return {noreply, StateUpdated}, then in
handle_cast({inject_message, Data}, State) (the handler for send_message/2
casts) check for a pending_recv and call gen_server:reply(From, {ok, Data}) (or
enqueue if none) before updating State — ensure handle_cast matches the
inject_message tuple and updates message_queue accordingly.

Comment on lines +287 to +302
client_connect_disconnect_test(Config) ->
CertFile = ?config(cert_file, Config),
KeyFile = ?config(key_file, Config),

%% Start server
case mumble:start_server(CertFile, KeyFile, 0) of
{ok, {mumble_server, _, _} = ServerRef} ->
%% Server started successfully
ct:pal("Server started: ~p", [ServerRef]),

%% Cleanup
ok = mumble:stop_listener(ServerRef),
ok;
Other ->
ct:fail({unexpected_server_result, Other})
end.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Multiple "client" and "full flow" tests are stubs that only test server start/stop.

client_connect_disconnect_test, client_with_callback_test, client_server_communication_test, and multiple_clients_test all share the same body: start a server, log, stop. They don't connect any client, test callbacks, send messages, or exercise multiple clients as their names promise. This inflates perceived coverage without testing the advertised functionality.

Consider either implementing the actual test logic or clearly marking them as {skip, "Not yet implemented"} (like server_multiple_instances_test) to avoid false confidence.

🤖 Prompt for AI Agents
In `@apps/mumble_protocol/test/mumble_SUITE.erl` around lines 287 - 302, The tests
named client_connect_disconnect_test, client_with_callback_test,
client_server_communication_test, and multiple_clients_test currently only
start/stop the server and do not exercise the client behaviors their names
promise; update each test to either implement the intended behavior (e.g., use
mumble:connect/3 or equivalent to create a client, perform connect/disconnect
flows, register and assert callback invocations in client_with_callback_test,
send/receive messages in client_server_communication_test, and spawn/connect
multiple clients and assert concurrency in multiple_clients_test) or explicitly
mark the test as skipped by returning {skip, "Not yet implemented"} (matching
the pattern used by server_multiple_instances_test); locate and edit the test
functions (client_connect_disconnect_test, client_with_callback_test,
client_server_communication_test, multiple_clients_test) to apply one of these
fixes so the suite no longer gives false confidence.

ocb128 keep track of stats
feat: implement mumble.erl API with comprehensive documentation

Add complete Mumble protocol implementation including:

API Implementation:
- start_server/1,2,3 with certificate validation and auto-generation
- start_client/4,5 with TLS support and callback handling
- stop_listener/1 and stop_client/1 for cleanup
- send/2, send_voice/2, get_state/1 for client operations
- server_version/0 and serverconfig/0 for server info

Connection Handling:
- mumble_client_conn: gen_statem-based client connection handler
- mumble_server_conn: server-side connection with OCB-AES128 encryption
- mumble_server_sup: supervisor managing TCP and UDP servers
- mumble_udp_server: UDP voice traffic handling

Protocol Support:
- mumble_tcp_proto: TCP message packing/unpacking
- mumble_udp_proto: UDP packet handling
- mumble_varint: Variable-length integer encoding
- mumble_msg: Record to map conversions

Behaviours:
- mumble_client_behaviour: client callback interface
- mumble_server_behaviour: server handler interface

Documentation:
- Complete -moduledoc for all modules
- Function -doc attributes with inputs/outputs
- Type documentation for public types
- Usage examples throughout

Testing:
- mumble_api_tests: API validation tests
- mumble_client_api_tests: client functionality tests
- mumble_cert_tests: certificate generation tests
- mumble_SUITE: integration tests

All 128 EUnit tests, 34 CT tests, and 5 property tests pass.
Dialyzer type checking passes with no warnings.

Refs: erlmur architecture v1.0

refactor: split mumble.erl into specialized modules

test: add comprehensive connection logic unit tests

Fix eunit tests

- Fixed client connection tests that were failing due to unhandled process exits
- Added trap_exit and logger suppression to prevent test failures
- Fixed mock handler and transport setup for server tests
- Client tests now pass successfully

Add mock transport support for server tests

- Fixed client connection tests that were failing due to unhandled process exits
- Added trap_exit and logger suppression to prevent test failures  
- Fixed mock handler and transport setup for server tests
- Client tests now pass successfully

Add mock transport support for server tests

- Fixed client connection tests that were failing due to unhandled process exits
- Added trap_exit and logger suppression to prevent test failures  
- Fixed mock handler and transport setup for server tests
- Client tests now pass successfully

Fixed build warnings

Fixed ping
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/ocb128_crypto/test/ocb128_crypto_benchmark_SUITE.erl (1)

3-11: ⚠️ Potential issue | 🔴 Critical

Missing exports for individual benchmark test functions.

The 18 test functions (encrypt_16b/1 through decrypt_4mb/1, lines 78–96) are defined but not exported. When RUN_BENCHMARKS=true, all/0 returns their names and Common Test will attempt to call them, resulting in undefined errors.

🐛 Add missing exports
 -export([
     all/0,
     groups/0,
     init_per_suite/1,
     end_per_suite/1,
     init_per_testcase/2,
     end_per_testcase/2,
-    run_benchmark/2
+    run_benchmark/2,
+    encrypt_16b/1, encrypt_32b/1, encrypt_128b/1,
+    encrypt_1kb/1, encrypt_4kb/1, encrypt_16kb/1,
+    encrypt_128kb/1, encrypt_1mb/1, encrypt_4mb/1,
+    decrypt_16b/1, decrypt_32b/1, decrypt_128b/1,
+    decrypt_1kb/1, decrypt_4kb/1, decrypt_16kb/1,
+    decrypt_128kb/1, decrypt_1mb/1, decrypt_4mb/1
 ]).
🤖 Fix all issues with AI agents
In `@apps/erlmur/src/erlmur_server_handler.erl`:
- Around line 26-34: The authenticate/2 function currently does a destructive
match on the call to erlmur_user_manager:register_user(self(), Username) which
will crash on {error,_}; update authenticate/2 to handle both successful and
error tuples from erlmur_user_manager:register_user/2 instead of using a direct
{ok, SessionId} = ... match: on {ok, SessionId} proceed to build UserInfo, log
with logger:info and return {ok, UserInfo, State#state{session_id = SessionId,
username = Username}}; on {error, Reason} log a warning or error with the Reason
(using logger:warning/2 or logger:error/2) and return a non-crashing error tuple
such as {error, {register_failed, Reason}, State} (or {error, invalid_auth,
State} if you prefer), ensuring you reference the authenticate/2 function,
erlmur_user_manager:register_user/2, SessionId, UserInfo and State#state when
making the change.

In `@apps/mumble_protocol/include/mumble_protocol.hrl`:
- Around line 42-56: The record stats currently types udp_ping_avg,
udp_ping_var, tcp_ping_avg and tcp_ping_var as non_neg_integer(), which
conflicts with the protobuf Ping definition that emits floats; update the stats
record (record name: stats) to declare those four fields as float() and set
their defaults to 0.0 (udp_ping_avg, udp_ping_var, tcp_ping_avg, tcp_ping_var)
so the types match gpb-decoded values and avoid runtime/type-mismatch issues.

In `@apps/mumble_protocol/src/mumble_server_behaviour.erl`:
- Around line 61-62: The callback spec for authenticate/2 is incorrect: it
currently lists an error return as {error, Reason} but the implementation
erlmur_server_handler:authenticate/2 returns {error, invalid_auth, State};
update the behaviour spec to allow a 3-tuple error that includes the preserved
State (e.g. {error, Reason :: any(), State :: any()}) so the contract matches
the implementation and callers can recover state after failed auth.

In `@apps/mumble_protocol/test/mock_transport.erl`:
- Around line 223-239: handle_cast/2's inject_message currently both sends the
message to Owner (active mode) and appends it to message_queue, causing recv/3
to return duplicates; change the logic in handle_cast({inject_message, Data},
State = #{owner := Owner, opts := Opts}) so that when Opts contains {active,
true} or {active, once} and Owner =/= undefined you do NOT append Data to
message_queue (i.e., set NewQueue = Queue unchanged or skip insertion), and only
enqueue Data when not in active mode; ensure State#{message_queue := NewQueue}
still updates correctly and that the active-once semantics (if any) are
preserved for subsequent deliveries.

In `@apps/mumble_protocol/test/mumble_client_api_tests.erl`:
- Around line 7-11: The test generator mumble_client_api_test_/0 currently only
lists client_start_invalid_certs/0 and client_invalid_ref/0 while the functions
client_start_no_certs/0, client_send_message/0, client_send_voice/0,
client_get_state/0, and client_stop/0 are defined but never executed; update
mumble_client_api_test_/0 to include all those test function references
(client_start_no_certs, client_send_message, client_send_voice,
client_get_state, client_stop) so EUnit will run them, or alternatively remove
the unused function definitions if they are intentionally deprecated.
🟡 Minor comments (18)
apps/mumble_protocol/README.md-31-31 (1)

31-31: ⚠️ Potential issue | 🟡 Minor

Typo: "recieved" → "received".

Proposed fix
-If no `Ping` is recieved for 30sec the client will be disconnected from the server.
+If no `Ping` is received for 30 seconds, the client will be disconnected from the server.
AGENTS.md-63-67 (1)

63-67: ⚠️ Potential issue | 🟡 Minor

Incorrect Erlang syntax in error handling guideline.

Line 65: Erlang's try expression doesn't have an else clause. The correct form is try Expr of Pattern -> Body catch Class:Reason -> Handler after Cleanup end. Consider correcting to try...of...catch...after.

Proposed fix
-- Use `try...of...else` for exceptions that need cleanup
+- Use `try...of...catch...after` for exceptions that need cleanup
apps/mumble_protocol/README.md-37-37 (1)

37-37: ⚠️ Potential issue | 🟡 Minor

Typo: "reciveing" → "receiving".

Proposed fix
-If reciveing a `UDPTunnel` message the connection is marked "UDP verified" false, until new UDP verification is done.
+If receiving a `UDPTunnel` message, the connection is marked "UDP verified" false until new UDP verification is done.
AGENTS.md-82-86 (1)

82-86: ⚠️ Potential issue | 🟡 Minor

Inaccurate Common Test guideline.

Line 84: Erlang Common Test does not define a -suite() attribute. CT suites export suite/0, all/0, init_per_suite/1, etc. as functions. Consider correcting the guidance.

Proposed fix
-- Common Test: Define `-suite()` and `-export([suite/0, ...])`
+- Common Test: Export `suite/0`, `all/0`, `init_per_suite/1`, `end_per_suite/1`, etc.
justfile-45-45 (1)

45-45: ⚠️ Potential issue | 🟡 Minor

find -name "doc" may delete unintended directories.

This will recursively find and remove any directory named doc under the project root, not just build-generated documentation. Consider scoping it to the known output location (e.g., rm -rf doc at the project root) or excluding source directories.

Proposed fix
-	find {{justfile_directory()}} -name "doc" -exec rm -rf {} +
+	rm -rf {{justfile_directory()}}/doc
apps/erlmur/src/erlmur_sup.erl-23-32 (1)

23-32: ⚠️ Potential issue | 🟡 Minor

Misleading log message — child hasn't started yet at Line 31.

init/1 only builds the child spec; the supervisor starts children after init/1 returns. The log at Line 31 will print "User manager started" before the process is actually started, which is misleading during troubleshooting.

Proposed fix
 init([]) ->
     logger:info("[erlmur_sup] Starting user manager..."),
     UserManager = #{
         id => erlmur_user_manager,
         start => {erlmur_user_manager, start_link, []},
         restart => permanent,
         type => worker
     },
-    logger:info("[erlmur_sup] User manager started"),
+    logger:info("[erlmur_sup] Supervisor init complete, children will be started"),
     {ok, {{one_for_one, 5, 10}, [UserManager]}}.
apps/mumble_protocol/src/mumble_version.erl-37-46 (1)

37-46: ⚠️ Potential issue | 🟡 Minor

decode/1 V1/V2 discrimination can misinterpret small V2 values.

The heuristic V1 =< 16#FFFFFFFF vs V2 > 16#FFFFFFFF breaks for V2-encoded versions with small fields. For example, V2 for 0.0.1 encodes to 65536 (fits in 32 bits), so decode(65536) would parse it as V1 and yield 0.1.0 instead. In practice this may not occur if Mumble versions are always ≥ 1.x, but the ambiguity is worth documenting.

Consider adding a two-argument variant decode(v1, Int) / decode(v2, Int) so callers can be explicit when the source format is known (as it is in protobuf messages where version_v1 and version_v2 are separate fields).

apps/erlmur/src/erlmur_server_handler.erl-78-86 (1)

78-86: ⚠️ Potential issue | 🟡 Minor

os_version is a string (list) while other fields are binaries — type inconsistency.

erlang:system_info(otp_release) returns a character list (e.g., "26"), but release and os are binaries. This may cause issues if downstream code expects all values to be binaries.

♻️ Proposed fix
-        os_version => erlang:system_info(otp_release)
+        os_version => list_to_binary(erlang:system_info(otp_release))
apps/mumble_protocol/src/mumble_server.erl-86-95 (1)

86-95: ⚠️ Potential issue | 🟡 Minor

os_version is a string (list) — should be binary for consistency.

erlang:system_info(otp_release) returns a character list (e.g., "27"), but all other string-like fields in the version record (release, os) are binaries. The same inconsistency appears in erlmur_server_handler:get_caps/1.

♻️ Proposed fix
-        os_version = erlang:system_info(otp_release)
+        os_version = list_to_binary(erlang:system_info(otp_release))
apps/erlmur/src/erlmur_user_manager.erl-81-86 (1)

81-86: ⚠️ Potential issue | 🟡 Minor

Username logged at info level — consider PII implications.

Line 85 logs the username in plain text. Depending on your compliance requirements, user-identifying information in logs may need to be masked or logged at debug level only.

apps/mumble_protocol/src/mumble_udp_server.erl-38-40 (1)

38-40: ⚠️ Potential issue | 🟡 Minor

send/3 spec promises {error, term()} but gen_server:cast/2 always returns ok.

The spec -> ok | {error, term()} is misleading. Since send/3 delegates to gen_server:cast, it will always return ok to the caller — any send error is handled asynchronously inside handle_cast. Either update the spec to -> ok or switch to gen_server:call if the caller needs error feedback.

🔧 Proposed fix (if async is intended)
--spec send(IP :: inet:ip_address(), inet:port_number(), binary()) -> ok | {error, term()}.
+-spec send(IP :: inet:ip_address(), inet:port_number(), binary()) -> ok.
apps/mumble_protocol/src/mumble_udp_server.erl-169-171 (1)

169-171: ⚠️ Potential issue | 🟡 Minor

get_port/0 spec returns inet:port_number() but the function returns {ok, Port}.

The handle_call(get_port, ...) at line 58-59 replies with {ok, Port}, so get_port/0 actually returns {ok, inet:port_number()}. The spec should match.

🔧 Proposed fix
--spec get_port() -> inet:port_number().
+-spec get_port() -> {ok, inet:port_number()}.
 get_port() ->
     gen_server:call(?MODULE, get_port).
apps/mumble_protocol/src/mumble_client_conn.erl-154-177 (1)

154-177: ⚠️ Potential issue | 🟡 Minor

Every received message logged at info level — will be noisy in production.

Line 162 logs every message type at info level. For an active connection receiving frequent Ping and voice data messages, this will flood logs. Downgrade to debug.

🔧 Proposed fix
     lists:foreach(fun(M) -> 
-        logger:info("Client received: ~p", [maps:get(message_type, M)]),
+        logger:debug("Client received: ~p", [maps:get(message_type, M)]),
apps/mumble_protocol/src/mumble_server_conn.erl-447-457 (1)

447-457: ⚠️ Potential issue | 🟡 Minor

send_msg logs every outgoing message at notice level.

Line 450 (logger:notice("Sending ~p (~p bytes)", ...)) runs for every message sent, including frequent Ping responses. This will clutter production logs. Downgrade to debug.

🔧 Proposed fix
-            logger:notice("Sending ~p (~p bytes)", [maps:get(message_type, Map), byte_size(Bin)]),
+            logger:debug("Sending ~p (~p bytes)", [maps:get(message_type, Map), byte_size(Bin)]),
apps/mumble_protocol/test/e2e_connection_SUITE.erl-40-44 (1)

40-44: ⚠️ Potential issue | 🟡 Minor

Test listeners may leak if a test crashes before ranch:stop_listener.

Each test manually calls ranch:stop_listener(Name) at the end, but if the test fails before reaching that line, the listener remains running. Use end_per_testcase to store and clean up the listener name.

♻️ Suggested approach

Store the listener name in the Config during init_per_testcase (or return it from the test setup), then stop it in end_per_testcase:

+end_per_testcase(_TC, Config) ->
+    case ?config(listener_name, Config) of
+        undefined -> ok;
+        Name -> catch ranch:stop_listener(Name)
+    end,
+    ok.
apps/mumble_protocol/test/mumble_SUITE.erl-238-240 (1)

238-240: ⚠️ Potential issue | 🟡 Minor

Brittle record field access via element/3.

element(3, State) assumes the port is at a fixed position in the #state{} record tuple. If the record definition in mumble_udp_server is reordered or fields are added before port, this silently reads the wrong value. Use sys:get_state/1 with a record pattern or export an accessor from mumble_udp_server instead.

apps/mumble_protocol/test/mumble_SUITE.erl-380-413 (1)

380-413: ⚠️ Potential issue | 🟡 Minor

auto_cert_reuse_test depends on side effects from the previous test.

This test assumes auto_cert_basic_test already ran and left cert files on disk (line 387: reads file info of the auto-generated cert). Because the group uses [sequence], this works in the happy path, but if auto_cert_basic_test fails or is skipped, this test will crash with a confusing {error, enoent} on file:read_file_info. Consider generating the cert explicitly in this test's setup or guarding with a file-existence check.

apps/mumble_protocol/include/mumble_protocol.hrl-85-85 (1)

85-85: ⚠️ Potential issue | 🟡 Minor

Remove or clarify the unused type field in the session record.

The type spec type = regular :: regular | type contains an ambiguous union alternative that shadows the Erlang keyword type. The field is not used anywhere in the codebase. Either remove this field entirely or, if it's intended for future use, replace the unclear type atom with a descriptive name (e.g., regular | special, regular | bot, or similar) that clarifies the semantic distinction.

🧹 Nitpick comments (25)
apps/ocb128_crypto/src/ocb128_crypto.erl (2)

1-18: Exported encrypt_ocb/3 and decrypt_ocb/3 lack -spec and -doc annotations.

These two functions are part of the public API (exported on lines 16–17) and are exercised in tests and property checks, yet they have no type specifications or documentation, unlike every other exported function in this module. Adding specs improves Dialyzer coverage and keeps the public surface consistent.

♻️ Proposed specs
+
+-doc """
+Encrypts a binary payload using AES-128 in OCB mode and returns a map
+containing the ciphertext and authentication tag.
+""".
+-spec encrypt_ocb(binary(), key(), nonce()) -> #{ciphertext := binary(), tag := binary()}.
 encrypt_ocb(Plain, Key, IV) ->
     ...
+
+-doc """
+Decrypts a binary ciphertext using AES-128 in OCB mode and returns a map
+containing the plaintext and authentication tag.
+""".
+-spec decrypt_ocb(binary(), key(), nonce()) -> #{text := binary(), tag := binary()}.
 decrypt_ocb(Encrypted, Key, IV) ->
     ...

423-458: Stats accounting for late looks correct but is subtle — consider a brief inline comment.

Line 444: #crypto_stats{lost = -1, late = 1} adjusts for a previously-counted lost packet that has now arrived late. The negative lost delta is correct (it reverts the earlier "lost" assumption), but this is non-obvious to future readers.

apps/ocb128_crypto/test/prop_ocb128_crypto.erl (1)

67-86: Verify that the LateDecryptedState re-binding on line 78 is intentional.

Line 68 binds LateDecryptedState via mapfoldl, and line 78 re-uses the same variable name in a foldl. In Erlang, this is a match assertion — it asserts the foldl result equals the mapfoldl accumulator. If the drop errors return an unmodified state (which they do per the current update_state_by_classification for drop), this assertion will pass. This is correct but subtle.

apps/ocb128_crypto/test/ocb128_crypto_benchmark_SUITE.erl (1)

18-22: Inconsistent indentation: tabs inside all/0, spaces elsewhere.

Lines 20–22 use tab indentation while the rest of the file uses spaces.

♻️ Proposed fix
 all() ->
     case os:getenv("RUN_BENCHMARKS") of
-			"true" -> maps:keys(test_params());
-			_ -> []
-		end.
+        "true" -> maps:keys(test_params());
+        _ -> []
+    end.
apps/ocb128_crypto/test/ocb128_crypto_tests.erl (1)

134-135: _State is used on the next line — drop the underscore prefix.

By Erlang convention, _Var signals the variable is intentionally unused. Referencing _State on line 135 contradicts that signal.

♻️ Proposed fix
-            fun({{ok, <<>>, _State}, I}) ->
-                Stats = ocb128_crypto:stats(_State),
+            fun({{ok, <<>>, DecState}, I}) ->
+                Stats = ocb128_crypto:stats(DecState),
apps/mumble_protocol/test/prop_protocol_tests.erl (1)

10-13: Placeholder property provides no value.

prop_tcp_proto_roundtrip/0 always returns true, which means it silently passes without testing anything. Consider either implementing it or removing it to avoid giving a false sense of coverage.

rebar.config (1)

40-45: Dependencies pinned to branch heads — consider using tags or commit SHAs.

Both ranch and eqwalizer_support are pinned to branch heads (master/main), which means builds are not reproducible and could break unexpectedly when upstream pushes breaking changes.

apps/mumble_protocol/docs/establishing_connection.md (1)

1-140: Well-written protocol documentation. Minor markdown formatting nits flagged by linter.

The content is technically thorough and the Mermaid sequence diagram is a great addition. Static analysis flagged multiple MD022 (blanks-around-headings) and MD031 (blanks-around-fences) violations throughout the file. Adding blank lines before and after headings and fenced code blocks would resolve these.

apps/mumble_protocol/test/mumble_client_api_tests.erl (1)

13-17: setup/0 and cleanup/1 are defined but unused.

These hooks are not referenced by the test generator. Either wire them into the fixture or remove them.

apps/mumble_protocol/test/client_logic_tests.erl (1)

407-423: network_error_test and ssl_close_test provide no meaningful coverage.

network_error_test only asserts that atoms are atoms. ssl_close_test pattern-matches a tuple it just constructed. Neither exercises any code in mumble_client_conn. Consider implementing real tests or marking them as TODO stubs explicitly.

apps/mumble_protocol/src/mumble_cert.erl (1)

102-104: Inconsistent indentation — mixed tabs and spaces.

Lines 103–104 and 121 use tab indentation while the rest of the file uses spaces. This hinders readability.

Proposed fix
     maybe
-			{ok,_} ?= {filelib:ensure_dir(KeyFile),KeyFile},
-			{ok,_} ?= {filelib:ensure_dir(CertFile),CertFile},
+        {ok, _} ?= {filelib:ensure_dir(KeyFile), KeyFile},
+        {ok, _} ?= {filelib:ensure_dir(CertFile), CertFile},
     else
         {false, Reason} -> 
             {error, {cert_generation_failed, string:trim(Reason)}};
-				{{error, Reason}, File} -> 
+        {{error, Reason}, File} -> 
             {error, {auto_cert_failed, Reason, File}}
     end.

Also applies to: 121-121

apps/mumble_protocol/test/mumble_api_tests.erl (1)

9-15: os:cmd("which openssl") may not return [] on all failure paths.

On some systems, which writes errors to stdout (e.g., "which: no openssl in ..."), so the check against [] would pass even when OpenSSL is absent. Consider checking the exit code or verifying the returned path exists.

♻️ Suggested improvement
 setup() ->
-    %% Ensure OpenSSL is available
-    case os:cmd("which openssl") of
-        [] -> error(openssl_not_available);
-        _ -> ok
-    end,
+    %% Ensure OpenSSL is available
+    Output = string:trim(os:cmd("which openssl")),
+    case Output of
+        "" -> error(openssl_not_available);
+        Path ->
+            case filelib:is_file(Path) of
+                true -> ok;
+                false -> error(openssl_not_available)
+            end
+    end,
     ok.
apps/mumble_protocol/test/mock_mumble_handler.erl (1)

15-31: Remove speculative comments; mock sends to self() which may not be the connection process.

Lines 23–26 contain uncertain comments about how mumble_server_conn works. In a mock, self() is the test process (or whatever calls handle_msg), not the connection pid. If this mock is only used in integration tests where the call originates from mumble_server_conn, then self() is correct — but if used elsewhere the send target will be wrong. Clean up the speculative comments regardless.

♻️ Clean up comments
         'TextMessage' ->
-            %% Reply with another text message
             Reply = #{
                 message_type => 'TextMessage',
                 message => <<"Echo: ", (maps:get(message, Msg))/binary>>
             },
-            %% We need to know who to send it to, but mock doesn't have session pid easily.
-            %% mumble_server_conn handles casting back if we return it? 
-            %% Actually mumble_server_conn:handle_protocol_msg calls Mod:handle_msg.
-            %% If we want to send something back, we'd typically use mumble_server_conn:send(self(), Reply),
             mumble_server_conn:send(self(), Reply),
             {ok, State};
apps/mumble_protocol/src/mumble_tcp_proto.erl (2)

137-144: find_msg_by_tag/1 always traverses the full list — use lists:keyfind/3 instead.

foldl cannot short-circuit: once the tag is found, it still iterates over remaining entries. lists:keyfind/3 (or lists:search/2 like find_msg_by_record/1 uses) stops on first match.

♻️ Proposed refactor
 find_msg_by_tag(Tag) ->
-    lists:foldl(fun ({T, Record}, _Acc) when T =:= Tag ->
-                        {ok, Tag, Record};
-                    (_, Acc) ->
-                        Acc
-                end,
-                error,
-                ?MESSAGE_TABLE).
+    case lists:keyfind(Tag, 1, ?MESSAGE_TABLE) of
+        {Tag, Record} -> {ok, Tag, Record};
+        false -> error
+    end.

98-108: Spec for pack/1 claims it always returns binary(), but it can throw.

Line 107 calls error({unknown_type, Record}) for unknown message types, yet the spec on line 98 declares -spec pack(map()) -> binary(). Either update the spec to include no_return() or return an error tuple.

♻️ Option: return error tuple instead of throwing
--spec pack(map()) -> binary().
+-spec pack(map()) -> binary() | {error, {unknown_type, term()}}.
 pack(MessageMap) when is_map(MessageMap) ->
     logger:debug("pack ~p", [MessageMap]),
     Record = mumble_msg:from_map(MessageMap),
     case find_msg_by_record(Record) of
         {ok, Tag, _} ->
             Bin = 'Mumble_gpb':encode_msg(Record),
             encode_message(Tag, Bin);
         error ->
-            error({unknown_type, Record})
+            {error, {unknown_type, Record}}
     end.
apps/mumble_protocol/src/mumble_msg.erl (1)

499-774: No catch-all clause in from_map/1 — unknown message_type gives an opaque function_clause error.

If from_map receives a map with an unrecognized message_type, the crash will be a generic function_clause that doesn't indicate which type was unexpected. A catch-all clause would improve debuggability.

♻️ Proposed addition at the end of from_map
from_map(#{message_type := Type}) ->
    error({unknown_message_type, Type});
from_map(Map) ->
    error({missing_message_type, Map}).
apps/mumble_protocol/src/mumble_server.erl (1)

113-130: Calling start_link outside a supervisor links the supervision tree to the caller.

mumble_server_sup:start_link(SupOptions) links the new supervisor to whichever process calls start_server/1. If that caller terminates, the entire supervision tree goes down. Consider using start_link only from a supervisor, or use supervisor:start_child to attach it under an existing supervision tree.

apps/erlmur/src/erlmur_user_manager.erl (2)

157-180: DOWN handler performs a full ETS table scan to find a user by PID.

ets:foldl iterates every entry even after finding the target. For a small user base this is fine, but it won't scale well. Consider maintaining a secondary pid → session_id ETS table (or ets:match_object/2 with a match spec on the pid field) for O(1) lookup.


107-115: unregister_user does not demonitor the process.

When a user is explicitly unregistered, the monitor set up during register_user (line 84) is never cancelled. The monitor ref isn't stored, so demonitor/1 can't be called. This means a subsequent DOWN message will still arrive and trigger the handle_info cleanup path (which will be a no-op since the user is already deleted). Not harmful, but slightly wasteful.

♻️ Suggested approach

Store the monitor reference in the #user{} record and call erlang:demonitor(MonRef, [flush]) in the unregister_user handler.

 -record(user, {
     session_id :: pos_integer(),
     pid :: pid(),
     username :: binary(),
-    udp_addr :: {inet:ip_address(), inet:port_number()} | undefined
+    udp_addr :: {inet:ip_address(), inet:port_number()} | undefined,
+    mon_ref :: reference() | undefined
 }).
apps/mumble_protocol/test/server_logic_tests.erl (1)

14-14: Unused ?TEST_TIMEOUT macro.

The macro is defined but never referenced. All timeouts in the test functions use hardcoded values. Either use it or remove it.

apps/mumble_protocol/src/mumble_client_conn.erl (1)

130-147: Stats field naming is misleading on the client side.

Fields like from_client_tcp_packets and from_client_tcp_bytes (lines 132-133) are being incremented when the client receives data from the server. The naming implies traffic originating from the client. Consider renaming to something directionally clear (e.g., received_tcp_packets / received_tcp_bytes) or documenting that the field names are reused from the shared #stats{} record.

apps/mumble_protocol/test/test_utils.erl (1)

85-95: wait_for_messages can pass a negative timeout to the recursive call.

If Elapsed > Timeout, the computed Timeout - Elapsed goes negative. While Erlang treats negative after values as 0, the _Pattern, 0 base case at line 85 won't match a negative value, causing one extra unnecessary receive call. Consider using max(0, Timeout - Elapsed).

♻️ Proposed fix
-            wait_for_messages(Pattern, Timeout - Elapsed, [Msg | Acc])
+            wait_for_messages(Pattern, max(0, Timeout - Elapsed), [Msg | Acc])
apps/mumble_protocol/test/mumble_SUITE.erl (2)

75-96: Fragile OpenSSL error detection.

Line 78: os:cmd("which openssl") returns "" on macOS/Linux when not found, but may include a trailing newline on some shells. On Windows it won't work at all. Line 94: only matching "Error" ++ _ misses other OpenSSL failure modes (e.g., exit codes, stderr-only output, or different error prefixes).

Consider using os:find_executable("openssl") for a more portable check, and verifying the generated cert files exist (which you already do at lines 99-100) as the definitive success signal rather than parsing stderr.

Suggested improvement
-    case os:cmd("which openssl") of
-        [] -> 
-            ct:fail(openssl_not_available);
-        _ -> 
-            ok
-    end,
+    case os:find_executable("openssl") of
+        false -> 
+            ct:fail(openssl_not_available);
+        _ -> 
+            ok
+    end,

518-576: Dead helper functions.

get_server_port/1 (hardcoded return), get_ranch_port/1 (unused), find_port_in_listeners/1 (only called by get_ranch_port), and wait_for_client_connection/2 (placeholder) are all unreferenced dead code. Remove them or mark with a TODO if they're intended for future use.

apps/mumble_protocol/test/mock_transport.erl (1)

87-88: gen_server:call timeout matches the recv timeout — potential premature timeout.

gen_server:call(Pid, {recv, Length, Timeout}, Timeout) uses the same Timeout for both the gen_server call envelope and the internal recv logic. If the internal receive (lines 180-185) consumes most of the timeout, the gen_server:call wrapper may also timeout, resulting in a confusing {error, timeout} from gen_server rather than the clean {error, timeout} reply. Consider adding a small buffer to the call timeout (e.g., Timeout + 500) or using infinity for the call timeout since the internal receive already handles the deadline.

Comment on lines +26 to +34
authenticate(#{message_type := 'Authenticate', username := Username}, State) ->
%% MVP: Accept all users unconditionally
{ok, SessionId} = erlmur_user_manager:register_user(self(), Username),
UserInfo = #{session_id => SessionId, username => Username},
logger:info("User ~s authenticated with session ~p", [Username, SessionId]),
{ok, UserInfo, State#state{session_id = SessionId, username = Username}};
authenticate(Msg, State) ->
logger:warning("Invalid authenticate message: ~p", [Msg]),
{error, invalid_auth, State}.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unhandled error from erlmur_user_manager:register_user/2 will crash the connection.

Line 28 uses {ok, SessionId} = erlmur_user_manager:register_user(self(), Username). If register_user returns {error, _} (e.g., duplicate user, manager down), the match fails and the process crashes with a badmatch. Handle the error case gracefully.

♻️ Proposed fix
 authenticate(#{message_type := 'Authenticate', username := Username}, State) ->
-    %% MVP: Accept all users unconditionally
-    {ok, SessionId} = erlmur_user_manager:register_user(self(), Username),
-    UserInfo = #{session_id => SessionId, username => Username},
-    logger:info("User ~s authenticated with session ~p", [Username, SessionId]),
-    {ok, UserInfo, State#state{session_id = SessionId, username = Username}};
+    %% MVP: Accept all users unconditionally
+    case erlmur_user_manager:register_user(self(), Username) of
+        {ok, SessionId} ->
+            UserInfo = #{session_id => SessionId, username => Username},
+            logger:info("User ~s authenticated with session ~p", [Username, SessionId]),
+            {ok, UserInfo, State#state{session_id = SessionId, username = Username}};
+        {error, Reason} ->
+            logger:error("Failed to register user ~s: ~p", [Username, Reason]),
+            {error, registration_failed, State}
+    end;
🤖 Prompt for AI Agents
In `@apps/erlmur/src/erlmur_server_handler.erl` around lines 26 - 34, The
authenticate/2 function currently does a destructive match on the call to
erlmur_user_manager:register_user(self(), Username) which will crash on
{error,_}; update authenticate/2 to handle both successful and error tuples from
erlmur_user_manager:register_user/2 instead of using a direct {ok, SessionId} =
... match: on {ok, SessionId} proceed to build UserInfo, log with logger:info
and return {ok, UserInfo, State#state{session_id = SessionId, username =
Username}}; on {error, Reason} log a warning or error with the Reason (using
logger:warning/2 or logger:error/2) and return a non-crashing error tuple such
as {error, {register_failed, Reason}, State} (or {error, invalid_auth, State} if
you prefer), ensuring you reference the authenticate/2 function,
erlmur_user_manager:register_user/2, SessionId, UserInfo and State#state when
making the change.

Comment on lines +42 to +56
-record(stats,
{server_ping = #ping{},
client_ping = #ping{},
udp_packets = 0 :: non_neg_integer(),
tcp_packets = 0 :: non_neg_integer(),
udp_ping_avg = 0 :: non_neg_integer(),
udp_ping_var = 0 :: non_neg_integer(),
tcp_ping_avg = 0 :: non_neg_integer(),
tcp_ping_var = 0 :: non_neg_integer(),
onlinesecs = 0 :: non_neg_integer(),
idlesecs = 0 :: non_neg_integer(),
from_client_tcp_packets = 0 :: non_neg_integer(),
from_client_udp_packets = 0 :: non_neg_integer(),
from_client_tcp_bytes = 0 :: non_neg_integer(),
from_client_udp_bytes = 0 :: non_neg_integer()}).
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

Mumble protocol ping statistics fields type specification

💡 Result:

In the Mumble TCP control protocol (protobuf), the Ping message carries the client/server ping statistics with the following field numbers and protobuf types:

Field # Name Protobuf type Meaning
1 timestamp uint64 Client timestamp (opaque; server echoes same value back)
2 good uint32 Count of “good” packets received
3 late uint32 Count of “late” packets received
4 lost uint32 Count of packets never received
5 resync uint32 Count of nonce resyncs
6 udp_packets uint32 Total UDP packets received
7 tcp_packets uint32 Total TCP packets received
8 udp_ping_avg float UDP ping average
9 udp_ping_var float UDP ping variance
10 tcp_ping_avg float TCP ping average
11 tcp_ping_var float TCP ping variance

Notes:

  • It’s defined as message Ping in Mumble.proto; all fields are optional in the proto2 schema, and the server replies echoing timestamp and providing its own stats. [1]
  • The same field numbering/types appear in generated bindings (e.g., Go), confirming the wire-format types (varint for the integers; fixed32 for the floats). [2]

Sources: [1] [2]


🏁 Script executed:

# Search for how these ping fields are used in the codebase
rg -i "udp_ping_avg|udp_ping_var|tcp_ping_avg|tcp_ping_var" --type erlang

Repository: freke/erlmur

Length of output: 2376


🏁 Script executed:

# Also check if there are any .proto files or documentation about the protocol
find . -type f \( -name "*.proto" -o -name "*mumble*" -o -name "*protocol*" \) | head -20

Repository: freke/erlmur

Length of output: 1004


🏁 Script executed:

# Check the Mumble.proto file to confirm field types
cat apps/mumble_protocol/proto/Mumble.proto | grep -A 20 "message Ping"

Repository: freke/erlmur

Length of output: 778


🏁 Script executed:

# Check mumble_msg.erl to see how these float values are decoded/assigned
sed -n '1,50p' apps/mumble_protocol/src/mumble_msg.erl

Repository: freke/erlmur

Length of output: 1365


🏁 Script executed:

# Look for the section handling Ping message decoding, particularly around the float fields
rg -A 5 -B 5 "udp_ping_avg|UdpAvg" apps/mumble_protocol/src/mumble_msg.erl | head -60

Repository: freke/erlmur

Length of output: 1843


Change ping average/variance fields from non_neg_integer() to float

The Mumble protocol specification (Mumble.proto in the repo and official spec) defines udp_ping_avg, udp_ping_var, tcp_ping_avg, and tcp_ping_var as float type in the Ping message. The current type annotation of non_neg_integer() contradicts the protocol definition and will cause type mismatches when the protobuf decoder (gpb) produces float values.

🤖 Prompt for AI Agents
In `@apps/mumble_protocol/include/mumble_protocol.hrl` around lines 42 - 56, The
record stats currently types udp_ping_avg, udp_ping_var, tcp_ping_avg and
tcp_ping_var as non_neg_integer(), which conflicts with the protobuf Ping
definition that emits floats; update the stats record (record name: stats) to
declare those four fields as float() and set their defaults to 0.0
(udp_ping_avg, udp_ping_var, tcp_ping_avg, tcp_ping_var) so the types match
gpb-decoded values and avoid runtime/type-mismatch issues.

Comment on lines +61 to +62
-callback authenticate(AuthMsg :: map(), State :: any()) ->
{ok, UserInfo :: any(), NewState :: any()} | {error, Reason :: any()}.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

authenticate/2 callback spec doesn't include a State return on error — mismatch with implementation.

The spec declares {error, Reason :: any()} (2-tuple), but erlmur_server_handler:authenticate/2 returns {error, invalid_auth, State} (3-tuple). This means either:

  • The caller cannot recover the handler state after a failed auth, or
  • The implementation violates the behaviour contract (Dialyzer should catch this).

If the state needs to be preserved on error, update the callback spec:

♻️ Proposed fix
 -callback authenticate(AuthMsg :: map(), State :: any()) ->
-    {ok, UserInfo :: any(), NewState :: any()} | {error, Reason :: any()}.
+    {ok, UserInfo :: any(), NewState :: any()} | {error, Reason :: any(), NewState :: any()}.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
-callback authenticate(AuthMsg :: map(), State :: any()) ->
{ok, UserInfo :: any(), NewState :: any()} | {error, Reason :: any()}.
-callback authenticate(AuthMsg :: map(), State :: any()) ->
{ok, UserInfo :: any(), NewState :: any()} | {error, Reason :: any(), NewState :: any()}.
🤖 Prompt for AI Agents
In `@apps/mumble_protocol/src/mumble_server_behaviour.erl` around lines 61 - 62,
The callback spec for authenticate/2 is incorrect: it currently lists an error
return as {error, Reason} but the implementation
erlmur_server_handler:authenticate/2 returns {error, invalid_auth, State};
update the behaviour spec to allow a 3-tuple error that includes the preserved
State (e.g. {error, Reason :: any(), State :: any()}) so the contract matches
the implementation and callers can recover state after failed auth.

Comment on lines +223 to +239
handle_cast({inject_message, Data}, State = #{owner := Owner, opts := Opts}) ->
NewQueue = case State of
#{message_queue := Queue} -> Queue ++ [Data];
_ -> [Data]
end,

%% Check if we should notify the owner (active mode)
case lists:keyfind(active, 1, Opts) of
{active, true} when Owner =/= undefined ->
Owner ! {ssl, self(), Data};
{active, once} when Owner =/= undefined ->
Owner ! {ssl, self(), Data};
_ ->
ok
end,

{noreply, State#{message_queue := NewQueue}};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Active-mode messages are enqueued but never dequeued — stale data in queue.

When inject_message arrives and active mode is enabled, the data is both sent to the owner via Owner ! {ssl, self(), Data} (line 232/234) and appended to message_queue (line 225). However, since it was already delivered, a subsequent recv/3 call will return the same data again from the queue. In active mode, the message should not be enqueued (or should be dequeued upon active delivery).

Suggested fix
 handle_cast({inject_message, Data}, State = #{owner := Owner, opts := Opts}) ->
-    NewQueue = case State of
-        #{message_queue := Queue} -> Queue ++ [Data];
-        _ -> [Data]
-    end,
-    
     %% Check if we should notify the owner (active mode)
     case lists:keyfind(active, 1, Opts) of
         {active, true} when Owner =/= undefined ->
-            Owner ! {ssl, self(), Data};
+            Owner ! {ssl, self(), Data},
+            {noreply, State};
         {active, once} when Owner =/= undefined ->
-            Owner ! {ssl, self(), Data};
+            Owner ! {ssl, self(), Data},
+            {noreply, State};
         _ ->
-            ok
-    end,
-    
-    {noreply, State#{message_queue := NewQueue}};
+            NewQueue = maps:get(message_queue, State, []) ++ [Data],
+            {noreply, State#{message_queue := NewQueue}}
+    end;
🤖 Prompt for AI Agents
In `@apps/mumble_protocol/test/mock_transport.erl` around lines 223 - 239,
handle_cast/2's inject_message currently both sends the message to Owner (active
mode) and appends it to message_queue, causing recv/3 to return duplicates;
change the logic in handle_cast({inject_message, Data}, State = #{owner :=
Owner, opts := Opts}) so that when Opts contains {active, true} or {active,
once} and Owner =/= undefined you do NOT append Data to message_queue (i.e., set
NewQueue = Queue unchanged or skip insertion), and only enqueue Data when not in
active mode; ensure State#{message_queue := NewQueue} still updates correctly
and that the active-once semantics (if any) are preserved for subsequent
deliveries.

Comment on lines +7 to +11
mumble_client_api_test_() ->
[
{"client start with invalid certs", fun client_start_invalid_certs/0},
{"client invalid ref handling", fun client_invalid_ref/0}
].
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Five test functions are defined but never executed.

The test generator mumble_client_api_test_/0 only includes client_start_invalid_certs and client_invalid_ref. The remaining functions — client_start_no_certs, client_send_message, client_send_voice, client_get_state, and client_stop (Lines 19, 31, 42, 53, 64) — are dead code that EUnit will never discover or run.

Either add them to the test generator or remove them to avoid false coverage confidence.

Proposed fix — include all tests
 mumble_client_api_test_() ->
     [
+        {"client start without certs", fun client_start_no_certs/0},
         {"client start with invalid certs", fun client_start_invalid_certs/0},
-        {"client invalid ref handling", fun client_invalid_ref/0}
+        {"client send message", fun client_send_message/0},
+        {"client send voice", fun client_send_voice/0},
+        {"client get state", fun client_get_state/0},
+        {"client stop and double-stop", fun client_stop/0},
+        {"client invalid ref handling", fun client_invalid_ref/0}
     ].
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
mumble_client_api_test_() ->
[
{"client start with invalid certs", fun client_start_invalid_certs/0},
{"client invalid ref handling", fun client_invalid_ref/0}
].
mumble_client_api_test_() ->
[
{"client start without certs", fun client_start_no_certs/0},
{"client start with invalid certs", fun client_start_invalid_certs/0},
{"client send message", fun client_send_message/0},
{"client send voice", fun client_send_voice/0},
{"client get state", fun client_get_state/0},
{"client stop and double-stop", fun client_stop/0},
{"client invalid ref handling", fun client_invalid_ref/0}
].
🤖 Prompt for AI Agents
In `@apps/mumble_protocol/test/mumble_client_api_tests.erl` around lines 7 - 11,
The test generator mumble_client_api_test_/0 currently only lists
client_start_invalid_certs/0 and client_invalid_ref/0 while the functions
client_start_no_certs/0, client_send_message/0, client_send_voice/0,
client_get_state/0, and client_stop/0 are defined but never executed; update
mumble_client_api_test_/0 to include all those test function references
(client_start_no_certs, client_send_message, client_send_voice,
client_get_state, client_stop) so EUnit will run them, or alternatively remove
the unused function definitions if they are intentionally deprecated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant