Qeli 0.8.1 · PACKET_MUX_V1

Recordizer: batching and record boundaries

What PACKET_MUX_V1 does before encryption, how it is negotiated after AUTH, and how to safely configure batching, record size, fragmentation and reassembly.

Purpose

The recordizer changes the IP-packet-to-record mapping

In the legacy data plane, one inner IPv4/IPv6 packet usually became one encrypted record. That does not reveal content but creates a stable size/timing relationship. Negotiated PACKET_MUX_V1 works before encryption:

  • several inner packets can share one encrypted record;
  • one inner packet can span several records;
  • target size and batching deadline vary within configured bounds;
  • mux headers are inside AEAD and invisible on the wire.
This is not a transport. The recordizer works over TCP and UDP carriers. It does not turn fake-TLS into TLS, QUIC-shaped framing into real QUIC/H3, or hide the endpoint and aggregate timing/volume.
Layer order

Where it runs

data plane
inner IPv4/IPv6 packet
  → PACKET_MUX recordizer
  → padding / traffic normalization
  → PacketCodec encryption
  → carrier (plain / fake-TLS / Reality-H2 / obfs-WS / UDP QUIC shape)
  → carrier/path fragmentation if required

Target record size is a batching threshold, not a promise to pad every record to that size. A deadline may flush a smaller record. Padding and traffic normalization run after the recordizer.

Negotiation

The server owns the policy

obf.recordizer.* is not a client setting and is not added to qeli://. The server sends the policy in the authenticated AUTH contract; changes apply after reconnect.

PolicyBehaviorRollout
offLegacy mapping: one inner packet roughly matches one record.Rollback or troubleshooting.
preferEnables PACKET_MUX_V1 only when the client advertises it; a legacy client stays on the legacy plane.Recommended first stage and shipped-template default.
requiredRejects an incompatible client before address allocation.Only after the whole fleet is upgraded.
Server profile

Recommended 0.8.1 settings

server.conf · [profile:main]
obf.recordizer.policy = prefer
obf.recordizer.batch.delay_min_ms = 2
obf.recordizer.batch.delay_max_ms = 8
obf.recordizer.batch.max_packets = 16
obf.recordizer.batch.max_queue_bytes = 262144
obf.recordizer.record.max_payload_bytes = 0
obf.recordizer.record.small_min_ratio = 0.25
obf.recordizer.record.small_max_ratio = 0.875
obf.recordizer.record.full_probability = 0.72
obf.recordizer.fragment.enabled = true
obf.recordizer.fragment.reassembly_timeout_ms = 3000
obf.recordizer.fragment.max_inflight_packets = 64
obf.recordizer.fragment.max_reassembly_bytes = 4194304
obf.recordizer.fragment.max_fragments_per_packet = 64
Settings

Batching, size and reassembly

GroupKeysMeaning and limits
Deadlinedelay_min_ms
delay_max_ms
Random flush window started by the first queued packet. 0/0 sends immediately; max must not be below min.
Queuemax_packets
max_queue_bytes
Maximum mux frames and hard per-direction memory limit. Queue bytes: 64…4194304.
Payload ceilingmax_payload_bytes0 derives safe plaintext from active carrier/path; explicit values are 64…MAX_TUNNEL_MTU and remain budget-capped.
Morphologysmall_min_ratio
small_max_ratio
full_probability
Small target uses 0 < min ≤ max ≤ 1; probability is 0…1 and selects a full target.
Fragmentfragment.enabledAllows one inner packet to span records. Do not disable unless every packet is guaranteed to fit.
Reassemblyreassembly_timeout_ms
max_inflight_packets
max_reassembly_bytes
max_fragments_per_packet
Bound time, packet IDs, total memory and fragments per packet for each direction.
Network tuning

Three safe starting points

ScenarioChangesTrade-off
Latency-sensitivedelay=0…2 ms, max_packets=4Less batching latency, weaker regrouping.
Lossy UDPdelay=0…2 ms, max_packets=2, full_probability=0.85Less loss amplification from one large datagram.
Experimental morphologydelay=3…12 ms, full_probability=0.55More variability but greater latency/overhead; test first.
Combinations

Interaction with other features

  • Works with all TCP/UDP modes, IPv4/IPv6/dual stack and TUN/TAP. TAP strips/restores Ethernet headers at the edge; arbitrary L2 frames are not carried.
  • Reality/H2 batching and recordizer delay can add together. Change one dimension at a time.
  • Large UDP batches amplify datagram loss; prefer a short window and small max_packets.
  • AWG junk runs before handshake; the recordizer starts after AUTH. QUIC shape is still not real QUIC.
  • obf.fragmentation.* splits selected handshake/carrier writes and does not replace obf.recordizer.fragment.*.
  • UDP DATA_FRAG/PMTU is outer fragmentation after encryption; recordizer fragmentation splits the inner packet before PacketCodec.
Data plane · 0.8.1

Batching system calls does not create a new record format

After Recordizer datagrams are ready, Linux and Android send up to 32 items with one sendmmsg; already queued inbound datagrams are read with recvmmsg. There is no extra coalescing delay, and neither the wire format nor obf.recordizer.* settings change.

Same-window A/BBeforeAfterChange
Upload320.8 Mbit/s694.7 Mbit/s+116.5%
Download359.8 Mbit/s697.5 Mbit/s+93.9%
Scope: this is an alternating A/B on the specific two-core lab after receive batching was already present. It verifies removal of the measured syscall bottleneck, but does not replace the full 0.8.0 benchmark on the Technology page or guarantee identical speed on another device.
Rollout

Verification and rollback

  1. Upgrade the server, set policy=prefer, run qeli check-config and restart the profile.
  2. Reconnect the client and find Packet recordizer: PACKET_MUX_V1 active in logs.
  3. Test bidirectional IPv4/IPv6, DNS, reconnect/roaming, PMTU and sustained load on every used transport.
  4. Optionally move to required after the whole fleet is upgraded.
Rollback with obf.recordizer.policy = off, restart the profile/service and reconnect sessions. A running session does not change without reconnect.
Primary sources

Complete 0.8.1 recordizer guide