Files
DECNET/decnet/os_fingerprint.py
anti 082d3fec19 fix(os-fingerprint): Win timestamps ON + windows_server profile + OS_MANGLE schema
Win10/11 run TCP timestamps ON (nmap SEQ.TS=A); the windows profile had them
OFF, fingerprinting as an ancient stack. Add a windows_server slug (ECN
negotiated, CC=Y) and point the server/DC archetypes at it. Introduce the
OS_MANGLE map (per-slug egress SYN-ACK shape: window, option order, IP-ID
policy) consumed by the new cloak package.
2026-06-19 21:32:43 -04:00

180 lines
7.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPDX-License-Identifier: AGPL-3.0-or-later
"""
OS TCP/IP fingerprint profiles for DECNET deckies.
Maps an nmap OS family slug to a dict of Linux kernel sysctls that, when applied
to a container's network namespace, make its TCP/IP stack behaviour resemble the
claimed OS as closely as possible within the Linux kernel's constraints.
All sysctls listed here are network-namespace-scoped and safe to set per-container
without --privileged (beyond the NET_ADMIN capability already granted).
Primary discriminator leveraged by nmap: net.ipv4.ip_default_ttl (TTL)
Linux → 64
Windows → 128
BSD (FreeBSD/macOS)→ 64 (different TCP options, but same TTL as Linux)
Embedded / network → 255
Secondary discriminators (nmap OPS / WIN / ECN / T2T6 probe groups):
net.ipv4.tcp_syn_retries SYN retransmits before giving up
net.ipv4.tcp_timestamps TCP timestamp option (OPS probes); Windows = off
net.ipv4.tcp_window_scaling Window scale option; embedded/Cisco typically off
net.ipv4.tcp_sack Selective ACK option; absent on most embedded stacks
net.ipv4.tcp_ecn ECN negotiation; Linux offers (2), Windows off (0)
net.ipv4.ip_no_pmtu_disc DF bit in ICMP replies (IE probes); embedded on
net.ipv4.tcp_fin_timeout FIN_WAIT_2 seconds (T2T6 timing); Windows shorter
ICMP tuning (nmap IE / U1 probe groups):
net.ipv4.icmp_ratelimit Min ms between ICMP error replies; Windows = 0 (none)
net.ipv4.icmp_ratemask Bitmask of ICMP types subject to rate limiting
Note: net.core.rmem_default is a global (non-namespaced) sysctl and cannot be
set per-container without --privileged; TCP window size is already correct for
Windows (64240) from the kernel's default tcp_rmem settings.
"""
from __future__ import annotations
from dataclasses import dataclass
OS_SYSCTLS: dict[str, dict[str, str]] = {
"linux": {
"net.ipv4.ip_default_ttl": "64",
"net.ipv4.tcp_syn_retries": "6",
"net.ipv4.tcp_timestamps": "1",
"net.ipv4.tcp_window_scaling": "1",
"net.ipv4.tcp_sack": "1",
"net.ipv4.tcp_ecn": "2",
"net.ipv4.ip_no_pmtu_disc": "0",
"net.ipv4.tcp_fin_timeout": "60",
"net.ipv4.icmp_ratelimit": "1000",
"net.ipv4.icmp_ratemask": "6168",
},
"windows": {
# Windows 10/11 workstation. NOTE: modern Windows runs TCP timestamps
# ON (nmap SEQ.TS=A) — an earlier value of 0 here fingerprinted as an
# ancient Windows/Linux stack. ECN off → nmap ECN.CC=N (workstation).
"net.ipv4.ip_default_ttl": "128",
"net.ipv4.tcp_syn_retries": "2",
"net.ipv4.tcp_timestamps": "1",
"net.ipv4.tcp_window_scaling": "1",
"net.ipv4.tcp_sack": "1",
"net.ipv4.tcp_ecn": "0",
"net.ipv4.ip_no_pmtu_disc": "0",
"net.ipv4.tcp_fin_timeout": "30",
"net.ipv4.icmp_ratelimit": "0",
"net.ipv4.icmp_ratemask": "0",
},
"windows_server": {
# Windows Server 2016/2019. Same NT stack as the workstation; the only
# stack-visible deltas nmap reads are ECN negotiated (CC=Y → tcp_ecn=1)
# and randomized IP-ID (SEQ.TI=RD, applied by the cloak mangler, not a
# sysctl). Everything else == "windows".
"net.ipv4.ip_default_ttl": "128",
"net.ipv4.tcp_syn_retries": "2",
"net.ipv4.tcp_timestamps": "1",
"net.ipv4.tcp_window_scaling": "1",
"net.ipv4.tcp_sack": "1",
"net.ipv4.tcp_ecn": "1",
"net.ipv4.ip_no_pmtu_disc": "0",
"net.ipv4.tcp_fin_timeout": "30",
"net.ipv4.icmp_ratelimit": "0",
"net.ipv4.icmp_ratemask": "0",
},
"bsd": {
"net.ipv4.ip_default_ttl": "64",
"net.ipv4.tcp_syn_retries": "6",
"net.ipv4.tcp_timestamps": "1",
"net.ipv4.tcp_window_scaling": "1",
"net.ipv4.tcp_sack": "1",
"net.ipv4.tcp_ecn": "0",
"net.ipv4.ip_no_pmtu_disc": "0",
"net.ipv4.tcp_fin_timeout": "60",
"net.ipv4.icmp_ratelimit": "250",
"net.ipv4.icmp_ratemask": "6168",
},
"embedded": {
"net.ipv4.ip_default_ttl": "255",
"net.ipv4.tcp_syn_retries": "3",
"net.ipv4.tcp_timestamps": "0",
"net.ipv4.tcp_window_scaling": "0",
"net.ipv4.tcp_sack": "0",
"net.ipv4.tcp_ecn": "0",
"net.ipv4.ip_no_pmtu_disc": "1",
"net.ipv4.tcp_fin_timeout": "15",
"net.ipv4.icmp_ratelimit": "0",
"net.ipv4.icmp_ratemask": "0",
},
"cisco": {
"net.ipv4.ip_default_ttl": "255",
"net.ipv4.tcp_syn_retries": "2",
"net.ipv4.tcp_timestamps": "0",
"net.ipv4.tcp_window_scaling": "0",
"net.ipv4.tcp_sack": "0",
"net.ipv4.tcp_ecn": "0",
"net.ipv4.ip_no_pmtu_disc": "1",
"net.ipv4.tcp_fin_timeout": "15",
"net.ipv4.icmp_ratelimit": "0",
"net.ipv4.icmp_ratemask": "0",
},
}
_DEFAULT_OS = "linux"
_REQUIRED_SYSCTLS: frozenset[str] = frozenset(OS_SYSCTLS["linux"].keys())
def get_os_sysctls(nmap_os: str) -> dict[str, str]:
"""Return the sysctl dict for *nmap_os*. Falls back to Linux on unknown slugs."""
return dict(OS_SYSCTLS.get(nmap_os, OS_SYSCTLS[_DEFAULT_OS]))
def all_os_families() -> list[str]:
"""Return all registered nmap OS family slugs."""
return list(OS_SYSCTLS.keys())
# ─── Egress mangle profiles (cloak) ──────────────────────────────────────────
#
# sysctls above reach only GLOBAL fields (TTL, timestamps on/off, ECN). The
# SYN-ACK *shape* nmap also scores — exact window, TCP option order, IP-ID
# generation — cannot be set per-container by sysctl. The cloak mangler
# (decnet/cloak) rewrites those on egress, driven by these profiles, keyed by
# the SAME nmap_os slug. A slug ABSENT here needs no mangling (its real Linux
# stack already approximates the target, e.g. "linux"/"bsd").
@dataclass(frozen=True)
class MangleProfile:
"""How the cloak rewrites a decky's egress to match an nmap_os family."""
window: int # TCP advertised window on SYN-ACK
mss: int # MSS option value
wscale: int # window-scale shift
# Ordered TCP option layout to emit on SYN-ACK. "TS" is kept only if the
# kernel emitted a Timestamp (sysctl tcp_timestamps=1) so its live,
# incrementing value survives the rewrite (nmap SEQ.TS rate test).
option_order: tuple[str, ...]
ipid: str # "incr" (TI=I) | "random" (TI=RD) | "keep"
respond_t2t3: bool # synthesize Windows T2/T3 replies
_WIN_OPTS = ("MSS", "NOP", "WScale", "SAckOK", "TS")
OS_MANGLE: dict[str, MangleProfile] = {
"windows": MangleProfile(
window=0x2000, mss=1460, wscale=8,
option_order=_WIN_OPTS, ipid="incr", respond_t2t3=True,
),
"windows_server": MangleProfile(
window=0x2000, mss=1460, wscale=8,
option_order=_WIN_OPTS, ipid="random", respond_t2t3=True,
),
}
def get_os_mangle(nmap_os: str) -> MangleProfile | None:
"""Return the cloak mangle profile for *nmap_os*, or None if it needs none."""
return OS_MANGLE.get(nmap_os)