# Real Phone TCP Stack, Not an Emulation: What Your Proxy's Packets Say About It

> Markdown version of [the article](https://simplyproxies.com/blog/real-phone-tcp-stack-proxy-fingerprints.html), kept in sync with it.
> Published 21 October 2026 by Simply Proxies.

Anti-bot teams never trust what a client says about itself. The User-Agent header is a string anybody can set, and the TLS handshake that [websites actually fingerprint](/blog/why-you-get-403s-http-fingerprinting.html) can be borrowed from a real browser with the right client library. But there is a third layer that gets far less attention, and it is the hardest one to fake: the IP and TCP headers of the packets that physically arrive at the server. Those are produced by the kernel of the machine that sends them, not by anything your HTTP client configures.

This post is about what that layer reveals. We captured the opening packets of four different connections to one of our own servers, including two exits from our own UK mobile proxy fleet, and read them the way an anti-bot engineer would. The short version: a proxy exit that is a real phone does not need to imitate anything, and the details that prove it are exactly the details a pool of emulated exits gets wrong.

## What a SYN packet leaks

Every TCP connection starts with a SYN packet, and its headers are full of small facts set by the sender's operating system:

- **TTL (time to live).** Every IP packet carries a counter that the sender sets and that every router on the path decrements by at least one before forwarding ([RFC 791](https://www.rfc-editor.org/rfc/rfc791.txt)). If it reaches zero the packet is destroyed, which is how routing loops die. Linux, and therefore Android, starts packets at 64 by default: the kernel's [own documentation](https://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt) sets `ip_default_ttl` to 64, "as recommended by RFC 1700". Windows-class systems traditionally start at 128, a value p0f's own signature table still carries. A receiver who sees TTL 46 is looking at a packet that left a 64-class machine roughly 18 hops away.
- **MSS (maximum segment size).** The SYN announces the largest TCP payload the sender's path allows, which on IPv4 is the route's MTU minus 40 bytes of headers ([RFC 879](https://www.rfc-editor.org/rfc/rfc879.txt)). Your HTTP client cannot choose it; the kernel reads it from the route. A datacenter host on standard Ethernet advertises 1460 (MTU 1500). A phone on a cellular carrier path advertises something smaller, because the carrier's network carries packets that big or bigger only with extra overhead.
- **TCP options and their order.** Window scaling, SACK support and timestamps are kernel build decisions, and the order they appear in the SYN is a kernel signature in its own right. Two machines running different operating systems produce visibly different option lists; two phones from different manufacturers often do too.

None of these fields travel through your proxy client's configuration. Whatever machine's network stack actually emits the packets is what the destination sees.

## The experiment: four connections, one server

We pointed four different connections at a plain Ubuntu server we control, capturing every arriving SYN on port 80 with `tcpdump` and naming them with [`p0f`](https://lcamtuf.coredump.cx/p0f3/), the standard passive fingerprinting tool (version 3.09b), which reads the same fields and estimates the sender's operating system, hop distance and link MTU from a single packet.

The four sources:

1. a desktop Linux workstation on a UK fixed-line connection, connecting directly;
2. a second Ubuntu server in the same datacenter, connecting directly (the datacenter baseline);
3. through our SOCKS5 endpoint pinned to a device on the EE network, using the `-ee` username token;
4. the same, pinned to a device on the Three network (`-three`).

The proxied connections used the ordinary public endpoint, no different from any customer request:

    curl -x socks5h://USER-ee:PASS@proxy.simplyproxies.com:6890 http://<server>/

Each request exits from a different real device (the fleet rotates), so the exit IP is a fresh carrier address per run. An identity check on one EE-exit run, queried through ip-api.com, returned:

    {"country":"United Kingdom","isp":"British Telecommunications PLC",
     "org":"EE Limited","mobile":true,"query":"31.94.6.212"}

**Results:**

| Connection | p0f OS guess | Hop distance | MSS (implied MTU) | TCP window, scale | Options, in order |
|---|---|---|---|---|---|
| Workstation, direct | Linux 2.2.x-3.x | 16 | 1460 (1500) | 64240, wscale 7 | mss, sackOK, TS, nop, wscale |
| Datacenter server | Linux 2.2.x-3.x | 16 | 1460 (1500) | 64240, wscale 7 | mss, sackOK, TS, nop, wscale |
| Mobile exit, EE-hosted | Linux 2.2.x-3.x | 18 | 1362 (1402) | 65535, wscale 9 | mss, sackOK, TS, nop, wscale |
| Mobile exit, Three-hosted | FreeBSD | 21 | 1320 (1360) | 65535, wscale 12 | mss, nop, wscale, sackOK, TS |

p0f condenses each SYN into a raw signature. The EE exit's reads:

    raw_sig  = 4:46+18:0:1362:65535,9:mss,sok,ts,nop,ws:df,id+:0
    |            |  |       |    |    |   |
    |            |  |       |    |    |   +-- option list: mss, sackOK, timestamps, nop, window scale
    |            |  |       |    |    +------ window scale factor 9
    |            |  |       |    +----------- advertised window 65535
    |            |  |       +---------------- MSS 1362
    |            |  +------------------------ 18 hops travelled
    |            +--------------------------- TTL 46 on arrival
    +---------------------------------------- IPv4

## Reading the results like an anti-bot engineer

**All four connections are 64-class.** The workstation and the datacenter server arrived 16 hops out (TTL 48); the two phones arrived 18 and 21 hops out (TTL 46 and 43). That is consistent with every source being a Linux-family kernel starting at 64, which Android phones are. Nothing here pretends to be anything.

**The mobile exits advertise smaller segments.** MSS 1362 and 1320 imply path MTUs of 1402 and 1360, against 1500 for both fixed-line sources. That is the cellular path showing through: carrier backhaul and NAT44 equipment run smaller effective MTUs, and the phone's kernel scales its opening segment to match. No HTTP library decides this. It comes from the route, on the carrier's network, at connection time.

**The two phones do not match each other.** Different window scale factors (9 and 12) and a different option order. They are different physical devices with different kernel builds, on different carrier paths. A synthetic pool emulating "Android" tends to converge on one polished signature; a fleet of real devices drifts the way real traffic does.

**And a lesson in humility: p0f got one wrong.** It guessed FreeBSD for the Three-hosted exit. Its signature table is over a decade old, and kernels built since then can fall outside it entirely. Passive fingerprinting produces hints, not verdicts. That cuts both ways: it means a coherent real device can be misnamed, and it also means a well-built emulation can survive a casual look. Serious anti-bot systems weigh the whole stack, not one tool's guess.

## The purchased comparison: another provider's UK mobile pool

Fingerprinting your own exits is only half the story, so we ran the same session's checks against a competing product: a paid subscription to a commercial UK "mobile" proxy pool from another provider (kept unnamed; the point is the check, not the brand). The plan was the same packet capture, but their gateway refused the destination before any packet flew. A raw-IP target is rejected outright with a SOCKS5 "connection not allowed by ruleset" reply (code 2 in [the SOCKS5 specification](https://www.rfc-editor.org/rfc/rfc1928.txt)), so the exit can only be reached through hostnames, and the two hostnames under our own control were refused just as instantly. Long-established connectivity-check domains used in the same tests passed without trouble. Why specific hostnames are refused is the provider's policy, not something an outsider can inspect; the practical effect is stated plainly: no packet-layer capture against this pool was possible, so for this product the comparison rests on the layer anti-bot systems check first, what IP intelligence sees.

So for this pool the packet layer is off the table, and we compared at the layer anti-bot systems check first: what IP intelligence sees. Six rotating exits, each queried through ip-api.com (one cross-checked on ipinfo.io) in the same session as our own checks above:

| Exit | ASN | IP-intelligence classification |
|---|---|---|
| 86.147.174.211 | AS2856 British Telecommunications | mobile: false |
| 109.147.231.92 | AS2856 British Telecommunications | mobile: false |
| 82.43.185.13 | AS5089 Virgin Media | mobile: false |
| 82.35.120.116 | AS5089 Virgin Media | mobile: false |
| 86.191.212.69 | AS2856 British Telecommunications | mobile: false |
| 81.136.87.164 | AS2856 British Telecommunications | mobile: false (reverse DNS: `...btcentralplus.com`, a BT consumer range) |

Six for six UK fixed-line consumer ranges. Not one mobile-carrier ASN. Our own exits, checked through the same API minutes apart, returned `mobile: true` on every run, from carrier ASNs (Hutchison 3G, Telefonica UK, EE Limited).

What you make of that depends on your task. If a site greets fixed-line consumer IPs and mobile-carrier IPs the same way, nothing is lost. But the plan is sold as mobile, and a buyer running the industry-standard check sees `mobile: false` on every sample: the label did not survive the check. Whether those exits are phones parked on home Wi-Fi, recycled fixed-line hardware, or something else inside the pool, we cannot see, and we will not guess; what a buyer can verify from outside is exactly what we have quoted.

The honest caveats, stated plainly: one provider, one plan, one afternoon, six rotating samples, at one point in time. Pools change; another region or month might differ. But the asymmetry of effort is the part worth remembering. Checking took one API call per exit. Selling the story takes a fleet.

## Why this matters for proxies

The signal an anti-bot system really uses is **coherence**. Each layer of a connection is set by a different component: the HTTP library sets headers and (with the right library) the TLS fingerprint; the operating system sets TTL, MSS and the TCP option list; the network path sets the hop count and, through its MTU, the MSS. When a visitor claims one thing and their packets say another, the layers disagree.

A browser-impersonating scraper behind a datacenter proxy is the classic mismatch: the TLS handshake and User-Agent say Chrome on Windows, but the SYN says a Linux server kernel on a 1500-MTU network a few hops from a hosting company. Each piece is individually defensible; together they are a costume with the tag still on.

A real phone exit is the opposite case. The IP registry says mobile carrier, the MSS says a 1400-class cellular path, the TTL distance says many hops from a 64-class phone kernel, and the TLS and headers, if you run a mobile-profile client, say a phone too. Nothing is being imitated, so nothing can fall out of character.

The honest limit runs in both directions. A phone cannot pretend to be Windows, and no proxy can make it: the exit's packets are what they are. If your targets are mobile sites and apps, a phone exit is not a workaround, it is the genuine article. If you specifically need to look like a desktop on a fixed line, pick an exit that is one.

## Try it yourself

On any Linux server you control:

    # capture incoming SYNs (let the capture finish before reading the file)
    sudo tcpdump -i any -w /tmp/syn.pcap 'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0 and port 80'

    # read them back: TTL, window, options
    tcpdump -nn -vv -r /tmp/syn.pcap | head

    # or let p0f interpret: OS guess, hop distance, link MTU
    p0f -r /tmp/syn.pcap

Connect once directly and once through your proxy, then compare the two SYNs: TTL class and hop distance, MSS and implied MTU, window scale, option order. If your proxy's exit tells the same story as the IP it hands out, you are looking at a real machine. If the story contradicts the HTTP and TLS layers above it, you have found the seam.

## Summary

- Anti-bot systems read three layers: what you claim (headers), what your client handshake looks like (TLS), and what your packets physically say (IP/TCP). Only the last one is produced entirely by the machine at the other end.
- TTL reveals the sender's OS family and the hop distance. MSS reveals the path MTU: 1500-class for fixed lines, 1360-1400 for UK cellular paths. TCP options and their order are kernel signatures.
- We captured four connections to one server: two direct (desktop, datacenter) and two through real UK mobile proxy exits on EE and Three. The mobile exits arrived from 64-class kernels 18-21 hops out, advertising 1362/1320 MSS, each with its own kernel's option list.
- Coherence is the tell. A real phone is consistent from registry to path to packet headers; emulated exits must keep several independently-set layers in agreement, which is where they slip.
- A purchased UK "mobile" pool from another provider failed the basic registry check: six of six rotating exits classified as fixed-line consumer ranges (`mobile: false`), against `mobile: true` on every one of our exits checked the same way. The check costs one API call per exit.
- Fingerprinting is probabilistic: p0f itself misnamed one of our real phones. Weigh layers together rather than trusting any single tool.

Last verified 2026-10-21

