# ai.tcp-proxy **Repository Path**: zinface/ai.tcp-proxy ## Basic Information - **Project Name**: ai.tcp-proxy - **Description**: 由 AI (Claude) 编写的简单 TCP 代理服务器,纯 C 实现,无外部依赖。 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-03-29 - **Last Updated**: 2026-03-29 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # tcp-proxy > 当你需要在两台机器之间透明转发 TCP 流量,但又不想配置 Nginx、iptables 等复杂工具时,这个单文件工具更直接。适合临时端口转发、内网穿透调试、服务访问中转等场景。 一个简单的 TCP 代理服务器,使用纯 C 实现,无外部依赖。 ## 功能 - 监听本地端口,将连接透明转发到目标主机 - 支持域名解析(IPv4 / IPv6) - 每个连接独立 fork 子进程处理,互不影响 - 双向全双工数据转发 ## 构建 ```bash gcc -Wall -o proxy main.c ``` ## 使用 ```bash ./proxy ``` | 参数 | 说明 | |------|------| | `listen_port` | 本地监听端口 | | `target_host` | 目标主机(IP 或域名) | | `target_port` | 目标端口 | ### 示例 ```bash # 将本地 18080 端口代理到 example.com:80 ./proxy 18080 example.com 80 # 将本地 3306 代理到远程 MySQL ./proxy 3306 db.internal 3306 ``` ## 架构 ``` client ──────────────────────────────── proxy ──────────────── target ← connect to :listen_port fork() connect to host:port → ← fork child process ─────────────────────────────────────────── ─► parent: client → target (forward request) child: target → client (forward response) ``` 每个新连接的处理流程: 1. `accept()` 收到客户端连接 2. `fork()` 派生子进程处理该连接(主进程继续 accept) 3. 子进程连接目标服务器 4. 子进程内再次 `fork()`: - **parent**:将客户端数据转发到目标(`client → target`) - **child**:将目标数据转发回客户端(`target → client`) 5. 两个方向任意一方关闭连接后,整个会话结束 ## FAQ ### Q: 子进程关闭 `listen_fd` 和主进程关闭 `client_fd`,有什么区别? `fork()` 后,父子进程都持有所有 fd 的副本,每个 fd 在内核中有引用计数,引用计数归零才真正关闭。 ``` fork() 之后: 主进程持有:listen_fd ✓ client_fd ✗ (需关闭) 子进程持有:listen_fd ✗ (需关闭) client_fd ✓ ``` **子进程关闭 `listen_fd`:** 子进程只负责转发数据,不需要接受新连接。不关闭只是浪费 fd, 但随着并发连接增多会耗尽进程的 fd 资源。 **主进程关闭 `client_fd`:** 主进程已将该连接交给子进程处理,自己不再使用。 如果主进程不关闭,即使子进程 exit,内核仍因主进程持有引用而不会关闭该连接, 导致客户端连接无法正常断开,并造成 fd 泄漏。 两者本质相同——都是 fork 后及时关闭自己不需要的 fd——但后果严重程度不同: 前者是资源浪费,后者会导致连接行为错误。 --- ### Q: 为什么要设置 `SO_REUSEADDR`? 程序退出后,内核会将该端口保留在 `TIME_WAIT` 状态一段时间(通常 60 秒), 防止旧连接的延迟数据包干扰新连接。这段时间内,默认情况下重新绑定同一端口会报 `Address already in use`。 设置 `SO_REUSEADDR` 后,内核允许在 `TIME_WAIT` 期间重新绑定端口, 开发和测试时重启服务不需要等待。 ```c int opt = 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof opt); ``` --- ### Q: `sigchld_handler` 中为什么要循环调用 `waitpid`? 如果多个子进程在同一时刻退出,操作系统只会向父进程发送一次 `SIGCHLD` 信号 (信号不排队,多个相同信号会合并)。如果只调用一次 `waitpid`, 其余退出的子进程会变成僵尸进程(zombie),占用进程表项。 循环调用直到返回 `-1`(无更多子进程),确保一次信号处理所有已退出的子进程: ```c while (waitpid(-1, NULL, WNOHANG) > 0); ``` `WNOHANG` 表示非阻塞:如果没有已退出的子进程,立即返回而不挂起父进程。 --- ### Q: `getaddrinfo` 相比手动填 `sockaddr_in` 有什么优势? 手动方式只支持 IPv4,且无法解析域名: ```c // 旧方式,仅 IPv4 struct sockaddr_in addr; addr.sin_family = AF_INET; inet_pton(AF_INET, "93.184.216.34", &addr.sin_addr); // 必须是 IP ``` `getaddrinfo` 的优势: - 自动解析域名(DNS 查询) - 同时支持 IPv4 和 IPv6(`AF_UNSPEC`) - 返回链表,可遍历多个地址直到连接成功 - 线程安全(相比已废弃的 `gethostbyname`) --- ### Q: `forward()` 为什么用内层 `while` 循环写入? `write()` 不保证一次写完所有数据,尤其在写入大块数据或对端接收缓冲区满时, 可能只写入部分字节并返回实际写入数量。 ```c while (sent < n) { ssize_t w = write(dst, buf + sent, (size_t)(n - sent)); if (w <= 0) return; sent += w; } ``` 内层循环确保读到的 `n` 字节被完整写出,避免数据丢失。 --- ### Q: 为什么 `handle_client` 内部使用 `exit()` 而不是 `return`? `handle_client` 是在子进程中调用的。子进程完成转发后必须退出, 不能 `return` 回到 `main` 的 `for` 循环继续 `accept` 新连接—— 那样会出现多个进程同时 `accept` 同一个 listen socket 的混乱情况。 使用 `exit()` 确保子进程在完成任务后立即终止。 --- ### Q: 这个代理能处理多少并发连接? 每个连接需要 fork 两次(main 中一次,handle_client 中一次),即每个连接消耗 **2 个进程**。 Linux 默认每个用户最多 ~1024 个进程(`ulimit -u`),理论并发上限约 **500 个连接**。 如需更高并发,应改用 `epoll` + 非阻塞 I/O 的事件驱动模型,单进程即可处理数万连接。 ## 限制 - 基于 `fork` 模型,高并发场景下进程数量较多 - 不支持 TLS/SSL - 不支持 SOCKS / HTTP CONNECT 协议 - 仅支持 Linux / macOS(POSIX)