软路由折腾记录一

透明代理

在购买了小主机J1900之后, 我在上面安装了OpenWrt, 一切都很正常, 我搭建了Clash, 配置了iptables使TCP/UDP走透明代理.

TCP透明代理

TCP透明代理比较简单, 因为TCP有状态, 所以透明代理成本较低, iptables提供重定向操作.

1
iptables -t nat -A CLASH -p tcp -j REDIRECT --to-ports 7892

规则将tcp所有流量重定向到本地7892, iptables只需要在SYN包时记录下原始目标地址, 7892端口accept之后通过”SO_ORIGINAL_DST”即可拿到原始目标地址.

UDP透明代理

UDP没有状态, 所以想和TCP进行透明代理是不可能的, 每个连接都像TCP一样保存原始目标地址, 成本较大.

1
2
3
iptables -t mangle -A CLASH -p udp -j TPROXY --on-port 7892 --tproxy-mark 0x111
ip rule add fwmark 0x111 table 0x162
ip route add local 0.0.0.0/0 dev lo table 0x162

UDP透明代理需要三条规则, 第一条使用iptables提供的tproxy模块, 将udp流量重定向到本地7892端口. 比较有意思的是tproxy不修改报文, 但是依旧可以完成重定向任务. 在报文匹配这条规则时, tproxy会查找本地是否开启7892端口, 并且该socket是否设置了”IP_TRANSPARENT”属性, 如果符合则将socket与报文做关联, 修改报文的结构体(skb->sock=sk).
但是在报文不做修改的情况下, 因为目的地址不是本机, 所以路由选择的时候, 该报文会被forward而不会进入本地协议栈进行处理, 那么关联socket的修改就没有意义了. 所以后面两条路由规则, 就是将报文送入本地协议栈进行处理.
tproxy会给每个匹配的报文打上标记”0x111”, 第二条路由规则表示带有”0x111”标记的报文使用”0x162”路由表. 第三条创建”0x162”路由表, 表里的规则将流量本地回环网卡lo, 这样流量顺利进入协议栈, 并且关联修改让它顺利被7892端口接收.

example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
#include <stdio.h>
#include <netinet/in.h>
#include <memory.h>
#include <arpa/inet.h>

int main() {
int server_fd, ret;
struct sockaddr_in ser_addr;

server_fd = socket(AF_INET, SOCK_DGRAM, 0);

if(server_fd < 0) {
printf("create socket fail!\n");
return -1;
}

int enable = 1;

if (setsockopt(server_fd, SOL_IP, IP_RECVORIGDSTADDR, (const char*)&enable, sizeof(enable)) != 0) {
printf("set socket op fail!\n");
return -1;
}

if (setsockopt(server_fd, SOL_IP, IP_TRANSPARENT, (const char*)&enable, sizeof(enable)) != 0) {
printf("set socket op fail!\n");
return -1;
}

memset(&ser_addr, 0, sizeof(ser_addr));

ser_addr.sin_family = AF_INET;
ser_addr.sin_addr.s_addr = htonl(INADDR_ANY);
ser_addr.sin_port = htons(7892);

ret = bind(server_fd, (struct sockaddr*)&ser_addr, sizeof(ser_addr));
if(ret < 0)
{
printf("socket bind fail!\n");
return -1;
}

char buffer[256];
unsigned char bytes[16*1024];

struct sockaddr_in srcIpAddr, dstIpAddr;
int dstPort;

struct iovec iov;

iov.iov_base = bytes;
iov.iov_len = sizeof(bytes)-1;

struct msghdr mh;

mh.msg_name = &srcIpAddr;
mh.msg_namelen = sizeof(struct sockaddr_in);
mh.msg_control = buffer;
mh.msg_controllen = 100;
mh.msg_iovlen = 1;
mh.msg_iov = &iov;

int res = recvmsg(server_fd, &mh, 0);

if (res <= 0) {
printf("recv msg failed");
return -1;
}

for(struct cmsghdr *msg = CMSG_FIRSTHDR(&mh); msg != NULL; msg = CMSG_NXTHDR(&mh, msg))
{
if(msg->cmsg_level != SOL_IP || msg->cmsg_type != IP_ORIGDSTADDR)
continue;

memcpy(&dstIpAddr, CMSG_DATA(msg), sizeof(struct sockaddr_in));
dstPort = ntohs(dstIpAddr.sin_port);

char str[INET_ADDRSTRLEN];
inet_ntop(AF_INET, &(dstIpAddr.sin_addr), str, INET_ADDRSTRLEN);

printf("%s:%d", str, dstPort);
}

return 0;
}

Docker

简单的翻墙使用, 让我觉得浪费了J1900的性能, 我突发奇想, 我想在上面装Docker! 当我在google反复搜索, 貌似出来的答案都是Docker里面装OpenWrt, 没有人在OpenWrt上装Docker, 唯一有价值的帖子是这篇Docker engine on OpenWrt.
Docker现在只存在于master分支, 如果想用需要自己编译, 于是我开始搭环境进行编译.

1
2
3
4
5
6
7
git clone https://git.openwrt.org/openwrt/openwrt.git
cd openwrt
./scripts/feeds update -a
./scripts/feeds install -a
make menuconfig
make -j8 download
make -j8

编译会遇到很多坑, 也和网络环境有关, 因为编译会下载大量的源码, 所以我全程使用了proxychains.

组件

make menuconfig时会让你选择组件, 此时我很自然的只勾选了docker-ce, 然而编译的结果是整个系统只有docker-ce! 自己编译是不会有软件源的, 因为内核版本不兼容, 你从OpenWrt的Snapshot源(官方dev源, 每天自动编译)下载的包是装不上, 所以连iptables-mod-tproxy都没有.
我勾选了tproxy模块重新编译后, 发现自带的web UI没有, 我开始暴躁了起来. 最后我发现官方发出的稳定版本会附上编译时的配置, 所以在make menuconfig之前先下载下来:

1
curl https://downloads.openwrt.org/releases/19.07.3/targets/x86/64/config.buildinfo > .config

再进行make menuconfig会在官方的配置上进行定制, 我勾选了所需要的模块, 编译进行的很顺利.

代理

我习惯使用proxychains进行代理, 它使用LD_PRELOAD进行socket相关的hook, 支持TCP/UDP. 不过在编译Python3时, 出现了错误:

1
2
Fatal: You must get working getaddrinfo() function.
or you can specify "--disable-ipv6".

搜索之后发现是proxychains的问题, 但是我没有详细探究原因, 我转而使用:

1
2
export http_proxy="http://127.0.0.1:8080"
export https_proxy="http://127.0.0.1:8080"

因为编译的下载大多都会使用该环境变量, 所以也没有发生下载缓慢的问题.

问题

之前的文章已经写了详细安装过程, 这里不赘述了, 使用时发现了两个问题.

  • 无法使用官方源, 要安装screen等软件需要自行编译
  • Docker可正常使用, tproxy无法正常工作

第一个问题完全可以忍受, 因为编译后的目录结构与官方源完全相同, 所以可以上传到自己的VPS, 然后修改opkg配置文件, 使用自建源. 第二个问题相当诡异, 调试了几天依旧找不到原因, 我一度以为是OpenWrt的bug, 甚至发朋友圈怒斥之.

转变

最后我起了个念头, 我想把系统换成Debian, 通过DIV将它改造成软路由. 大概想了一下, 无非是搭建DNS/DHCP, 桥接网卡、开启ip中转, 设置iptables的DNAT规则. 我重新燃起了热情, 为即将可以使用舒适稳定的系统而躁动不安, 此时的我显然没有意识到有一个bug在前方等待着我.