如何为Node.js net.connect()的TCP数据包设置标记以实现IP规则路由
在Node.js的net.connect()中为TCP连接设置SO_MARK标记
要解决Web代理流量源IP单一导致路由规则失效的问题,核心是利用Linux内核的SO_MARK套接字选项为不同目标的TCP连接打上标记,让VPN服务器的路由规则可以基于标记区分流量。Node.js的net模块本身没有直接暴露该API,但可以通过底层句柄结合系统调用实现。
前提条件
- 你的VPN服务器运行在Linux系统上(
SO_MARK是Linux特有的特性) - Node.js进程拥有足够权限:要么以root用户运行,要么给Node.js添加
CAP_NET_ADMIN能力(执行setcap cap_net_admin+ep /path/to/node)
方案一:使用专用Socket库(推荐)
使用@socketsupply/socket库,它封装了底层socket系统调用,简化标记设置流程:
- 安装依赖
npm install @socketsupply/socket
- 实现带标记的连接函数
const net = require('net'); const { Socket: SupplySocket, SOL_SOCKET, SO_MARK } = require('@socketsupply/socket'); // 创建带SO_MARK标记的TCP连接 function connectWithMark(host, port, mark) { return new Promise((resolve, reject) => { const socket = net.connect(port, host, () => { // 获取底层socket的文件描述符 const fd = socket._handle.fd; const supplySocket = new SupplySocket(fd); // 设置SO_MARK标记 supplySocket.setOption(SOL_SOCKET, SO_MARK, mark); resolve(socket); }); socket.on('error', reject); }); } // 连接extern/target1,标记为1 connectWithMark('extern/target1', 80, 1) .then(socket => { socket.write('GET / HTTP/1.1\r\nHost: extern/target1\r\n\r\n'); socket.on('data', data => console.log(data.toString())); }) .catch(err => console.error('连接失败:', err)); // 连接extern/target2,标记为2 connectWithMark('extern/target2', 80, 2) .then(socket => { socket.write('GET / HTTP/1.1\r\nHost: extern/target2\r\n\r\n'); socket.on('data', data => console.log(data.toString())); }) .catch(err => console.error('连接失败:', err));
方案二:直接调用系统调用(底层实现)
如果不想引入第三方库,可以用ffi-napi直接调用Linux的setsockopt系统函数:
- 安装依赖
npm install ffi-napi ref-napi
- 实现带标记的连接函数
const net = require('net'); const ffi = require('ffi-napi'); const ref = require('ref-napi'); // Linux套接字常量 const SOL_SOCKET = 1; const SO_MARK = 36; // 多数Linux内核版本中该值为36,可通过`grep SO_MARK /usr/include/linux/socket.h`确认 // 封装setsockopt系统调用 const libc = ffi.Library(null, { 'setsockopt': ['int', ['int', 'int', 'int', ref.refType('int'), 'int']] }); // 给指定fd的socket设置SO_MARK标记 function setSocketMark(fd, mark) { const markBuf = ref.alloc('int', mark); return libc.setsockopt(fd, SOL_SOCKET, SO_MARK, markBuf, ref.sizeof.int); } // 创建带SO_MARK标记的TCP连接 function connectWithMark(host, port, mark) { return new Promise((resolve, reject) => { const socket = net.connect(port, host, () => { const fd = socket._handle.fd; const result = setSocketMark(fd, mark); if (result !== 0) { reject(new Error('设置SO_MARK标记失败:权限不足或系统不支持')); } else { resolve(socket); } }); socket.on('error', reject); }); } // 使用示例同方案一 connectWithMark('extern/target1', 80, 1) .then(socket => { /* 处理连接 */ }) .catch(err => console.error(err));
路由规则配置(VPN服务器端)
设置好标记后,需要在VPN服务器上配置基于标记的路由规则,示例:
# 为标记1的流量创建路由表100,并指向对应边缘路由器 ip rule add fwmark 1 table 100 ip route add default via <边缘路由器1的IP> dev <VPN接口> table 100 # 为标记2的流量创建路由表200,并指向对应边缘路由器 ip rule add fwmark 2 table 200 ip route add default via <边缘路由器2的IP> dev <VPN接口> table 200
注意事项
socket._handle是Node.js内部属性,不同版本可能存在兼容性差异,生产环境需测试目标Node.js版本。- 非Linux系统(如Windows、BSD)不支持
SO_MARK,该方案仅适用于Linux环境。
内容的提问来源于stack exchange,提问作者Tom Freudenberg
相关产品推荐
相关产品推荐

