Overview
Signals visible at the TCP/IP and transport layer, before any HTTP or JS execution.
1. TCP/IP Stack Fingerprint (JA4T / Satori / p0f)
Mechanism
Passive OS fingerprinting reads the TCP SYN packet to identify the operating system. Each OS TCP/IP stack has a unique combination of:
- Initial TTL: 64 (Linux/macOS), 128 (Windows), 255 (Some UNIX)
- Window size: 64240 (Chrome/Linux), 65535 (macOS), 8192 (older systems)
- MSS (Maximum Segment Size): Derived from MTU
- TCP options: Order and selection of MSS, SACK_PERM, TIMESTAMP, NOP, WINDOW_SCALE
- IP flags: DF (Don't Fragment) bit behavior
Formats
JA4T: 64240_2-4-8-1-3_1452_10
| Component | Meaning | Example |
|---|---|---|
| 64240 | TCP Window Size | Windows default |
| 2-4-8-1-3 | TCP option order (MSS, SACK, TIMESTAMP, NOP, WINDOW) | Chrome/Linux order |
| 1452 | MSS | Derived from MTU |
| 10 | Window Scale shift count | 2^10 = 1024 multiplier |
Satori fingerprint: 64240:64:1:60:M1452,S,T,N,W10:.
OS Detection
| OS | Window | TTL | MSS | Options |
|---|---|---|---|---|
| Linux | 29200-65535 | 64 | 1460 | M,S,T,N,W(7-8) |
| Windows 10/11 | 64240 | 128 | 1460 | M,S,T,N,W(10) |
| macOS | 65535 | 64 | 1460 | M,S,T,N,W(6-7) |
| Android | 29200-65535 | 64 | 1460 | M,S,T,N,W(7-8) |
Impact on Obscura
Client Obscura Container (TCP termination) Internet
Client TCP stack Container TCP stack
(visible to Obscura) (visible to destination)
The destination server sees Obscura's TCP stack, not the client's. This is unavoidable because the container terminates the client TCP connection.
Mitigation: None at TCP level. However, most fingerprinting services focus on application-level fingerprints (TLS, HTTP) rather than TCP-level. TCP fingerprint alone rarely identifies a user.
2. TLS Fingerprint (JA3 / JA4)
See tls-fingerprinting.md for full details.
Key points for network layer:
- TLS handshake is the most important network-level signal
- Fully controllable via
utls(Go) or custom Rust TLS - Critical for Obscura must spoof or the proxy is revealed
3. HTTP/2 Fingerprint
Mechanism
HTTP/2 connection preface and initial frames vary by implementation.
Signals:
- SETTINGS frame parameters (order matters):
SETTINGS_HEADER_TABLE_SIZESETTINGS_ENABLE_PUSHSETTINGS_MAX_CONCURRENT_STREAMSSETTINGS_INITIAL_WINDOW_SIZESETTINGS_MAX_FRAME_SIZESETTINGS_MAX_HEADER_LIST_SIZE
- WINDOW_UPDATE initial frame
- PING behavior
- PRIORITY frame structure
Browser Examples
| Browser | Initial Window | Max Concurrent | Header Table Size |
|---|---|---|---|
| Chrome | 6291456 | 1000 | 65536 |
| Firefox | 131072 | 100 | 65536 |
| Safari | 1048576 | 100 | 65536 |
Impact on Obscura
If using Go's net/http for HTTP/2, the server sees Go's HTTP/2 fingerprint. Must implement custom HTTP/2 or use a library that allows SETTINGS override.
4. QUIC / HTTP/3 Fingerprint
Problem
HTTP/3 runs over QUIC (UDP). QUIC connections are encrypted end-to-end including the transport layer:
Client QUIC (UDP 443) Server
Encrypted Encrypted
(cannot inspect) (cannot inspect)
QUIC transport parameters, connection IDs, and version information are visible in the initial handshake, but the proxy cannot modify them.
Strategy
Block UDP 443 to force fallback to HTTP/2 or HTTP/1.1 over TCP:
iptables -A FORWARD -p udp --dport 443 -j DROP
This prevents QUIC connections from bypassing the proxy.
Trade-off: Slightly higher latency (TCP vs QUIC), no 0-RTT, no connection migration.
5. Timing-Based Fingerprints
5.1 Performance Timing
performance.now() // High-resolution timestamp in ms (sub-millisecond precision)
performance.timeOrigin
performance.getEntriesByType('navigation')
Mitigation: Round performance.now() to 100ms resolution via JS injection:
const originalNow = performance.now.bind(performance)
performance.now = () => Math.round(originalNow() / 100) * 100
5.2 Clock Skew
Servers can measure clock skew by comparing Date.now() values across requests.
Mitigation: Round Date.now() resolution.
5.3 Network Timing via Resource Loading
Sites measure latency by timing how long resources take to load:
const start = performance.now()
fetch('/image.png').then(() => {
const rtt = performance.now() - start
})
Mitigation: Cannot fully prevent the proxy adds its own latency but the relative timing still leaks information.
6. WebRTC IP Leak
Mechanism
WebRTC bypasses the proxy by using STUN/TURN servers to discover the real IP.
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }]
})
pc.createDataChannel('test')
pc.createOffer().then(d => pc.setLocalDescription(d))
pc.onicecandidate = e => {
if (e.candidate) {
// e.candidate.candidate contains IP address
console.log(e.candidate.candidate)
}
}
Mitigation via JS Injection
// Block WebRTC completely
Object.defineProperty(window, 'RTCPeerConnection', { value: undefined })
Object.defineProperty(window, 'RTCDataChannel', { value: undefined })
Object.defineProperty(window, 'RTCSessionDescription', { value: undefined })
Effect
| Setting | WebRTC Leak | Detection |
|---|---|---|
| WebRTC available | IP leaks | Not detected |
| WebRTC blocked | No leak | Detectable (feature absence) |
7. DNS Fingerprint
Signals
- DNS resolver IP (Cloudflare 1.1.1.1 vs Google 8.8.8.8 vs ISP)
- DNS over HTTPS vs DNS over TLS vs plain DNS
- EDNS0 client subnet (reveals approximate network location)
- DNSSEC support
Mitigation
Obscura controls DNS completely. Use a consistent, privacy-focused upstream (Quad9 9.9.9.9 or Cloudflare 1.1.1.1) and strip EDNS0 client subnet.
Conclusions for Obscura
| Vector | Controllable | Priority | Notes |
|---|---|---|---|
| TCP/IP fingerprint | No (inherited from container) | Low | Low entropy, rarely used alone |
| TLS fingerprint | Yes (via utls) | Critical | Must spoof or proxy is revealed |
| HTTP/2 fingerprint | Partial | Medium | Requires custom HTTP/2 impl |
| QUIC/HTTP/3 | Block or inherit | Medium | Block UDP 443 |
| Timing attacks | Partial | Low | Round performance.now() |
| WebRTC IP leak | Yes (block via JS) | High | Critical for VPN users |
| DNS fingerprint | Yes | High | Obscura controls DNS |