404PrivacyDocs

eBPF packet path

Rose ships ttl_editor.o, a Linux eBPF classifier compiled from src/ebpf/ttl_editor.c. It runs at the traffic control (TC) egress hook on attached WSL2 interfaces. STATIC makes the upstream connection; the kernel then gives the classifier a chance to change selected fields of an outgoing packet. This is a separate part of the stack from STATIC’s HTTP, TLS, and JavaScript shaping.

Where the classifier sits

Windows browser
    │  local proxy request
    ▼
STATIC inside Rose ── creates upstream TCP/TLS connection
    │
    ▼  outgoing Linux packet
WSL2 eth* → TC clsact / egress → ttl_editor.o → WSL networking → route → website
                ▲                     │
                └── fingerprint_profiles[0] ← STATIC's active profile

The classifier sees egress packets on interfaces where it is attached. It does not change Windows browser packets before they reach STATIC, choose the exit route, or guarantee that a value will survive WSL/host networking and later hops. A website may see a lower TTL after intervening routers. Capture traffic on the actual path when you need to know what is visible outside the distro.

Attachment during startup

The fallback launcher mounts bpffs at /sys/fs/bpf, discovers active eth* interfaces (or uses EGRESS_IFACES), adds a clsact qdisc, and attaches the egress classifier with tc. It attempts to load and pin the program with bpftool, with an object-file attach path as a fallback. It then pins the map at /sys/fs/bpf/404/fingerprint_profiles and starts STATIC. The desktop path is documented as orchestrating its own startup; its app implementation could not be verified in this revision.

The attach and pin steps are best effort in the shell script. STATIC can be listening even if the classifier missed the routed interface or map synchronization failed. Rose includes bpftool and iproute2; the executable and object file are separate release artifacts. See Distribution anatomy and Build and release.

From JSON profile to kernel map

ebpf.rs converts the active, materialized profile into a C-compatible PacketProfile. It picks a platform default from profile_identity.platform (with a fingerprint OS fallback), then applies optional packet_profile overrides. Profile selection through the local API attempts another sync. On Linux the Rust process opens the pinned map and writes the profile to key 0 using BPF_MAP_UPDATE_ELEM.

profile JSON → materialized persona → PacketProfile
                                      │
                                      ▼
                         /sys/fs/bpf/404/fingerprint_profiles
                                      │ key 0
                                      ▼
                         TC classifier reads profile per packet

The map has one entry, so the packet program uses one active profile on that classifier, not a separate profile per browser connection. A failed sync logs a warning and does not stop the proxy. The C program contains a compiled fallback profile if map lookup fails, but a pinned array map can have a key 0 even before it has been populated with the intended values. Do not assume the compiled fallback is the active persona; inspect the map.

Packet fields and scope

FieldWhat the classifier doesWhen
IPv4Rewrites ToS and TTL; optionally randomizes IP ID when fragment offset is zero.Outgoing IPv4 packets on the attached interface.
IPv6Sets traffic class and hop limit; can randomize the flow label.Outgoing IPv6 packets with a directly readable base header.
TCP windowReplaces the advertised window value and updates its checksum.Outgoing TCP packets it can parse, including non-SYN packets.
TCP optionsReorders and fills selected MSS, window-scale, SACK, and timestamp options; may resize an option block.Initial SYN without ACK with an existing option block and valid bounds; other packets keep their option layout.
Protocol counterIncrements the indexed counter for recognized IP next-protocol values.Matching outgoing IPv4/IPv6 frames, whether or not a later rewrite succeeds.

This is Linux egress, not a general TCP implementation. The parser expects an Ethernet frame followed by IPv4 or a basic IPv6 header. The IPv6 TCP path checks the base header’s immediate nexthdr, so extension-header chains are not walked. Non-TCP traffic can receive IP-level edits but not TCP-specific edits. The classifier returns TC_ACT_OK on ordinary passes and many early exits; a counter or an attached filter does not prove every change was applied.

Reading a SYN on the wire

Here is the relationship between fields, not a literal capture or a promise about exact values:

Ethernet | IPv4: ToS · ID · TTL · checksum | TCP: ports · flags=SYN · window · checksum
         |                                 |      data offset → [MSS][NOP][WS][SACK]…
                                                   ↑ ordered by packet_profile.options

Ethernet | IPv6: traffic class · flow label · hop limit | TCP: flags=SYN · window
         |                                           |      data offset → options…

The options array in a profile names tokens in order: mss, nop, window_scale, sack_permitted, timestamp, and eol. Rust encodes them, records offsets for variable values, pads to a four-byte boundary, and rejects layouts exceeding TCP’s 40-byte options budget. Timestamp randomization requires a timestamp option. The classifier can change header length for a payload-free SYN, update the IP length and TCP pseudo-header length, and recompute relevant checksums. It skips option rewriting for a SYN with no existing options or malformed/unsupported lengths. Other fields may already have been edited on such a packet.

The Rust platform defaults include Windows TTL 128 and window 64240, macOS TTL 64 and window 65535, and Linux TTL 64 and window 65535; explicit profile settings can override them. Read Profile anatomy for how a persona is materialized. A chosen packet target does not itself establish on-wire parity.

Verify each boundary

  1. Attach: in the distro, check the routed interface and tc filter show dev <interface> egress. The classifier must be on the interface used by the upstream flow.
  2. Map: inspect the pinned map and entry 0 using scripts/inspect-ebpf-state.sh. Check that it reflects the currently selected profile.
  3. Wire: capture an outbound SYN on that interface with tcpdump -i <interface> -nnvv -Q out 'tcp[tcpflags] & tcp-syn != 0' and compare TTL/hop limit, window, MSS, scale, timestamps, and option order with the map. Offload and capture location affect what a local capture shows; use an external capture for claims about the final destination.

Changing a profile in STATIC and seeing /status succeed only confirms part of this chain. Missing privileges for BPF_OBJ_GET or BPF_MAP_UPDATE_ELEM, a missing pin, attach failure, wrong interface, or WSL forwarding can break packet fidelity independently. The eBPF implementation notes document the helper scripts and capabilities for manual Linux testing; the STATIC packet boundary describes the userspace handoff.