透明代理的旁路由实现、原理和常见的误区
首先介绍旁路由是什么,旁路由也叫旁路网关,比较典型的架构是通过一个单网口的设备(N1盒子、开发板等)接入主路由,然后在主路由的的DHCP选项里将LAN口网关设置为该设备的IP(也就是所谓的“网关互指”,其实根本就没有互指,主路由的网关依然是原来的网关,只是接入主路由的设备的网关变成了旁路由),流量经由该设备分流,实现科学研究的目的。
提醒,使用旁路由时必须关闭主路由的硬件加速,否则会出现各种各样的问题!!!
具体的网络拓扑如下图所示。
(图片来自: Openwrt 作为旁路网关(不是旁路由、单臂路由)的终极设置方法,破解迷思 - 少数派 (sspai.com) )
再分析流量的走向,在理想情况下,我们会有下图的流量走向。
(图片来自: 旁路由的原理与配置一文通 - Eason Yang’s Blog )
可是事实真的如此么?其实不然,要达到这种理想情况,目前大部分的教程给出的方法都多多少少存在的问题,实际上的网络走向不会是这样,下行流量也会经过旁路由,导致跑国内 speedtest 的时候旁路由的利用率暴涨。可以用 htop 或者 btop观察到这一点。
造成这种情况的有两种原因:
- 在旁路由上进行了SNAT,导致所有的连接都必须经过旁路由,即
iptables -t nat -I POSTROUTING -j MASQUERADE
规则 - 透明代理的实现原理决定了实现透明代理必须将两条TCP流拼接在一起(UDP未验证,不是本文的重点),这样所有的连接都也必须经过旁路由
第一点的情况比较常见,其实也是情有可原,因为如果不在旁路由上开启SNAT,很可能主路由对流量的处理会出现问题,即:不开启透明代理的情况下无法访问WAN区域的主机。
造成此情况的原因有以下:
- 由于主路由设置了
net.bridge.bridge-nf-call-*tables = 1
,导致无线流量未被正确的NAT,此时有线网络可以正常访问WAN区域的主机。原因见: 关于旁路由设置后,主路由WIFI无法上网的问题_旁路由作为网关不能上网_锦夏挽秋的博客-CSDN博客 - 未开启IP转发,即
net.ipv4.ip_forward = 0
,导致旁路由在收到其他设备发送的数据包时不会转发给主路由。原因见: networking - What exactly happens when I enable net.ipv4.ip_forward=1? - Unix & Linux Stack Exchange - 开启了IP转发,但是主路由认为该数据包不合法,或对该数据包的处理不正确,因为旁路由的MAC对应了多个来源地址,建议使用Openwrt,不过这种情况比较少见
接下来说说第二点,即在正确设置了上面的内核参数的情况下,为什么下行流量还是会经过旁路由。
为了证明这一点,我们需要了解透明代理的原理:
简单来说,透明代理就相当于DNAT,其将流量重定向到本地的服务,由本地的服务再发起真正的连接,然后其将两条连接拼在一起,起到了中间人的作用。
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);
}
}
}
测试:
➜ ~ 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
➜ ~ 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/ 测速,bashtop可以看到跑满流量。
解决办法:
使用ipset前置分流,对于大陆IP的流量直接由内核转发,不流入透明代理。
v2raya hooks:
#!/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:
#!/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
参考链接:
Openwrt 作为旁路网关(不是旁路由、单臂路由)的终极设置方法,破解迷思 - 少数派 (sspai.com)
旁路由的原理与配置一文通 - Eason Yang’s Blog
关于旁路由设置后,主路由WIFI无法上网的问题_旁路由作为网关不能上网_锦夏挽秋的博客-CSDN博客
networking - What exactly happens when I enable net.ipv4.ip_forward=1? - Unix & Linux Stack Exchange
Transparent proxy support — The Linux Kernel documentation
tproxy package - github.com/e1732a364fed/v2ray_simple/netLayer/tproxy - Go Packages