Seregon/zftpd

Zero-copy FTP/HTTP Daemon compatible with all POSIX systems

C/11.0 KB/No license
include/ps5_net_filter.h
zftpd / include / ps5_net_filter.h
1/*
2 * MIT License — Copyright (c) 2026 SeregonWar
3 * See LICENSE for full text.
4 */
5 
6/**
7 * @file ps5_net_filter.h
8 * @brief PS5 kernel-level outbound connection filter
9 *
10 * PROBLEM AD DRESSED
11 * -----------------
12 * Sony system daemons (ScePatchChecker, PFAuthClient, SceConsoleFeatureFlagChecker,
13 * SceHidConfigService, SceWorkaroundCtl, RNPS Curl) enter aggressive retry loops
14 * when DNS blocks their servers. Each failed attempt still generates outgoing
15 * packets, saturating the bandwidth available to FTP transfers by up to 40%.
16 *
17 * ROOT CAUSE
18 * ----------
19 * DNS-level blocking returns NXDOMAIN or timeout, which the daemons treat as a
20 * *transient* network failure and retry with exponential back-off that saturates
21 * the wire. The kernel itself never learns that the connection should be rejected.
22 *
23 * SOLUTION
24 * --------
25 * Intercept connect(2) at the kernel sysent table level. When a non-zftpd process
26 * attempts to reach a destination outside the local subnet, return ENETUNREACH
27 * immediately. The daemon interprets this as a *local* error and either backs off
28 * aggressively or stops retrying altogether — no packet is ever generated.
29 *
30 *
31 * ARCHITECTURE OVERVIEW
32 * ---------------------
33 *
34 * ┌──────────────────────────────────────────────────────────────┐
35 * │ Userland (zftpd) │
36 * │ │
37 * │ ps5_net_filter_install() │
38 * │ │ │
39 * │ ├─ [1] Locate sysent[SYS_CONNECT] via firmware table │
40 * │ ├─ [2] Save original sy_call pointer │
41 * │ ├─ [3] Copy hook_sys_connect() bytes → kernel RWX page │
42 * │ └─ [4] kernel_copyin(&hook_addr, &sysent[98].sy_call, 8) │
43 * │ │
44 * └──────────────────────────────────────────────────────────────┘
45 *
46 * ┌──────────────────────────────────────────────────────────────┐
47 * │ Kernel space (PS5 FreeBSD 11) │
48 * │ │
49 * │ ScePatchChecker calls connect(sock, &sony_addr, len) │
50 * │ │ │
51 * │ └─► sysent[98].sy_call → hook_sys_connect() │
52 * │ │ │
53 * │ ├─ td->td_proc->p_pid == zftpd_pid? │
54 * │ │ YES → original_connect() (FTP traffic) │
55 * │ │ │
56 * │ └─ destination in SONY_IP_RANGES? │
57 * │ YES → return ENETUNREACH (no packet!) │
58 * │ NO → original_connect() │
59 * │ │
60 * └──────────────────────────────────────────────────────────────┘
61 *
62 *
63 * USAGE
64 * -----
65 * // After ps5_jailbreak():
66 * ps5_net_filter_config_t cfg = PS5_NET_FILTER_CONFIG_DEFAULT;
67 * int rc = ps5_net_filter_install(&cfg);
68 * if (rc != 0) { // handle: filter not installed, FTP still works }
69 *
70 * // At shutdown:
71 * ps5_net_filter_uninstall();
72 *
73 *
74 * SAFETY PROPERTIES
75 * -----------------
76 * - The hook ALWAYS passes through connections from zftpd's own PID.
77 * - The hook ALWAYS passes through connections to RFC-1918 addresses
78 * (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) and loopback.
79 * - The blocklist is a fixed compile-time array; no dynamic allocation
80 * occurs in kernel context.
81 * - If the sysent offset table does not contain an entry for the running
82 * firmware version, install() returns an error and the kernel is NOT
83 * modified.
84 * - uninstall() is idempotent.
85 *
86 *
87 * FIRMWARE SUPPORT
88 * ----------------
89 * Supported firmware versions (sysent offsets validated):
90 * 4.03, 7.00, 7.61, 8.20, 8.60, 9.00, 9.40, 9.60, 10.00, 10.01, 10.50
91 *
92 * Unsupported firmware: install() returns PS5_NET_FILTER_ERR_FW_UNSUPPORTED.
93 *
94 *
95 * THREAD SAFETY
96 * -------------
97 * - install() and uninstall() must be called from the main thread only.
98 * - Must be called after ps5_jailbreak() and before ftp_server_start().
99 * - The installed kernel hook itself is fully re-entrant (no mutable globals
100 * in the kernel-context code path).
101 *
102 *
103 * @note This module is compiled ONLY when PLATFORM_PS5 is defined.
104 * On all other platforms the header provides no-op stubs.
105 *
106 * @warning Modifies kernel sysent table. Always call uninstall() before
107 * the payload exits, otherwise the kernel will jump to freed memory.
108 */
109 
110#ifndef PS5_NET_FILTER_H
111#define PS5_NET_FILTER_H
112 
113/*===========================================================================*
114 * COMPILE GUARD — PS5 only
115 *===========================================================================*/
116 
117#ifdef PLATFORM_PS5
118 
119#include <stdint.h>
120#include <stdatomic.h>
121#include <netinet/in.h>
122 
123#ifdef __cplusplus
124extern "C" {
125#endif
126 
127/*===========================================================================*
128 * CONSTANTS
129 *===========================================================================*/
130 
131/**
132 * Maximum number of IP subnet rules in the block list.
133 *
134 * RATIONALE: Fixed compile-time limit prevents any dynamic allocation
135 * in the kernel-context hot path. 32 entries covers all known Sony
136 * CDN and PSN IP ranges with room to spare.
137 */
138#define PS5_NET_FILTER_MAX_RULES 32U
139 
140/** Maximum length of a daemon name string (including NUL). */
141#define PS5_NET_FILTER_MAX_NAME 24U
142 
143/*===========================================================================*
144 * ERROR CODES
145 *===========================================================================*/
146 
147typedef enum {
148 PS5_NET_FILTER_OK = 0, /**< Success */
149 PS5_NET_FILTER_ERR_INVALID_PARAM = -1, /**< NULL pointer or bad argument */
150 PS5_NET_FILTER_ERR_FW_UNSUPPORTED = -2, /**< Firmware version not in table */
151 PS5_NET_FILTER_ERR_ALREADY_ACTIVE = -3, /**< install() called twice */
152 PS5_NET_FILTER_ERR_NOT_INSTALLED = -4, /**< uninstall() without install() */
153 PS5_NET_FILTER_ERR_KMAP_FAILED = -5, /**< Could not map kernel executable page */
154 PS5_NET_FILTER_ERR_KWRITE_FAILED = -6, /**< kernel_copyin() failed */
155 PS5_NET_FILTER_ERR_FW_DETECT = -7, /**< Could not read firmware version */
156 PS5_NET_FILTER_ERR_SYSENT_INVALID = -8, /**< Sysent sanity check failed */
157 PS5_NET_FILTER_ERR_EXTERNAL_HOOK = -9, /**< Another payload already hooked sysent */
158} ps5_net_filter_err_t;
159 
160/*===========================================================================*
161 * IP SUBNET RULE
162 *===========================================================================*/
163 
164/**
165 * An IPv4 subnet to block (network byte order).
166 *
167 * A connection is blocked when:
168 * (dest_addr & mask) == (network & mask)
169 */
170typedef struct {
171 uint32_t network; /**< Network address (network byte order) */
172 uint32_t mask; /**< Subnet mask (network byte order) */
173} ps5_net_filter_rule_t;
174 
175/*===========================================================================*
176 * STATISTICS
177 *===========================================================================*/
178 
179/**
180 * Runtime statistics collected by the kernel hook.
181 *
182 * @note All fields updated with atomic increments; safe to read at any time.
183 */
184typedef struct {
185 uint64_t blocked_total; /**< Total connections blocked */
186 uint64_t allowed_self; /**< Connections allowed: zftpd's own PID */
187 uint64_t allowed_local; /**< Connections allowed: RFC-1918 / loopback */
188 uint64_t allowed_other; /**< Connections allowed: not in block list */
189 uint64_t hook_calls_total; /**< Total hook invocations */
190} ps5_net_filter_stats_t;
191 
192/*===========================================================================*
193 * CONFIGURATION
194 *===========================================================================*/
195 
196/**
197 * Filter configuration passed to ps5_net_filter_install().
198 *
199 * DESIGN: The caller builds the rule table in userland before install().
200 * The table is copied into the kernel-accessible hook page alongside the
201 * hook code itself, avoiding any userland pointer dereferences in kernel
202 * context.
203 */
204typedef struct {
205 /**
206 * Array of IP subnets to block.
207 *
208 * If rule_count == 0, the default Sony IP table is used automatically.
209 * To disable all blocking (test/debug), set rule_count = 0 and
210 * use_default_rules = false — install() will still succeed but the
211 * hook will be a passthrough.
212 */
213 ps5_net_filter_rule_t rules[PS5_NET_FILTER_MAX_RULES];
214 
215 /** Number of valid entries in rules[]. Range [0, PS5_NET_FILTER_MAX_RULES]. */
216 uint32_t rule_count;
217 
218 /**
219 * If true and rule_count == 0, fill rules[] with the built-in Sony
220 * CDN/PSN table before installing. Default: true.
221 */
222 uint8_t use_default_rules;
223 
224 /**
225 * Also hook sendto(2) in addition to connect(2).
226 *
227 * Sony's Curl-based daemons (RNPS) use sendto() on UDP sockets for
228 * some telemetry paths. Enabling this intercepts those as well.
229 *
230 * Default: true.
231 */
232 uint8_t hook_sendto;
233 
234 /** Reserved for future use. Must be zero-initialised. */
235 uint8_t _reserved[2];
236 
237} ps5_net_filter_config_t;
238 
239/**
240 * Default configuration initialiser.
241 *
242 * Usage:
243 * ps5_net_filter_config_t cfg = PS5_NET_FILTER_CONFIG_DEFAULT;
244 * ps5_net_filter_install(&cfg);
245 */
246#define PS5_NET_FILTER_CONFIG_DEFAULT \
247 { .rules = {{0, 0}}, .rule_count = 0U, .use_default_rules = 1, .hook_sendto = 1, ._reserved = {0, 0} }
248 
249/*===========================================================================*
250 * PUBLIC API
251 *===========================================================================*/
252 
253/**
254 * @brief Install the outbound connection filter into the PS5 kernel.
255 *
256 * Steps performed:
257 * 1. Detect running firmware version.
258 * 2. Look up sysent[SYS_CONNECT] offset for that firmware.
259 * 3. Populate cfg->rules[] with default Sony IP table if requested.
260 * 4. Allocate a kernel-executable page and copy the hook code + rule
261 * table into it.
262 * 5. Atomically replace sysent[98].sy_call (and optionally sysent[133])
263 * with the address of the installed hook.
264 * 6. Save our own PID so the hook can fast-path our FTP traffic.
265 *
266 * @param[in] cfg Filter configuration. May be NULL to use all defaults.
267 *
268 * @return PS5_NET_FILTER_OK on success, negative error code on failure.
269 *
270 * @retval PS5_NET_FILTER_OK Hook installed successfully.
271 * @retval PS5_NET_FILTER_ERR_FW_UNSUPPORTED Firmware not in support table.
272 * @retval PS5_NET_FILTER_ERR_ALREADY_ACTIVE Called twice without uninstall.
273 * @retval PS5_NET_FILTER_ERR_KMAP_FAILED Kernel page allocation failed.
274 * @retval PS5_NET_FILTER_ERR_KWRITE_FAILED kernel_copyin() failed.
275 *
276 * @pre ps5_jailbreak() has been called successfully (kernel r/w access).
277 * @pre Must be called from the main thread.
278 * @post On success, sysent[98].sy_call points to the installed hook.
279 *
280 * @note Thread-safety: NOT thread-safe. Call from main thread only.
281 * @note The install is reversible: always call ps5_net_filter_uninstall()
282 * before the payload exits.
283 * @note Failure is non-fatal: FTP server can start and operate normally;
284 * bandwidth saturation will continue to occur.
285 */
286int ps5_net_filter_install(const ps5_net_filter_config_t *cfg);
287 
288/**
289 * @brief Uninstall the connection filter and restore the original kernel state.
290 *
291 * Restores the original sysent[98].sy_call (and sysent[133] if hooked).
292 * Frees the kernel-executable page.
293 *
294 * @return PS5_NET_FILTER_OK on success, negative error code on failure.
295 *
296 * @retval PS5_NET_FILTER_OK Hook removed, kernel restored.
297 * @retval PS5_NET_FILTER_ERR_NOT_INSTALLED install() was never called.
298 * @retval PS5_NET_FILTER_ERR_KWRITE_FAILED kernel_copyin() failed.
299 *
300 * @note Idempotent: safe to call even if install() failed.
301 * @note Thread-safety: NOT thread-safe. Call from main thread only.
302 * @note WCET: Two kernel_copyin() calls + one munmap(). Bounded.
303 */
304int ps5_net_filter_uninstall(void);
305 
306/**
307 * @brief Check whether the filter is currently installed.
308 *
309 * @return 1 if installed and active, 0 otherwise.
310 *
311 * @note Thread-safety: Safe (atomic load).
312 */
313int ps5_net_filter_is_active(void);
314 
315/**
316 * @brief Read runtime statistics from the filter.
317 *
318 * @param[out] out Destination buffer for statistics snapshot.
319 *
320 * @return PS5_NET_FILTER_OK on success, PS5_NET_FILTER_ERR_INVALID_PARAM
321 * if out is NULL.
322 *
323 * @pre out != NULL
324 * @note Thread-safety: Safe (atomic reads, snapshot may be slightly stale).
325 */
326int ps5_net_filter_get_stats(ps5_net_filter_stats_t *out);
327 
328/**
329 * @brief Retrieve a human-readable description of an error code.
330 *
331 * @param[in] err Error code returned by any ps5_net_filter_* function.
332 *
333 * @return Static string describing the error. Never returns NULL.
334 *
335 * @note Thread-safety: Safe (read-only static data).
336 */
337const char *ps5_net_filter_strerror(int err);
338 
339#ifdef __cplusplus
340}
341#endif
342 
343/*===========================================================================*
344 * STUB IMPLEMENTATIONS — non-PS5 platforms
345 *===========================================================================*/
346 
347#else /* !PLATFORM_PS5 */
348 
349/*
350 * Provide no-op stubs so callers can compile on POSIX without ifdefs.
351 * The compiler will inline and eliminate these entirely at -O2.
352 */
353typedef struct { int _dummy; } ps5_net_filter_config_t;
354typedef struct { int _dummy; } ps5_net_filter_stats_t;
355 
356#define PS5_NET_FILTER_CONFIG_DEFAULT { 0 }
357#define PS5_NET_FILTER_OK 0
358 
359static inline int ps5_net_filter_install(const ps5_net_filter_config_t *cfg)
360 { (void)cfg; return 0; }
361static inline int ps5_net_filter_uninstall(void) { return 0; }
362static inline int ps5_net_filter_is_active(void) { return 0; }
363static inline int ps5_net_filter_get_stats(ps5_net_filter_stats_t *s)
364 { (void)s; return 0; }
365static inline const char *ps5_net_filter_strerror(int e)
366 { (void)e; return "not supported on this platform"; }
367 
368#endif /* PLATFORM_PS5 */
369 
370#endif /* PS5_NET_FILTER_H */
371