Files
Robovoice/Server/PROTOCOL.md
T

4.5 KiB
Raw Blame History

Robovoice DHCP Tunnel Protocol

Overview

Robovoice communicates with a remote STT server by tunneling through the DHCP UDP ports (68→67). This exploits a common killswitch exception: VPN software (e.g. WireGuard) blocks all traffic except DHCP, which is allowed for network connectivity maintenance.

[Robovoice client] --broadcast UDP :68→:67--> [STT server]
[Robovoice client] <--unicast UDP :67→:68-- [STT server]

The client broadcasts NOP heartbeats while PTT is held. The server starts recording on the first NOP and stops when it receives OFF or when 150ms pass with no NOPs.

Transport

  • Protocol: UDP (connectionless, unreliable)
  • Client → Server: broadcast, source port 68, dest port 67
  • Server → Client: unicast, source port 67, dest port 68
  • Client binds: to a specific LAN interface IP on port 68 (with SO_REUSEADDR to coexist with the Windows DHCP service)
  • No connection state — purely fire-and-forget datagrams

Wire format

All messages are plain text, newline-terminated (\n). Every message starts with the 6-byte magic HKMSTR to distinguish our traffic from real DHCP.

Client → Server

NOP (heartbeat while PTT held):

HKMSTR <nonce>\n

Sent every 50ms while PTT is held. The nonce is an incrementing unsigned integer that makes each datagram unique. The server discards it — it's purely for packet uniqueness, not for any protocol logic.

OFF (PTT released):

HKMSTR:OFF <nonce>\n

Sent once when PTT is released. This is the fast-stop signal. If lost, the 150ms timeout acts as a backstop.

Server → Client

Partial transcript:

HKMSTR:P <text>\n

Intermediate recognition result. Fire-and-forget. Client logs it but does not act on it.

Final transcript:

HKMSTR:F <text>\n

Complete utterance. Client feeds this to the TTS engine.

Server state machine

                    ┌──────────────────────────────────────────┐
                    │                                          │
                    ▼                                          │
              ┌──────────┐  first NOP   ┌──────────────┐       │
              │  IDLE    │ ──────────► │  RECORDING   │       │
              └──────────┘             └──────────────┘       │
                                           │      │           │
                                   OFF     │      │ 150ms     │
                                   recv'd  │      │ silence   │
                                           ▼      ▼           │
                                       ┌─────────────┐        │
                                       │  PROCESSING │        │
                                       └─────────────┘        │
                                            │                 │
                                     STT    │                 │
                                     done   │                 │
                                            ▼                 │
                                       send HKMSTR:F ─────────┘
  • IDLE → RECORDING: first NOP received, start mic capture
  • RECORDING → PROCESSING: OFF received, OR 150ms since last NOP
  • PROCESSING → IDLE: STT done, send HKMSTR:F <text>

Timing

Parameter Value Purpose
NOP interval 50ms Heartbeat frequency while PTT held
Silence timeout 150ms Stop recording if no NOPs (3 missed = lost OFF)
NOP bandwidth ~20 msg/s × ~20 bytes ~400 bytes/s — negligible

Why this works

  1. Outbound broadcast :68→:67 to 255.255.255.255 passes the WireGuard WFP killswitch (DHCP exception matches this exact pattern)
  2. Inbound :67→:68 has no address restriction in the WFP rule, so unicast replies pass through
  3. Binding to a specific interface IP (not 0.0.0.0) wins unicast delivery over the Windows DHCP client service
  4. NOP spam ensures the ON message gets through even at 5% packet loss (3 consecutive NOPs = ~0.01% drop probability)
  5. 150ms timeout is the backstop for lost OFF — at 50ms intervals, 3 consecutive NOPs must all be lost to false-stop