Packet Format
Every openme knock is a single UDP datagram of exactly 165 bytes. Packets of any other size are silently discarded by the server.
Outer Packet
0 1 33 45 101 165
┌───────┬──────┬───────┬─────────────────────┬─────────────────────┐
│version│ephem │ nonce │ ciphertext │ ed25519_sig │
│ 1 byte│pubkey│12bytes│ 56 bytes │ 64 bytes │
│ │32 byt│ │ (plaintext + tag) │ │
└───────┴──────┴───────┴─────────────────────┴─────────────────────┘
◄───────────────── signed portion (101 bytes) ────────────────────►
| Field | Offset | Size | Description |
|---|---|---|---|
version |
0 | 1 | Protocol version. Currently 0x01. |
ephemeral_pubkey |
1 | 32 | Client’s ephemeral Curve25519 public key for this knock. |
nonce |
33 | 12 | ChaCha20-Poly1305 AEAD nonce (random, per knock). |
ciphertext |
45 | 56 | Encrypted plaintext (40 bytes) + AEAD authentication tag (16 bytes). |
ed25519_sig |
101 | 64 | Ed25519 signature over bytes 0–100 (the signed portion). |
Total: 165 bytes.
The Ed25519 signature covers all fields except itself — bytes 0 through 100 inclusive. This prevents any field from being tampered with after signing.
Plaintext Payload
After successful AEAD decryption, the 40-byte plaintext contains:
0 8 24 40
┌───────────┬──────────┬──────────┐
│ timestamp │ random │target_ip │
│ 8 bytes │ nonce │ 16 bytes │
│ (int64 ns)│ 16 bytes │ (IPv6) │
└───────────┴──────────┴──────────┘
| Field | Offset | Size | Description |
|---|---|---|---|
timestamp |
0 | 8 | Unix nanoseconds (big-endian int64). Used for replay window check. |
random_nonce |
8 | 16 | 128 bits of CSPRNG output. Used for nonce uniqueness check. |
target_ip |
24 | 16 | IPv6 address (or IPv4-mapped IPv6) to open the firewall to. All-zeros = use source IP of knock packet. |
No Port Field
The plaintext contains no port number. Ports are determined entirely by the server’s per-client configuration (allowed_ports in the server config). This is a deliberate security decision: the client cannot request ports it is not authorised for, and the attack surface of the packet format is minimised.
IPv4 and IPv6
target_ip is always 16 bytes. IPv4 addresses are stored as IPv4-mapped IPv6 addresses (::ffff:a.b.c.d). All-zeros (::) means “use the source IP of the knock packet.”
Size Breakdown
| Component | Size |
|---|---|
version |
1 byte |
ephemeral_pubkey |
32 bytes |
nonce |
12 bytes |
timestamp (plaintext) |
8 bytes |
random_nonce (plaintext) |
16 bytes |
target_ip (plaintext) |
16 bytes |
| AEAD tag | 16 bytes |
ed25519_sig |
64 bytes |
| Total | 165 bytes |
Byte-Level Example
A valid knock from 192.168.1.10 requesting the firewall open for its own source IP:
Offset Bytes (hex) Field
000000 01 version = 1
000001 a3f1...b2 (32 bytes) ephemeral_pubkey
000021 c4d5e6f7a8b9 (12 bytes) nonce
00002d <56 bytes AEAD ciphertext+tag> ciphertext
000065 <64 bytes Ed25519 signature> ed25519_sig
Decrypted plaintext:
Offset Bytes (hex) Field
000000 0000018c4a3b2100 timestamp (ns since epoch)
000008 <16 random bytes> random_nonce
000018 00000000000000000000000000000000 target_ip = :: (wildcard → source IP)