Skip to main content
Every “confirmed live via a real packet capture” claim in Live validation against production and elsewhere in this dossier was produced by the same four-step pipeline. This page is that pipeline, written down once, so reproducing or extending any of those findings doesn’t require re-deriving it from scratch. It works at all because the SFS2X game socket is plain TCP with no TLS. Capturing it is exactly as simple as capturing any other unencrypted traffic, no certificate pinning to defeat, no interception proxy, no MITM setup. tcpdump against the real app while it plays through the same network interface as the analysis machine is sufficient.

Step 1: capture

Run tcpdump against the known game/CDN hosts and the SFS2X port, then launch and use the real app (/Applications/Last War.app on macOS, or the equivalent iOS/Android install) while it’s running:
The host list is every server IP this project has seen the client actually talk to (GSL HTTP endpoints, CDN, and SFS2X game servers); the port list catches every SFS2X game port seen across captures so far. Keep the filter broad; a capture with some irrelevant traffic in it costs nothing at the decode step, but a filter that’s too narrow silently drops the packets you actually need.
Why the host list has held up better than the port list. An account can get moved to a different zone server-side, confirmed live via a real server merge that moved this project’s account from zone APS783 to APS8092 with an entirely different port (1778325092) mid-session (see Live validation against production for the client-side fix this required). The port is zone-specific and will drift every time this happens, there’s no way to keep it current in a static list. The host IPs above turned out to be shared CDN/infra-level addresses rather than zone-specific ones: the post-merge zone’s actual game-server host (lastwar-game-cf.lastwarapp.net) resolved to 172.65.210.24, already on this list. Since the filter is a flat OR, host 172.65.210.24 alone captured the new zone’s traffic on its new port without the port list needing to know about it. That’s not guaranteed to hold forever, if you’re capturing after your own account has moved zones and traffic isn’t showing up, re-resolve the current game-server hostnames (they’re in a login response’s serverInfo field, or already-resolved in your session config after a redirect) and add any genuinely new IP to the host list.
Give it real time. A rushed capture is the single most common failure mode: the real client’s own SFS2X connection can churn through its login/bootstrap burst in under a second, so if you start tcpdump, immediately open the app, do one thing, and stop, you’ll likely capture only the bootstrap burst and miss the actual action you were trying to observe. Let the app fully settle into its main screen (10–15 seconds after login) before doing anything you actually want captured, and let it sit a few seconds after before closing.

Step 2: find the game socket

A single capture typically contains several TCP streams: the real game session, a handful of short-lived “losing” connection attempts (the client races several GSL-provided candidate hosts and keeps only one), and unrelated TLS side-channels (chat, logging, CDN). The pcap tool lists them, decodable plain streams first, then encrypted tls ones:
The game socket is the plain stream to the game server’s host and port, port 17783, or whatever the current zone’s port has drifted to (see the port note above). Note its index (1 above). That’s all you need: the tool auto-detects the client side (the connection initiator, or the private-address endpoint) and both directions, so there’s no separate stream-index or client-IP lookup to do, and it reads both classic pcap and pcapng, with no tshark in the loop.
If the game-socket stream has only a handful of segments and well under a second of activity, the capture likely caught only a “losing” race candidate or missed the real session, see Step 1’s timing note. Re-capture with more time around the action.

Step 3: reassemble and decode

One command reassembles the chosen stream by TCP sequence number (so out-of-order and retransmitted segments land correctly, rather than corrupting the stream the way arrival-order reassembly can) and decodes every SFS2X packet in both directions:
To keep the raw reassembled bytes (e.g. to re-run the client’s own -decode-stream), use -out instead of -decode:
Pass -client <ip> to pcap to override the auto-detected client side if the heuristic ever picks wrong.
The whole pipeline is one Go module, so nothing can drift out of sync. Both the reassembler (internal/pcap) and the decoder are pure Go with no external dependencies; pcap -decode and -decode-stream frame and decode using the exact same ReadPacket (packet.go) and DecodeObject (sfsobject.go) the client uses on its own live connection. Any fix or extension to the real codec is reflected the next time you decode a capture, with no manual sync step.

Reading the output

Each decoded line is one SFS2X extension-call envelope: c is the SFSObject’s top-level wrapper (1 = extension call, 0 = system/heartbeat), a is the sub-action, and p holds the actual payload; for extension calls, p.c is the command name and p.p is its params object. Client→server calls almost always carry _id, an ever-incrementing per-connection request id; the matching server response echoes the same _id. See Wire protocol for the full packet-envelope and SFSObject binary format this output is decoded from, and Command reference for what a given p.c command name actually does.

Known limitation: some captures stop decoding partway through

Server→client decoding has, on several real captures to date, hit either a DecodeObject error: expected top-level tag 18 (SFS_OBJECT), got ... or a zstd decode: ... error, most often right around the same point in the bootstrap-burst response sequence, but not always at an identical byte offset. Client→server decoding has not shown this problem in any capture so far. These two error kinds behave differently, and only one of them actually stops the decode. A DecodeObject error is non-fatal: -decode-stream logs it inline for that one packet and continues decoding every packet that follows in the stream (see decode_test.go’s “DecodeObject error on one packet continues to the next” regression test). A zstd decode error, by contrast, comes from a lower layer (packet framing/decompression, not the SFSObject decoder) and is fatal, it halts the rest of the stream, so only what was decoded before that point is usable. The working theory was always that this is a reassembly edge case (a reordering/overlap pattern) rather than a decoder bug, since -decode-stream uses the identical codec the live Go client relies on and that connection has never shown equivalent corruption. Replacing the old tshark-fed Python reassembler with the pure-Go internal/pcap (Step 3 above) supports that theory: on a real August-2026 capture whose server→client stream the old path decoded only partway, the Go reassembler decoded the entire stream cleanly, every byte consumed as valid framed packets, ~35 more server→client packets than the old path recovered. The Go reassembler places each segment by its exact sequence offset (rather than relying on tshark’s own stream-reassembly opinion), which is what makes it more robust to the out-of-order/retransmit patterns that tripped the old pipeline. It’s plausible this closes the limitation outright, but it hasn’t been re-tested across enough captures to declare it gone. If a decode still stops partway: a DecodeObject error is isolated (that one packet is skipped, everything before and after is valid), enough to confirm a client→server request’s exact shape even when its matching response is the corrupt one; confirming that one response instead means replaying the request live through the Go client (e.g. via -interactive) and reading its response directly. A zstd decode error is fatal to the rest of the stream, and the same live-replay approach then applies to everything after the cutoff.