Current FNE runtime networking is split across multiple classes rather than a single `FNENetwork` implementation:
The `FNENetwork` class implements master-side functionality for managing connected peers:
- **TrafficNetwork**: Primary master-side traffic/control plane. Manages connected peers, routing, affiliations, ACL distribution, spanning tree state, HA state, and protocol call handlers.
- **MetadataNetwork**: Separate metadata/activity/diagnostic/status listener bound to `master.port + 1`, sharing the same peer/runtime context as `TrafficNetwork`.
- **PeerNetwork**: Upstream or neighbor FNE client link implementation used when this FNE also peers to other masters.
- **Peer Management**: Track connected peers, their capabilities, and metadata
- **Call Routing**: Route traffic between peers based on talkgroup affiliations
- **Access Control**: Whitelist/blacklist management for RIDs and talkgroups
At the application level, `HostFNE` owns `m_network` (`TrafficNetwork`), `m_mdNetwork` (`MetadataNetwork`), and `m_peerNetworks` (`PeerNetwork` map). Startup order is `createMasterNetwork()`, then `initializeRESTAPI()`, then `createPeerNetworks()`.
---
## 3. Network Protocol Layers
@ -291,7 +310,7 @@ The DVM network stack implements a layered protocol architecture:
- **Socket Operations**: Standard UDP datagram socket (IPv4/IPv6)
- `FNE_MAX_CONN` (Code 8): If received while in `NET_STAT_RUNNING` state, indicates the FNE is overloaded or shutting down. Peer should implement exponential backoff before reconnecting.
- `FNE_DUPLICATE_CONN` (Code 9): The peer transitions to `NET_STAT_WAITING_CONNECT`, clears `m_remotePeerId`, sets the duplicate-connection flag, reduces retry count thresholds, and uses the extended duplicate-connection reconnect delay (`DUPLICATE_CONN_RETRY_TIME`) to avoid rapid reconnect storms.
The RPTC configuration uses a nested JSON structure with three main object groups: `info` (system information), `channel` (RF parameters), and `rcon` (remote control/REST API).
The RPTC configuration uses a nested JSON structure with three main object groups: `info` (system information), `channel` (RF parameters), and `rcon` (remote control/REST API). Current peers also include the top-level `peerClass` field, which is the canonical way to describe the peer connection class.
```json
{
@ -916,13 +939,25 @@ The RPTC configuration uses a nested JSON structure with three main object group
"port": 0 // REST API port
},
"externalPeer": false, // External network peer flag (optional)
"peerClass": 2, // Canonical peer connection class enum
"externalPeer": false, // Legacy compatibility flag for neighbor peers
- **5 (`PEER_CONN_CLASS_INVALID`)**: Sentinel only. Values greater than or equal to this are treated as invalid and normalized to `PEER_CONN_CLASS_STANDARD` by the FNE.
**Example RPTC Configuration:**
```json
@ -956,9 +991,12 @@ The RPTC configuration uses a nested JSON structure with three main object group
"port": 9990
},
"peerClass": 2,
"externalPeer": false,
"conventionalPeer": false,
"sysView": false,
"consolePeer": false,
"software": "DVMHOST_R04A00"
}
```
@ -968,9 +1006,11 @@ The RPTC configuration uses a nested JSON structure with three main object group
**Top-Level Fields:**
- **identity**: Unique identifier for the peer (callsign, site name, etc.)
- **rxFrequency/txFrequency**: Operating frequencies in Hertz
- **externalPeer**: Indicates peer is outside the primary network (affects routing)
- **peerClass**: Canonical peer connection class. Current peers should send this field rather than relying on legacy boolean flags.
- **externalPeer**: Legacy compatibility flag indicating `PEER_CONN_CLASS_NEIGHBOR`
- **conventionalPeer**: Indicates non-trunked operation mode (affects grant behavior)
- **sysView**: Indicates monitoring-only peer (affiliation viewer, no traffic routing)
- **sysView**: Legacy compatibility flag indicating `PEER_CONN_CLASS_SYSVIEW`
- **consolePeer**: Reporting flag indicating `PEER_CONN_CLASS_CONSOLE`
- **software**: Software version string (e.g., `DVMHOST_R04A00`) for compatibility checking
**System Information Object (`info`):**
@ -998,10 +1038,13 @@ The RPTC configuration uses a nested JSON structure with three main object group
The FNE master stores the complete configuration JSON in the peer connection object (`FNEPeerConnection::config`) and extracts specific fields for connection management:
- `identity` → Used for peer identification in logs and routing tables
- `software` → Logged for version tracking and compatibility checks
- `sysView` → Determines if peer is monitoring-only (no traffic routing)
- `externalPeer` → Used for spanning tree routing decisions (external peers have special routing rules)
- `peerClass` → Primary peer classification used to determine routing and peer behavior
- `sysView` / `externalPeer` → Accepted for backward compatibility with older peers and translated into the corresponding `peerClass`
- `consolePeer` → Emitted by the FNE when reporting a console-class peer configuration
- `conventionalPeer` → Affects talkgroup affiliation and grant behavior (conventional peers don't require grants)
If `peerClass` is absent, the FNE falls back to legacy `sysView` and `externalPeer` booleans. If `peerClass` is `PEER_CONN_CLASS_UNKNOWN` or greater than or equal to `PEER_CONN_CLASS_INVALID`, the FNE normalizes it to `PEER_CONN_CLASS_STANDARD`. After parsing, the FNE reports the resolved class back through the stored configuration JSON and backfills the legacy boolean fields for compatibility with older REST API consumers.
**Step 4: ACK/NAK Response**
Master responds with ACK (success) or NAK (failure):
@ -1551,6 +1594,63 @@ The difference between **TDU** and **TDULC**:
- **TDU:** Simple terminator, no additional payload (24 bytes total)
- **TDULC:** Terminator with embedded link control (78 bytes total), provides complete call metadata at termination
**P25 Phase 2 (TDMA Voice/Data) Network Format:**
P25 Phase 2 traffic is carried under its own network subfunction, `NET_SUBFUNC::PROTOCOL_SUBFUNC_P25_P2 (0x03)`, rather than the classic `PROTOCOL_SUBFUNC_P25` path. `BaseNetwork::writeP25P2()` creates the encapsulated packet and `Network::clock()` handles it in a dedicated receive branch with separate per-slot receive stream tracking.
The encapsulated Phase 2 format is fixed-length:
| Offset | Length | Field | Description |
|--------|--------|-------|-------------|
| 0-23 | 24 | Message Header | Standard P25 network header created by `createP25_MessageHdr()` |
| 24-63 | 40 | P25 Phase 2 Frame Data | Raw Phase 2 payload (`P25_P2_FRAME_LENGTH_BYTES = 40U`) |
P25 Phase 2 is a separate transport lane in DVM documentation because it has its own subfunction code, receive buffer, reset path (`resetP25P2(slotNo)`), and per-slot stream tracking even though it reuses the generic P25 message header builder.
#### NXDN Message Format
NXDN frames use a fixed-length network format created by `createNXDN_Message()` in `BaseNetwork.cpp`. The frame size is 70 bytes as defined by `NXDN_PACKET_LENGTH = 70U`.
@ -1629,7 +1729,7 @@ The actual NXDN frame data (48 bytes) contains the over-the-air NXDN frame struc
#### Analog Message Format
Analog audio frames use G.711 μ-law encoding and are created by `createAnalog_Message()` in `BaseNetwork.cpp`. The frame size is 324 bytes as defined by `ANALOG_PACKET_LENGTH = 324U`.
Analog audio frames are created by `createAnalog_Message()` in `BaseNetwork.cpp`. The current network frame size is 344 bytes as defined by `ANALOG_PACKET_LENGTH = 344U`.
The analog audio packet uses the TAG_ANALOG_DATA identifier and contains 300 bytes of audio samples (calculated as 324 - 20 header - 4 trailer).
The analog audio packet uses the `TAG_ANALOG_DATA` identifier (`"ANOD"`) and contains 320 bytes of audio payload (`AUDIO_SAMPLES_LENGTH_BYTES = 320U`). The inline comments in current analog code describe this as 20 ms of audio at 8 kHz, while `analog::data::NetData` still notes that the audio buffer is expected to be MuLaw encoded. The network builder itself follows the 320-byte payload constant.
| Offset | Length | Field | Description |
|--------|--------|-------|-------------|
| 0-3 | 4 | Analog Tag | "ADIO" (0x4144494F) or similar ASCII identifier |
| 0-3 | 4 | Analog Tag | "ANOD" (`TAG_ANALOG_DATA`) |
| 4 | 1 | Sequence Number | Packet sequence number for audio ordering (0-255) |
| 5-7 | 3 | Source ID | Radio ID of transmitting station |
| 8-10 | 3 | Destination ID | Target radio ID or conference ID |
@ -1658,51 +1758,33 @@ The analog audio packet uses the TAG_ANALOG_DATA identifier and contains 300 byt
| 14 | 1 | Control Byte | Network control flags |
| 15 | 1 | Frame Type / Group | Bit 7: Group flag (0=private, 1=group)<br/>Bits 0-6: Audio frame type |
| 16-19 | 4 | Reserved | Reserved for future use (0x00000000) |
- **Implementation Note:**`AnalogDefines.h` describes this as 20 ms of 16-bit audio at 8 kHz; `NetData.h` describes the buffer passed into `setAudio()` as MuLaw encoded. The network packetizer currently uses the 320-byte constant and copies that payload verbatim.
G.711 μ-law Audio Sample Example (first 16 bytes of audio data shown):
FF FE FD FC FB FA F9 F8 F7 F6 F5 F4 F3 F2 F1 F0 ...
@ -1710,15 +1792,9 @@ FF FE FD FC FB FA F9 F8 F7 F6 F5 F4 F3 F2 F1 F0 ...
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴──┴─── Each byte is one 8kHz audio sample (μ-law encoded)
```
**G.711 μ-law Encoding:**
The current analog network payload is sized for 320 bytes per frame, with the packetizer copying the application-provided buffer directly into the on-wire message.
G.711 μ-law is a logarithmic audio compression standard that provides toll-quality voice at 64 kbit/s:
- **Range:** ±8159 linear PCM values compressed to 256 μ-law values (0x00-0xFF)
- **Mode**: ECB - each 16-byte block encrypted independently
- **Padding**: PKCS#7 padding for variable-length payloads
- **Padding**: Zero-padding to the AES block boundary
- **Packet Leader**: `0xC0FE` (`AES_WRAPPED_PCKT_MAGIC`) is prepended before encrypted payloads
**Runtime Behavior:** When preshared-key wrapping is enabled, receive logic drops datagrams that do not carry the `0xC0FE` leader. This is transport wrapping around the existing RTP/FNE payload, not SRTP.
**Note**: ECB mode is used for simplicity and performance in this implementation. While ECB has known cryptographic weaknesses (identical plaintext blocks produce identical ciphertext), the primary goal is to provide basic confidentiality for network traffic between trusted peers rather than military-grade security. For high-security deployments, additional network-layer security (IPsec, VPN) should be used.
Current FNE runtime uses a separate `MetadataNetwork` listener for activity log transfer, diagnostic transfer, status transfer, and packet-buffered replication metadata:
```cpp
class DiagNetwork : public BaseNetwork {
class MetadataNetwork : public BaseNetwork {
private:
FNENetwork* m_fneNetwork;
uint16_t m_port; // Separate diagnostic port
TrafficNetwork* m_trafficNetwork;
uint16_t m_port; // Bound to master port + 1
ThreadPool m_threadPool;
public:
void processNetwork(); // Process diagnostic packets
void processNetwork(); // Process metadata / transfer packets
};
```
`HostFNE::createMasterNetwork()` creates `TrafficNetwork` on the configured master port and `MetadataNetwork` on `master.port + 1`.
**Benefits:**
- Isolate diagnostic traffic from operational traffic
- Different QoS/priority handling
- Optional firewall rules
- Reduced congestion on main port
- Reduced congestion on main traffic port
### Network Statistics
@ -2329,7 +2410,7 @@ Key metrics tracked:
- BaseNetwork: ~20KB
- Network (Peer): ~50KB
- FNENetwork (Master): ~100KB base
- TrafficNetwork + MetadataNetwork (FNE runtime): ~100KB base before peer/session maps and call-handler state
- Per-peer state: ~1-2KB
- **Total for 100 peers: ~300-400MB**
@ -2461,16 +2542,12 @@ Peer A (Initiator) FNE Master Peer B (Recipient)
**Author:** AI Assistant (based on source code analysis)
**Version:** 1.2
**Date:** May 8, 2026
**Author:** AI Assistant (updated based on current source code analysis)
AI WARNING: This document was mainly generated using AI assistance. As such, there is the possibility of some error or inconsistency. Examples in Section 14 and Appendix C are *strictly* examples only for how the API *could* be used.
@ -33,9 +33,9 @@ The DVM (Digital Voice Modem) FNE (Fixed Network Equipment) REST API provides a
### 1.1 Base Configuration
**Default Ports:**
- HTTP: User-configurable (typically 9990)
- HTTPS: User-configurable (typically 9443)
**Default Port:**
- REST API listener: User-configurable via `system.restPort` (typically `9990`)
- The same configured port is used for HTTP or HTTPS depending on `system.restSsl`
**Transport:**
- Protocol: HTTP/1.1 or HTTPS
@ -44,7 +44,7 @@ The DVM (Digital Voice Modem) FNE (Fixed Network Equipment) REST API provides a
**SSL/TLS Support:**
- Optional HTTPS with certificate-based security
- Configurable via `keyFile` and `certFile` parameters
- Configurable via `restSslKey` and `restSslCertificate`
- Uses OpenSSL when `ENABLE_SSL` is defined
### 1.2 API Architecture
@ -52,8 +52,10 @@ The DVM (Digital Voice Modem) FNE (Fixed Network Equipment) REST API provides a
The REST API is built on:
- **Request Dispatcher:** Routes HTTP requests to appropriate handlers
- `400 Bad Request`: Invalid password, malformed auth string, or invalid characters
- `401 Unauthorized`: Authentication failed
- `400 Bad Request`: Invalid password, malformed auth field, empty auth string, auth longer than 64 characters, or invalid hex characters
**Notes:**
- Password must be pre-hashed with SHA-256 on client side
- The configured FNE password in YAML is plain text (`system.restPassword`); the server hashes it internally at startup and compares the client-supplied hex digest against that hash
- Token is a 64-bit unsigned integer represented as a string
- Tokens are invalidated when:
- Client authenticates again
- Server explicitly invalidates the token
- Server restarts
- Authentication tokens are bound to the request's `RemoteHost` value
- FNE truncates configured REST passwords longer than 64 characters before hashing
**Example (bash with curl):**
```bash
@ -311,6 +315,7 @@ Content-Type: application/json
```
**Notes:**
- Returns active connected peers plus any peer objects learned from upstream replica peer state
- Forces peer disconnect and requires re-authentication
- Useful for recovering from stuck connections
- Peer will need to complete RPTL/RPTK/RPTC sequence again
**Description:** Send a connection NAK to a currently known peer using its peer ID.
**Request Headers:**
```
X-DVM-Auth-Token: {token}
Content-Type: application/json
```
**Request Body:**
```json
{
"peerId": 10001,
"tag": "RPTL",
"reason": 6
}
```
**Request Fields:**
- `peerId` (required): Target peer identifier
- `tag` (required): Four-character or related network tag identifying the exchange being rejected, such as `RPTL`, `RPTK`, or `RPTC`
- `reason` (required): Numeric `NET_CONN_NAK_REASON` value
**Response:**
```json
{
"status": 200,
"message": "OK"
}
```
**Notes:**
- This endpoint sends a wire-level peer NAK through `TrafficNetwork::writePeerNAK()`
- Common reason codes include general failure, unauthorized, bad connection state, invalid config data, peer reset, peer ACL, max connections, and duplicate connection
- The handler validates only the JSON types; the caller must supply a semantically valid `tag` and `reason`
---
### 4.6 Endpoint: PUT /peer/nak/byAddress
**Method:** `PUT`
**Description:** Send a connection NAK to a peer by explicit network address/port rather than by current tracked connection object.
**Request Headers:**
```
X-DVM-Auth-Token: {token}
Content-Type: application/json
```
**Request Body:**
```json
{
"address": "192.168.1.100",
"port": 54321,
"peerId": 10001,
"tag": "RPTK",
"reason": 8
}
```
**Request Fields:**
- `address` (required): Destination IP address or hostname
- `port` (required): Destination UDP port
- `peerId` (required): Peer identifier to encode in the NAK
- `tag` (required): Network tag associated with the rejected exchange
- `reason` (required): Numeric `NET_CONN_NAK_REASON` value
**Response:**
```json
{
"status": 200,
"message": "OK"
}
```
**Notes:**
- The endpoint resolves the supplied address and port before sending the NAK
- Useful for forcing a specific rejection even when a full tracked peer object is not available locally
---
## 5. Radio ID (RID) Management
The Radio ID (RID) management endpoints allow dynamic modification of the radio ID whitelist/blacklist.
@ -1287,6 +1377,8 @@ X-DVM-Auth-Token: {token}
"lastPing": "Fri Dec 6 10:30:45 2025",
"pingsReceived": 1234,
"missedMetadataUpdates": 0,
"isConsole": false,
"isSysView": false,
"isNeighbor": false,
"isReplica": false
}
@ -1299,6 +1391,8 @@ X-DVM-Auth-Token: {token}
"cryptoKeyLastLoadTime": "Fri Dec 6 08:15:30 2025"
},
"totalCallsProcessed": 5678,
"totalCallCollisions": 12,
"totalActiveCalls": 1,
"ridTotalEntries": 150,
"tgTotalEntries": 45,
"peerListTotalEntries": 8,
@ -1317,6 +1411,8 @@ X-DVM-Auth-Token: {token}
- `lastPing`: Last ping timestamp (human-readable format)
- `pingsReceived`: Total pings received from this peer
- `missedMetadataUpdates`: Number of missed metadata updates
- `isConsole`: Whether this peer is classified as a console peer
- `isSysView`: Whether this peer is classified as a SysView peer
- `isNeighbor`: Whether this is a neighbor FNE peer
- `isReplica`: Whether this peer participates in replication
@ -1329,6 +1425,8 @@ X-DVM-Auth-Token: {token}
**Statistics Totals:**
- `totalCallsProcessed`: Total number of calls processed since FNE startup
- `totalCallCollisions`: Total number of tracked call collisions since FNE startup
- `totalActiveCalls`: Current internal active-call counter value
- `ridTotalEntries`: Total entries in radio ID lookup table
- `tgTotalEntries`: Total entries in talkgroup rules table
- `peerListTotalEntries`: Total entries in authorized peer list
@ -1344,7 +1442,106 @@ X-DVM-Auth-Token: {token}
---
### 9.7 Endpoint: GET /report-affiliations
### 9.7 Endpoint: GET /stat-reset-total-calls
**Method:** `GET`
**Description:** Reset the `totalCallsProcessed` counter exposed by `/stats`.
**Request Headers:**
```
X-DVM-Auth-Token: {token}
```
**Response:**
```json
{
"status": 200
}
```
**Notes:**
- Resets only the total-calls counter
- Does not restart the FNE or affect peer connectivity
---
### 9.8 Endpoint: GET /stat-reset-active-calls
**Method:** `GET`
**Description:** Reset the `totalActiveCalls` counter exposed by `/stats`.
**Request Headers:**
```
X-DVM-Auth-Token: {token}
```
**Response:**
```json
{
"status": 200
}
```
**Notes:**
- Resets the active-call counter maintained by `TrafficNetwork`
---
### 9.9 Endpoint: GET /stat-reset-call-collisions
**Method:** `GET`
**Description:** Reset the `totalCallCollisions` counter exposed by `/stats`.
**Request Headers:**
```
X-DVM-Auth-Token: {token}
```
**Response:**
```json
{
"status": 200
}
```
**Notes:**
- Resets the call-collision counter only
---
### 9.10 Endpoint: GET /report-unit-regs
**Method:** `GET`
**Description:** Return the current global unit registration table.
**Request Headers:**
```
X-DVM-Auth-Token: {token}
```
**Response:**
```json
{
"status": 200,
"totalUnits": 3,
"units": [123456, 234567, 345678]
}
```
**Response Fields:**
- `totalUnits`: Count of registered units returned
- `units[]`: Flat array of registered source IDs
**Notes:**
- Data is sourced from the global affiliation/unit registration table maintained by `TrafficNetwork`
---
### 9.11 Endpoint: GET /report-affiliations
**Method:** `GET`
@ -1359,6 +1556,7 @@ X-DVM-Auth-Token: {token}
```json
{
"status": 200,
"totalAffiliations": 2,
"affiliations": [
{
"peerId": 10001,
@ -1387,6 +1585,7 @@ X-DVM-Auth-Token: {token}
```
**Response Fields:**
- `totalAffiliations`: Count of peer affiliation groups returned by the API
- `affiliations[]`: Array of peer affiliation records
- `peerId`: Peer ID where affiliations are registered
- `affiliations[]`: Array of affiliation records for this peer
@ -1400,7 +1599,50 @@ X-DVM-Auth-Token: {token}
---
### 9.8 Endpoint: GET /spanning-tree
### 9.12 Endpoint: GET /report-grants
**Method:** `GET`
**Description:** Return the currently active talkgroup grant table.
**Request Headers:**
```
X-DVM-Auth-Token: {token}
```
**Response:**
```json
{
"status": 200,
"totalGrants": 2,
"grants": [
{
"srcId": 123456,
"dstId": 1,
"ssrc": 305419896
},
{
"srcId": 234567,
"dstId": 2,
"ssrc": 2271560481
}
]
}
```
**Response Fields:**
- `totalGrants`: Count of grant entries returned
- `grants[]`: Array of active grant records
- `srcId`: Current granted source radio ID
- `dstId`: Destination talkgroup or unit ID holding the grant
- `ssrc`: RTP SSRC associated with the active grant when present, otherwise `0`
**Notes:**
- Data is built from the global grant source-ID and SSRC tables
---
### 9.13 Endpoint: GET /spanning-tree
**Method:** `GET`
@ -1718,13 +1960,7 @@ Error responses include HTTP status code and JSON error object:
**Invalid Content-Type:**
When Content-Type is not `application/json`, the server returns a plain text error response:
```
HTTP/1.1 400 Bad Request
Content-Type: text/plain
Invalid Content-Type. Expected: application/json
```
When `Content-Type` is not `application/json`, the server returns an HTTP 400 status payload rather than processing the request body as JSON.
**Not a JSON Object:**
```json
@ -1762,21 +1998,19 @@ Examples of field validation errors: