Loading... 首先介绍旁路由是什么,旁路由也叫旁路网关,比较典型的架构是通过一个单网口的设备(N1盒子、开发板等)接入主路由,然后在主路由的的DHCP选项里将LAN口网关设置为该设备的IP(也就是所谓的“网关互指”,其实根本就没有互指,主路由的网关依然是原来的网关,只是接入主路由的设备的网关变成了旁路由),流量经由该设备分流,实现科学研究的目的。 **提醒,使用旁路由时必须关闭主路由的硬件加速,否则会出现各种各样的问题!!!** 具体的网络拓扑如下图所示。 ![](https://cdn.gta5pdx.cn/usr/uploads/2024/03/1959196470.png) (图片来自: [Openwrt 作为旁路网关(不是旁路由、单臂路由)的终极设置方法,破解迷思 - 少数派 (sspai.com) ](https://sspai.com/post/68511 "Openwrt 作为旁路网关(不是旁路由、单臂路由)的终极设置方法,破解迷思 - 少数派 (sspai.com)")) 再分析流量的走向,在**理想情况**下,我们会有下图的流量走向。 ![请输入图片描述](https://cdn.gta5pdx.cn/usr/uploads/2024/03/3895913796.png) (图片来自: [旁路由的原理与配置一文通 - Eason Yang’s Blog ](https://easonyang.com/posts/transparent-proxy-in-router-gateway/ "旁路由的原理与配置一文通 - Eason Yang’s Blog")) 可是事实真的如此么?其实不然,要达到这种理想情况,目前大部分的教程给出的方法都多多少少存在的问题,实际上的网络走向不会是这样,下行流量也会经过旁路由,导致跑国内 speedtest 的时候旁路由的利用率暴涨。可以用 htop 或者 btop观察到这一点。 造成这种情况的有两种原因: 1. 在旁路由上进行了SNAT,导致所有的连接都必须经过旁路由,即`iptables -t nat -I POSTROUTING -j MASQUERADE`规则 2. 透明代理的实现原理决定了实现透明代理必须将两条TCP流拼接在一起(UDP未验证,不是本文的重点),这样所有的连接都也必须经过旁路由 第一点的情况比较常见,其实也是情有可原,因为如果不在旁路由上开启SNAT,很可能主路由对流量的处理会出现问题,即:不开启透明代理的情况下无法访问WAN区域的主机。 造成此情况的原因有以下: * 由于主路由设置了`net.bridge.bridge-nf-call-*tables = 1`,导致无线流量未被正确的NAT,此时有线网络可以正常访问WAN区域的主机。原因见: [关于旁路由设置后,主路由WIFI无法上网的问题_旁路由作为网关不能上网_锦夏挽秋的博客-CSDN博客 ](https://blog.csdn.net/qq1337715208/article/details/122271608 "关于旁路由设置后,主路由WIFI无法上网的问题_旁路由作为网关不能上网_锦夏挽秋的博客-CSDN博客") * 未开启IP转发,即`net.ipv4.ip_forward = 0`,导致旁路由在收到其他设备发送的数据包时不会转发给主路由。原因见: [networking - What exactly happens when I enable net.ipv4.ip_forward=1? - Unix & Linux Stack Exchange ](https://unix.stackexchange.com/questions/673573/what-exactly-happens-when-i-enable-net-ipv4-ip-forward-1 "networking - What exactly happens when I enable net.ipv4.ip_forward=1? - Unix & Linux Stack Exchange") * 开启了IP转发,但是主路由认为该数据包不合法,或对该数据包的处理不正确,因为旁路由的MAC对应了多个来源地址,建议使用Openwrt,不过这种情况比较少见 接下来说说第二点,即在正确设置了上面的内核参数的情况下,为什么下行流量还是会经过旁路由。 为了证明这一点,我们需要了解透明代理的原理: 简单来说,透明代理就相当于DNAT,其将流量重定向到本地的服务,由本地的服务再发起真正的连接,然后其将两条连接拼在一起,起到了中间人的作用。 C语言示例: ```c #include <arpa/inet.h> #include <sys/types.h> #include <sys/socket.h> #include <linux/netfilter_ipv4.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <signal.h> #define PORT 12345 #define ADDR "127.0.0.1" #define BUFFER_SIZE 4096 /* # 设置策略路由 ip rule add fwmark 1 table 100 ip route add local 0.0.0.0/0 dev lo table 100 # PREROUTING iptables -t mangle -N V2RAY iptables -t mangle -A V2RAY -d 127.0.0.1/32 -j RETURN # 透明代理响应局域网(本机)的请求 iptables -t mangle -A V2RAY -d 192.168.0.0/16 -j RETURN # 透明代理响应局域网(本机)的请求 iptables -t mangle -A V2RAY -p tcp -j TPROXY --on-port 12345 --tproxy-mark 1 # 给 TCP 打标记 1,转发至 12345 端口 iptables -t mangle -A PREROUTING -j V2RAY # 应用规则 # OUTPUT iptables -t mangle -N V2RAY_MASK iptables -t mangle -A V2RAY_MASK -j RETURN -m mark --mark 0xff # 直连 SO_MARK 为 0xff 的流量,此规则目的是避免代理本机(网关)流量出现回环问题 iptables -t mangle -A V2RAY_MASK -p tcp -j MARK --set-mark 1 # 给 TCP 打标记,重路由 iptables -t mangle -A OUTPUT -j V2RAY_MASK # 应用规则 */ void forward_data(int from_sock, int to_sock) { char buffer[BUFFER_SIZE]; ssize_t bytes_read; while ((bytes_read = read(from_sock, buffer, sizeof(buffer))) > 0) { ssize_t bytes_written = write(to_sock, buffer, bytes_read); if (bytes_written <= 0) { perror("write"); break; } } close(from_sock); close(to_sock); } void handle_client(int client_sock, struct sockaddr_in client_addr) { int server_sock; struct sockaddr_in intended_dest_addr; socklen_t dest_addr_len = sizeof(intended_dest_addr); server_sock = socket(AF_INET, SOCK_STREAM, 0); if (server_sock == -1) { perror("error creating server socket"); close(client_sock); return; } // set so_mark int mark = 0xff; setsockopt(server_sock, SOL_SOCKET, SO_MARK, &mark, sizeof(mark)); getsockname(client_sock, (struct sockaddr *)&intended_dest_addr, &dest_addr_len); // REDIRECT mode: // getsockopt (client_fd, SOL_IP, SO_ORIGINAL_DST, &intended_dest_addr, &dest_addr_len); printf("they think they're talking to %s:%d\n", inet_ntoa(intended_dest_addr.sin_addr), ntohs(intended_dest_addr.sin_port)); if (connect(server_sock, (struct sockaddr *)&intended_dest_addr, dest_addr_len) == -1) { perror("error connecting to destination server"); close(client_sock); close(server_sock); return; } // Create separate threads or processes to handle data forwarding in both directions if (fork() == 0) { forward_data(client_sock, server_sock); exit(0); } if (fork() == 0) { forward_data(server_sock, client_sock); exit(0); } exit(0); } int main(void) { signal(SIGCHLD, SIG_IGN); // kernel reap the exited child int listener_fd = socket(AF_INET, SOCK_STREAM, 0); int value = 1; setsockopt(listener_fd, SOL_IP, IP_TRANSPARENT, &value, sizeof(value)); // 允许发送来源地址非本机的数据报 struct sockaddr_in name; name.sin_family = AF_INET; // TCP name.sin_port = htons(PORT); // 本地监听端口 inet_pton(AF_INET, ADDR, &name.sin_addr.s_addr); // 绑定的IP地址 if (bind(listener_fd, (struct sockaddr *)&name, sizeof(name)) < 0) perror("bind failed"); if (listen(listener_fd, 10) < 0) perror("listen failed"); printf("now TPROXY-listening on %s:%d", ADDR, PORT); printf("...\nbut actually accepting any TCP SYN with dport 80, regardless of" " dest IP, that hits the loopback interface!\n\n"); while (1) { struct sockaddr_in client_addr; socklen_t client_addr_len = sizeof(client_addr); int client_fd = accept(listener_fd, (struct sockaddr *)&client_addr, &client_addr_len); pid_t pid = fork(); if (pid < 0) { perror("Error forking"); exit(1); } else if (pid == 0) { printf("accepted socket from %s:%d; ", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port)); puts("handling requst"); handle_client(client_fd, client_addr); } } } ``` 测试: ```apache ➜ ~ sudo ./transparent_proxy now TPROXY-listening on 127.0.0.1:12345... but actually accepting any TCP SYN with dport 80, regardless of dest IP, that hits the loopback interface! accepted socket from 192.168.204.134:49852; handling requst they think they're talking to 1.1.1.1:80 ``` ```html ➜ ~ curl 1.1.1.1 <html> <head><title>301 Moved Permanently</title></head> <body> <center><h1>301 Moved Permanently</h1></center> <hr><center>cloudflare</center> </body> </html> ``` 旁路由真机验证: 使用 [https://10000.gd.cn/ ](https://10000.gd.cn/ "https://10000.gd.cn/") 测速,bashtop可以看到跑满流量。 ![请输入图片描述](https://cdn.gta5pdx.cn/usr/uploads/2024/03/748045925.png) 解决办法: 使用ipset前置分流,对于大陆IP的流量直接由内核转发,不流入透明代理。 ```bash #!/bin/bash # parse the arguments for i in "$@"; do case $i in --transparent-type=*) TYPE="${i#*=}" shift ;; --stage=*) STAGE="${i#*=}" shift ;; --v2raya-confdir=*) CONFDIR="${i#*=}" shift ;; -*|--*) echo "Unknown option $i" shift ;; *) ;; esac done # print $TYPE, $STAGE and $CONFDIR echo "Transparent Type = ${TYPE}" echo "Stage = ${STAGE}" echo "Config Directory = ${CONFDIR}" if [ "$STAGE" == "post-stop" ]; then #清除规则 echo "chnroute: purging rules" iptables -t nat -D TP_RULE -m set --match-set chnroute dst -j RETURN 2>/dev/null exit 0 elif [ "$STAGE" == "post-start" ]; then echo "chnroute: adding rules" sleep 1 #创建规则 iptables -t nat -I TP_RULE -m set --match-set chnroute dst -j RETURN 2>/dev/null fi ``` 创建ipset: ```bash #!/usr/bin/env bash set -ex GREEN='\033[0;32m' NC='\033[0m' CHNROUTE_URL="http://ftp.apnic.net/apnic/stats/apnic/delegated-apnic-latest" echo -e "${GREEN}>>> downloading chnroute...${NC}" curl -L -o cn.zone.raw $CHNROUTE_URL if [[ -e "cn.zone.raw" ]]; then # create ipset set ipset -q create chnroute hash:net || true ipset create chnroute_new hash:net # parse chnroute file, add to temp set awk -F\| '/CN\|ipv4/ { printf("%s/%d\n", $4, 32-log($5)/log(2)) }' cn.zone.raw > cn.zone cat cn.zone | xargs -I ip ipset add chnroute_new ip # swap old and new set ipset swap chnroute_new chnroute ipset destroy chnroute_new echo -e "${GREEN}>>> update chnroute done!!!${NC}" rm cn.zone.raw else echo "download chnroute file failed!!!" fi ``` 最后修改:2024 年 03 月 09 日 © 允许规范转载 赞 如果觉得我的文章对你有用,请随意赞赏