Seregon/zftpd

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

C/11.0 KB/No license
src/pal_network.c
zftpd / src / pal_network.c
1/*
2MIT License
3 
4Copyright (c) 2026 Seregon
5 
6Permission is hereby granted, free of charge, to any person obtaining a copy
7of this software and associated documentation files (the "Software"), to deal
8in the Software without restriction, including without limitation the rights
9to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10copies of the Software, and to permit persons to whom the Software is
11furnished to do so, subject to the following conditions:
12 
13The above copyright notice and this permission notice shall be included in all
14copies or substantial portions of the Software.
15 
16THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22SOFTWARE.
23*/
24 
25/**
26 * @file pal_network.c
27 * @brief Platform Abstraction Layer - Network Implementation
28 *
29 * @author SeregonWar
30 * @version 1.0.0
31 * @date 2026-02-13
32 *
33 */
34 
35#include "pal_network.h"
36#include "ftp_config.h"
37#include "ftp_log.h"
38#include <errno.h>
39#include <fcntl.h>
40#include <stdlib.h>
41#include <string.h>
42#include <sys/time.h>
43#include <unistd.h>
44 
45#if FTP_SOCKET_TELEMETRY
46static void pal_socket_telemetry(socket_t fd) {
47 int sndbuf = -1;
48 int rcvbuf = -1;
49 int nodelay = -1;
50 int keepalive = -1;
51 socklen_t optlen = (socklen_t)sizeof(int);
52 
53 (void)PAL_GETSOCKOPT(fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, &optlen);
54 optlen = (socklen_t)sizeof(int);
55 (void)PAL_GETSOCKOPT(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, &optlen);
56 optlen = (socklen_t)sizeof(int);
57 (void)PAL_GETSOCKOPT(fd, IPPROTO_TCP, TCP_NODELAY, &nodelay, &optlen);
58 optlen = (socklen_t)sizeof(int);
59 (void)PAL_GETSOCKOPT(fd, SOL_SOCKET, SO_KEEPALIVE, &keepalive, &optlen);
60 
61 char lip[INET_ADDRSTRLEN] = "0.0.0.0";
62 char rip[INET_ADDRSTRLEN] = "0.0.0.0";
63 uint16_t lport = 0U;
64 uint16_t rport = 0U;
65 
66 struct sockaddr_in sa;
67 socklen_t salen = (socklen_t)sizeof(sa);
68 if (PAL_GETSOCKNAME(fd, (struct sockaddr *)&sa, &salen) == 0) {
69 (void)PAL_INET_NTOP(AF_INET, &sa.sin_addr, lip, sizeof(lip));
70 lport = (uint16_t)PAL_NTOHS(sa.sin_port);
71 }
72 
73 salen = (socklen_t)sizeof(sa);
74 if (PAL_GETPEERNAME(fd, (struct sockaddr *)&sa, &salen) == 0) {
75 (void)PAL_INET_NTOP(AF_INET, &sa.sin_addr, rip, sizeof(rip));
76 rport = (uint16_t)PAL_NTOHS(sa.sin_port);
77 }
78 
79 char line[256];
80 (void)snprintf(
81 line, sizeof(line),
82 "SOCK L=%s:%u R=%s:%u SNDBUF=%d RCVBUF=%d NODELAY=%d KEEPALIVE=%d", lip,
83 (unsigned)lport, rip, (unsigned)rport, sndbuf, rcvbuf, nodelay,
84 keepalive);
85 ftp_log_line(FTP_LOG_INFO, line);
86}
87#endif
88 
89/*===========================================================================*
90 * NETWORK INITIALIZATION
91 *===========================================================================*/
92 
93/**
94 * @brief Initialize network subsystem
95 */
96ftp_error_t pal_network_init(void) {
97#if defined(PLATFORM_PS3)
98 static atomic_int initialized = ATOMIC_VAR_INIT(0);
99 
100 if (atomic_load(&initialized) != 0) {
101 return FTP_OK;
102 }
103 
104 /* Initialize PS3 network */
105 int ret = netInitialize();
106 if (ret < 0) {
107 return FTP_ERR_SOCKET_CREATE;
108 }
109 
110 atomic_store(&initialized, 1);
111 return FTP_OK;
112 
113#else
114 /* POSIX: Network always available */
115 return FTP_OK;
116#endif
117}
118 
119/**
120 * @brief Cleanup network subsystem
121 */
122void pal_network_fini(void) {
123#if defined(PLATFORM_PS3)
124 netFinalize();
125#else
126 /* POSIX: No cleanup needed */
127#endif
128}
129 
130/*===========================================================================*
131 * SOCKET CONFIGURATION
132 *===========================================================================*/
133 
134/**
135 * @brief Configure socket for optimal performance
136 */
137ftp_error_t pal_socket_configure(socket_t fd) {
138 int ret;
139 
140 /* Validate socket descriptor */
141 if (fd < 0) {
142 return FTP_ERR_INVALID_PARAM;
143 }
144 
145#if FTP_TCP_NODELAY
146 /* Disable Nagle's algorithm (reduce latency) */
147 {
148 int nodelay = 1;
149 ret =
150 PAL_SETSOCKOPT(fd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));
151 if (ret < 0) {
152 /* Non-fatal: continue with other options */
153 }
154 }
155#endif
156 
157 /* Set send buffer size */
158 {
159 int sndbuf = (int)FTP_TCP_SNDBUF;
160 ret = PAL_SETSOCKOPT(fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));
161 if (ret < 0) {
162 /* Non-fatal */
163 }
164 }
165 
166 /* Set receive buffer size */
167 {
168 int rcvbuf = (int)FTP_TCP_RCVBUF;
169 ret = PAL_SETSOCKOPT(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
170 if (ret < 0) {
171 /* Non-fatal */
172 }
173 }
174 
175#if FTP_TCP_KEEPALIVE
176 /* Enable TCP keepalive */
177 {
178 int keepalive = 1;
179 ret = PAL_SETSOCKOPT(fd, SOL_SOCKET, SO_KEEPALIVE, &keepalive,
180 sizeof(keepalive));
181 if (ret < 0) {
182 /* Non-fatal */
183 }
184 }
185 
186 /* Set keepalive parameters (Linux/FreeBSD specific) */
187#if defined(__linux__) || defined(__FreeBSD__) || defined(PLATFORM_PS4) || \
188 defined(PLATFORM_PS5)
189 {
190 int idle = (int)FTP_TCP_KEEPIDLE;
191 int intvl = (int)FTP_TCP_KEEPINTVL;
192 int cnt = (int)FTP_TCP_KEEPCNT;
193 
194 (void)PAL_SETSOCKOPT(fd, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle));
195 (void)PAL_SETSOCKOPT(fd, IPPROTO_TCP, TCP_KEEPINTVL, &intvl, sizeof(intvl));
196 (void)PAL_SETSOCKOPT(fd, IPPROTO_TCP, TCP_KEEPCNT, &cnt, sizeof(cnt));
197 }
198#endif
199#endif /* FTP_TCP_KEEPALIVE */
200 
201#if FTP_SOCKET_TELEMETRY
202 pal_socket_telemetry(fd);
203#endif
204 
205 return FTP_OK;
206}
207 
208/*===========================================================================*
209 * DATA SOCKET CONFIGURATION
210 *
211 * Tuned for bulk file transfer (STOR / RETR / APPE)
212 *
213 * ctrl socket data socket
214 * ────────── ───────────
215 * TCP_NODELAY = 1 TCP_NODELAY = 0 (Nagle ON → coalesce)
216 * no SO_LINGER SO_LINGER = 10 s
217 * no I/O timeout SO_RCVTIMEO / SO_SNDTIMEO = 120 s
218 * keepalive = 60/10/3 keepalive = 60/10/3
219 * no cork cork/uncork around bursts
220 *
221 *===========================================================================*/
222 
223/*---------------------------------------------------------------------------*
224 * TCP_CORK / TCP_NOPUSH (packet coalescing)
225 *
226 * cork(): hold all small segments in kernel until uncork
227 * uncork(): flush accumulated data as large MSS-sized packets
228 *
229 * ┌─────────────────────────────────────────┐
230 * │ send(512 B) → held in kernel buffer │
231 * │ send(512 B) → still held │
232 * │ send(512 B) → still held │
233 * │ uncork() → one 1536 B TCP segment │
234 * └─────────────────────────────────────────┘
235 *
236 * Linux: TCP_CORK (IPPROTO_TCP)
237 * BSD/PS5: TCP_NOPUSH (IPPROTO_TCP)
238 *---------------------------------------------------------------------------*/
239 
240void pal_socket_cork(socket_t fd) {
241 int one = 1;
242#if defined(__linux__)
243 (void)PAL_SETSOCKOPT(fd, IPPROTO_TCP, TCP_CORK, &one, sizeof(one));
244#elif defined(__FreeBSD__) || defined(PLATFORM_PS4) || \
245 defined(PLATFORM_PS5) || defined(__APPLE__)
246 (void)PAL_SETSOCKOPT(fd, IPPROTO_TCP, TCP_NOPUSH, &one, sizeof(one));
247#else
248 (void)fd;
249 (void)one;
250#endif
251}
252 
253void pal_socket_uncork(socket_t fd) {
254 int zero = 0;
255#if defined(__linux__)
256 (void)PAL_SETSOCKOPT(fd, IPPROTO_TCP, TCP_CORK, &zero, sizeof(zero));
257#elif defined(__FreeBSD__) || defined(PLATFORM_PS4) || \
258 defined(PLATFORM_PS5) || defined(__APPLE__)
259 (void)PAL_SETSOCKOPT(fd, IPPROTO_TCP, TCP_NOPUSH, &zero, sizeof(zero));
260#else
261 (void)fd;
262 (void)zero;
263#endif
264}
265 
266ftp_error_t pal_socket_configure_data(socket_t fd) {
267 int ret;
268 
269 if (fd < 0) {
270 return FTP_ERR_INVALID_PARAM;
271 }
272 
273 /*----------- Nagle ON for bulk coalescing ----------------*/
274 {
275 int nodelay = 0;
276 ret =
277 PAL_SETSOCKOPT(fd, IPPROTO_TCP, TCP_NODELAY, &nodelay, sizeof(nodelay));
278 (void)ret;
279 }
280 
281 /*----------- Send buffer --------------------------------*/
282 /*
283 * SO_SNDBUF is intentionally NOT set here on non-PS4/PS5 platforms.
284 *
285 * Setting SO_SNDBUF explicitly — even on a pre-bind listening socket —
286 * disables the kernel's TCP send-buffer auto-tuning (net.inet.tcp.sendbuf_auto
287 * on FreeBSD, tcp_wmem on Linux). Auto-tuning grows the buffer dynamically
288 * to fill the measured RTT×BDP product, which is what allows the HTTP server
289 * to saturate any internet link without knowing the client's RTT in advance.
290 *
291 * EXCEPTION — PS4/PS5 OrbisOS:
292 * The OrbisOS kernel clamps SO_SNDBUF auto-tuning to a system maximum
293 * that is lower than what a GbE LAN transfer needs. Setting FTP_TCP_DATA_SNDBUF
294 * explicitly forces the full 4 MB, covering the BDP for 1 GbE at LAN RTTs
295 * and eliminating the ~50–100 Mbps throughput loss from the kernel clamp.
296 */
297#if (defined(PS5) || defined(PLATFORM_PS5) || defined(PS4) || defined(PLATFORM_PS4)) && \
298 defined(FTP_TCP_DATA_SNDBUF) && (FTP_TCP_DATA_SNDBUF > 0U)
299 {
300 int sndbuf = (int)FTP_TCP_DATA_SNDBUF;
301 (void)PAL_SETSOCKOPT(fd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));
302 }
303#endif
304 /* cmd_PASV / cmd_EPSV intentionally omit SO_SNDBUF on other platforms so auto-tuning is active. */
305 (void)pal_socket_set_timeouts(fd, FTP_DATA_IO_TIMEOUT_MS,
306 FTP_DATA_IO_TIMEOUT_MS);
307 
308 /*----------- SO_LINGER (flush before close) -------------*/
309 {
310 struct linger lg;
311 lg.l_onoff = 1;
312 lg.l_linger = FTP_DATA_LINGER_TIMEOUT_S;
313 ret = PAL_SETSOCKOPT(fd, SOL_SOCKET, SO_LINGER, &lg, sizeof(lg));
314 (void)ret;
315 }
316 
317 /*----------- Keepalive -----------------------------------*/
318#if FTP_TCP_KEEPALIVE
319 {
320 int keepalive = 1;
321 ret = PAL_SETSOCKOPT(fd, SOL_SOCKET, SO_KEEPALIVE, &keepalive,
322 sizeof(keepalive));
323 (void)ret;
324 }
325#if defined(__linux__) || defined(__FreeBSD__) || defined(PLATFORM_PS4) || \
326 defined(PLATFORM_PS5)
327 {
328 int idle = (int)FTP_TCP_KEEPIDLE;
329 int intvl = (int)FTP_TCP_KEEPINTVL;
330 int cnt = (int)FTP_TCP_KEEPCNT;
331 (void)PAL_SETSOCKOPT(fd, IPPROTO_TCP, TCP_KEEPIDLE, &idle, sizeof(idle));
332 (void)PAL_SETSOCKOPT(fd, IPPROTO_TCP, TCP_KEEPINTVL, &intvl, sizeof(intvl));
333 (void)PAL_SETSOCKOPT(fd, IPPROTO_TCP, TCP_KEEPCNT, &cnt, sizeof(cnt));
334 }
335#endif
336#endif /* FTP_TCP_KEEPALIVE */
337 
338#if FTP_SOCKET_TELEMETRY
339 pal_socket_telemetry(fd);
340#endif
341 
342 return FTP_OK;
343}
344 
345/**
346 * @brief Set socket to non-blocking mode
347 */
348ftp_error_t pal_socket_set_nonblocking(socket_t fd) {
349 if (fd < 0) {
350 return FTP_ERR_INVALID_PARAM;
351 }
352 
353 /* POSIX: Use fcntl */
354 int flags = fcntl(fd, F_GETFL, 0);
355 if (flags < 0) {
356 return FTP_ERR_SOCKET_SEND;
357 }
358 
359 if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) < 0) {
360 return FTP_ERR_SOCKET_SEND;
361 }
362 
363 return FTP_OK;
364}
365 
366/**
367 * @brief Set socket to blocking mode
368 */
369ftp_error_t pal_socket_set_blocking(socket_t fd) {
370 if (fd < 0) {
371 return FTP_ERR_INVALID_PARAM;
372 }
373 
374 /* POSIX: Use fcntl */
375 int flags = fcntl(fd, F_GETFL, 0);
376 if (flags < 0) {
377 return FTP_ERR_SOCKET_SEND;
378 }
379 
380 if (fcntl(fd, F_SETFL, flags & ~O_NONBLOCK) < 0) {
381 return FTP_ERR_SOCKET_SEND;
382 }
383 
384 return FTP_OK;
385}
386 
387/**
388 * @brief Enable address reuse
389 */
390ftp_error_t pal_socket_set_reuseaddr(socket_t fd) {
391 if (fd < 0) {
392 return FTP_ERR_INVALID_PARAM;
393 }
394 
395 int optval = 1;
396 int ret =
397 PAL_SETSOCKOPT(fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
398 if (ret < 0) {
399 return FTP_ERR_SOCKET_SEND;
400 }
401 
402 return FTP_OK;
403}
404 
405ftp_error_t pal_socket_set_timeouts(socket_t fd, uint32_t recv_timeout_ms,
406 uint32_t send_timeout_ms) {
407 if (fd < 0) {
408 return FTP_ERR_INVALID_PARAM;
409 }
410 
411 struct timeval tv;
412 
413 tv.tv_sec = (time_t)(recv_timeout_ms / 1000U);
414 tv.tv_usec = (suseconds_t)((recv_timeout_ms % 1000U) * 1000U);
415 if (PAL_SETSOCKOPT(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, (socklen_t)sizeof(tv)) <
416 0) {
417 return FTP_ERR_SOCKET_SEND;
418 }
419 
420 tv.tv_sec = (time_t)(send_timeout_ms / 1000U);
421 tv.tv_usec = (suseconds_t)((send_timeout_ms % 1000U) * 1000U);
422 if (PAL_SETSOCKOPT(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, (socklen_t)sizeof(tv)) <
423 0) {
424 return FTP_ERR_SOCKET_SEND;
425 }
426 
427 return FTP_OK;
428}
429 
430ssize_t pal_send_all(socket_t fd, const void *buffer, size_t length,
431 int flags) {
432 if ((buffer == NULL) || (length == 0U)) {
433 errno = EINVAL;
434 return -1;
435 }
436 if (fd < 0) {
437 errno = EBADF;
438 return -1;
439 }
440 
441 const uint8_t *p = (const uint8_t *)buffer;
442 size_t total = 0U;
443 
444 while (total < length) {
445 size_t chunk = length - total;
446 ssize_t n = PAL_SEND(fd, p + total, chunk, flags);
447 if (n > 0) {
448 total += (size_t)n;
449 continue;
450 }
451 if (n == 0) {
452 errno = EPIPE;
453 return -1;
454 }
455 if (errno == EINTR) {
456 continue;
457 }
458 if ((errno == EAGAIN) || (errno == EWOULDBLOCK)) {
459 usleep(1000);
460 continue;
461 }
462 return -1;
463 }
464 
465 return (ssize_t)total;
466}
467 
468/*===========================================================================*
469 * UTILITY FUNCTIONS
470 *===========================================================================*/
471 
472/**
473 * @brief Extract IP address from sockaddr
474 */
475ftp_error_t pal_sockaddr_to_ip(const struct sockaddr_in *addr, char *buffer,
476 size_t size) {
477 /* Validate parameters */
478 if ((addr == NULL) || (buffer == NULL)) {
479 return FTP_ERR_INVALID_PARAM;
480 }
481 
482 if (size < INET_ADDRSTRLEN) {
483 return FTP_ERR_INVALID_PARAM;
484 }
485 
486 /* Convert IP to string */
487 const char *result =
488 PAL_INET_NTOP(AF_INET, &addr->sin_addr, buffer, (socklen_t)size);
489 if (result == NULL) {
490 return FTP_ERR_INVALID_PARAM;
491 }
492 
493 return FTP_OK;
494}
495 
496/**
497 * @brief Extract port from sockaddr
498 */
499uint16_t pal_sockaddr_get_port(const struct sockaddr_in *addr) {
500 if (addr == NULL) {
501 return 0U;
502 }
503 
504 return PAL_NTOHS(addr->sin_port);
505}
506 
507/**
508 * @brief Create sockaddr from IP and port (IPv4 only, legacy)
509 */
510ftp_error_t pal_make_sockaddr(const char *ip, uint16_t port,
511 struct sockaddr_in *addr) {
512 /* Validate parameters */
513 if ((ip == NULL) || (addr == NULL)) {
514 return FTP_ERR_INVALID_PARAM;
515 }
516 
517 if (port == 0U) {
518 return FTP_ERR_INVALID_PARAM;
519 }
520 
521 /* Zero-initialize structure */
522 memset(addr, 0, sizeof(*addr));
523 
524 /* Set address family */
525 addr->sin_family = AF_INET;
526 
527 /* Convert IP string to binary */
528 int ret = PAL_INET_PTON(AF_INET, ip, &addr->sin_addr);
529 if (ret != 1) {
530 return FTP_ERR_INVALID_PARAM;
531 }
532 
533 /* Set port (convert to network byte order) */
534 addr->sin_port = PAL_HTONS(port);
535 
536 return FTP_OK;
537}
538 
539/**
540 * @brief Create sockaddr from IP:port string with IPv6 bracket support
541 *
542 * Parses:
543 * - "192.168.1.1:8888" → IPv4
544 * - "[::1]:8888" → IPv6 loopback
545 * - "[fe80::1%eth0]:8888" → IPv6 with zone ID
546 *
547 * @param addr_str Address string with embedded port (e.g., "[::1]:8888")
548 * @param out_addr Output sockaddr_storage (IPv4 or IPv6)
549 * @param out_len Output length (sizeof(sockaddr_in) or sizeof(sockaddr_in6))
550 *
551 * @return FTP_OK on success, FTP_ERR_INVALID_PARAM on parse error
552 */
553ftp_error_t pal_make_sockaddr_ex(const char *addr_str,
554 struct sockaddr_storage *out_addr,
555 socklen_t *out_len) {
556 if ((addr_str == NULL) || (out_addr == NULL) || (out_len == NULL)) {
557 return FTP_ERR_INVALID_PARAM;
558 }
559 
560 memset(out_addr, 0, sizeof(*out_addr));
561 
562 /* Check for IPv6 bracket notation: [ipv6]:port */
563 if (addr_str[0] == '[') {
564 /* IPv6 address */
565 const char *close_bracket = strchr(addr_str, ']');
566 if (close_bracket == NULL) {
567 return FTP_ERR_INVALID_PARAM;
568 }
569 
570 /* Extract IPv6 address (between [ and ]) */
571 size_t ipv6_len = (size_t)(close_bracket - addr_str - 1);
572 if (ipv6_len == 0 || ipv6_len >= INET6_ADDRSTRLEN) {
573 return FTP_ERR_INVALID_PARAM;
574 }
575 
576 char ipv6_str[INET6_ADDRSTRLEN];
577 memcpy(ipv6_str, addr_str + 1, ipv6_len);
578 ipv6_str[ipv6_len] = '\0';
579 
580 /* Parse port after ] */
581 const char *port_str = close_bracket + 1;
582 if (*port_str != ':') {
583 return FTP_ERR_INVALID_PARAM;
584 }
585 port_str++;
586 
587 uint16_t port = (uint16_t)strtoul(port_str, NULL, 10);
588 if (port == 0U) {
589 return FTP_ERR_INVALID_PARAM;
590 }
591 
592 /* Build sockaddr_in6 */
593 struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)out_addr;
594 addr6->sin6_family = AF_INET6;
595 addr6->sin6_port = PAL_HTONS(port);
596 
597 if (PAL_INET_PTON(AF_INET6, ipv6_str, &addr6->sin6_addr) != 1) {
598 return FTP_ERR_INVALID_PARAM;
599 }
600 
601 *out_len = (socklen_t)sizeof(struct sockaddr_in6);
602 return FTP_OK;
603 }
604 
605 /* IPv4: "192.168.1.1:port" */
606 const char *colon = strchr(addr_str, ':');
607 if (colon == NULL) {
608 return FTP_ERR_INVALID_PARAM;
609 }
610 
611 size_t ipv4_len = (size_t)(colon - addr_str);
612 if (ipv4_len == 0 || ipv4_len >= INET_ADDRSTRLEN) {
613 return FTP_ERR_INVALID_PARAM;
614 }
615 
616 char ipv4_str[INET_ADDRSTRLEN];
617 memcpy(ipv4_str, addr_str, ipv4_len);
618 ipv4_str[ipv4_len] = '\0';
619 
620 uint16_t port = (uint16_t)strtoul(colon + 1, NULL, 10);
621 if (port == 0U) {
622 return FTP_ERR_INVALID_PARAM;
623 }
624 
625 /* Build sockaddr_in */
626 struct sockaddr_in *addr4 = (struct sockaddr_in *)out_addr;
627 addr4->sin_family = AF_INET;
628 addr4->sin_port = PAL_HTONS(port);
629 
630 if (PAL_INET_PTON(AF_INET, ipv4_str, &addr4->sin_addr) != 1) {
631 return FTP_ERR_INVALID_PARAM;
632 }
633 
634 *out_len = (socklen_t)sizeof(struct sockaddr_in);
635 return FTP_OK;
636}
637 
638ftp_error_t pal_network_get_primary_ip(char *buffer, size_t size) {
639 if (buffer == NULL) {
640 return FTP_ERR_INVALID_PARAM;
641 }
642 if (size < INET_ADDRSTRLEN) {
643 return FTP_ERR_INVALID_PARAM;
644 }
645 
646 int fd = PAL_SOCKET(AF_INET, SOCK_DGRAM, 0);
647 if (fd < 0) {
648 return FTP_ERR_SOCKET_CREATE;
649 }
650 
651 struct sockaddr_in dst;
652 memset(&dst, 0, sizeof(dst));
653 dst.sin_family = AF_INET;
654 dst.sin_port = PAL_HTONS(53);
655 (void)PAL_INET_PTON(AF_INET, "8.8.8.8", &dst.sin_addr);
656 
657 if (PAL_CONNECT(fd, (struct sockaddr *)&dst, sizeof(dst)) < 0) {
658 PAL_CLOSE(fd);
659 return FTP_ERR_SOCKET_CREATE;
660 }
661 
662 struct sockaddr_in local;
663 socklen_t local_len = (socklen_t)sizeof(local);
664 memset(&local, 0, sizeof(local));
665 
666 if (getsockname(fd, (struct sockaddr *)&local, &local_len) < 0) {
667 PAL_CLOSE(fd);
668 return FTP_ERR_INVALID_PARAM;
669 }
670 
671 PAL_CLOSE(fd);
672 return pal_sockaddr_to_ip(&local, buffer, size);
673}
674 
675/*===========================================================================*
676 * NETWORK STACK RESET
677 *
678 * ROOT CAUSE OF DEGRADATION (Issues #3 and #7):
679 *
680 * On PS4/PS5 OrbisOS the TCP stack does not automatically reclaim kernel
681 * socket send/receive buffers after a data connection is closed. After a
682 * large transfer — especially to the internal SSD whose PFS crypto layer
683 * stalls writes for tens of milliseconds — the kernel's internal socket
684 * buffer accounting can remain inflated. Subsequent connections share the
685 * same per-process socket budget; new connections are therefore allocated
686 * smaller-than-configured buffers, degrading throughput.
687 *
688 * Additionally, after an M.2 folder upload the NIC driver's TX descriptor
689 * ring can become partially saturated if SO_SNDBUF on the data socket was
690 * not explicitly released. This manifests as a ~50% throughput reduction
691 * on subsequent internal SSD transfers until the network interface is
692 * cycled.
693 *
694 * WHAT THIS FUNCTION DOES:
695 *
696 * 1. Iterates the session pool (passed in via the sessions array and count).
697 * 2. For each idle session (ctrl_fd valid, no active data transfer), resets
698 * SO_SNDBUF and SO_RCVBUF to the kernel default (0 = let the kernel
699 * choose) and then back to the configured target values. This forces
700 * the kernel to flush its internal accounting for those sockets.
701 * 3. Calls shutdown(SHUT_RDWR) + close() on any orphaned data socket
702 * (data_fd >= 0 with no active session thread), which releases the TX
703 * descriptor reservation.
704 * 4. Sends a PAL notification to confirm the reset completed.
705 *
706 * WHAT IT DOES NOT DO:
707 * - It does NOT tear down active sessions or interrupt transfers in progress.
708 * - It does NOT toggle the network interface (unlike the manual workaround).
709 * - It is NOT a substitute for a full reboot; it only flushes buffer
710 * accounting within the current process lifetime.
711 *
712 * @param sessions Pointer to the server's session pool array
713 * @param count Number of slots in the pool (FTP_MAX_SESSIONS)
714 *
715 * @return 0 on success, -1 on partial failure (sessions still reset)
716 *
717 * @note Thread-safety: NOT safe to call while session_lock is held by caller.
718 * The server must ensure no session is mid-accept during the call.
719 * Safe to call from the HTTP API handler thread.
720 *
721 * @note WCET: Bounded by FTP_MAX_SESSIONS iterations of setsockopt() pairs.
722 * At most 2 × FTP_MAX_SESSIONS setsockopt syscalls + 1 notify.
723 *===========================================================================*/
724 
725/**
726 * @brief Reset TCP buffer accounting for idle FTP sessions.
727 *
728 * Addresses the progressive network degradation observed after transfers to
729 * the internal SSD or M.2 on PS4/PS5 (Issues #3 and #7).
730 */
731int pal_network_reset_ftp_stack(ftp_session_t *sessions, size_t count)
732{
733 if ((sessions == NULL) || (count == 0U)) {
734 return -1;
735 }
736 
737 int resets = 0;
738 
739 for (size_t i = 0U; i < count; i++) {
740 ftp_session_t *s = &sessions[i];
741 int state = atomic_load(&s->state);
742 
743 /*
744 * Only reset buffer accounting for idle (authenticated but not
745 * actively transferring) sessions. Skip sessions that are in
746 * FTP_STATE_TRANSFERRING to avoid disrupting an active data stream.
747 */
748 if (state == FTP_STATE_TRANSFERRING) {
749 continue;
750 }
751 
752 /* Reset ctrl socket buffers: zero → re-configure target value */
753 int cfd = s->ctrl_fd;
754 if (cfd >= 0) {
755 int zero = 0;
756 int sndbuf = (int)FTP_TCP_SNDBUF;
757 int rcvbuf = (int)FTP_TCP_RCVBUF;
758 
759 /*
760 * Step 1: write 0 — forces kernel to flush internal accounting
761 * and reset the buffer to the kernel minimum.
762 */
763 (void)PAL_SETSOCKOPT(cfd, SOL_SOCKET, SO_SNDBUF, &zero, sizeof(zero));
764 (void)PAL_SETSOCKOPT(cfd, SOL_SOCKET, SO_RCVBUF, &zero, sizeof(zero));
765 
766 /*
767 * Step 2: restore configured values.
768 * On OrbisOS the kernel clamps SO_SNDBUF/SO_RCVBUF to the system
769 * maximum, but the double-write forces a reallocation cycle that
770 * clears the stale accounting.
771 */
772 (void)PAL_SETSOCKOPT(cfd, SOL_SOCKET, SO_SNDBUF, &sndbuf, sizeof(sndbuf));
773 (void)PAL_SETSOCKOPT(cfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf));
774 
775 resets++;
776 }
777 
778 /*
779 * Close any orphaned data socket.
780 *
781 * A data_fd can remain open if the client disconnected mid-transfer
782 * and the session thread had not yet called
783 * ftp_session_close_data_connection(). These stale sockets hold
784 * NIC TX descriptor reservations that prevent buffer reclamation.
785 */
786 if ((s->data_fd >= 0) && (state != FTP_STATE_TRANSFERRING)) {
787 (void)shutdown(s->data_fd, SHUT_RDWR);
788 PAL_CLOSE(s->data_fd);
789 s->data_fd = -1;
790 }
791 if ((s->pasv_fd >= 0) && (state != FTP_STATE_TRANSFERRING)) {
792 PAL_CLOSE(s->pasv_fd);
793 s->pasv_fd = -1;
794 }
795 }
796 
797 return (resets >= 0) ? 0 : -1;
798}
799