本文是对不同平台不同 IP packet 过滤程序透明代理实现的一些思考,主要考虑过滤规则以及如何在代理程序中获取客户端网络请求的原目标 IP 地址。
参数、函数的解释
dst_addr:所需获取的目标地址;
client_addr:客户端请求的源地址,可通过 accept() 获取;
local_addr:代理程序绑定的地址,在 bind() 调用中传入的地址;
log_printf():类比 printf(),只不过用来输出信息至日志文件,这里没有给出具体的实现。
Netfilter
应用于 Linux kernel 2.4+。
过滤规则
TCP 过滤规则用了 REDIRECT 规则,UDP 过滤规则用了 TPROXY 规则,TCP 同样也可以使用 TPROXY 规则过滤。需要注意的是,使用 TPROXY 规则时,在写代理程序时需要 setsockopt(IP_TRANSPARENT),具体见下文 TPROXY 方案获取目标 IP 地址。
设置规则可参阅 iptables(8)、Netfilter 文档。
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
|
iptables -t nat -N TRANSPARENT_PROXY
iptables -t mangle -N TRANSPARENT_PROXY
iptables -t nat -A TRANSPARENT_PROXY -d $SERVER_IP -j RETURN
iptables -t mangle -A TRANSPARENT_PROXY -d $SERVER_IP -j RETURN
iptables -t nat -A TRANSPARENT_PROXY -d 0.0.0.0/8 -j RETURN
iptables -t mangle -A TRANSPARENT_PROXY -d 0.0.0.0/8 -j RETURN
iptables -t nat -A TRANSPARENT_PROXY -p tcp -j REDIRECT --to-ports $PROXY_PORT
ip rule add fwmark 1 lookup 100
ip route add local 0.0.0.0/0 dev lo table 100
iptables -t mangle -A TRANSPARENT_PROXY -p udp -j TPROXY --on-port $PROXY_PORT --tproxy-mark 0x1/0x1
iptables -t nat -A PREROUTING -p tcp -j TRANSPARENT_PROXY
iptables -t mangle -A PREROUTING -p udp -j TRANSPARENT_PROXY
|
获取目标 IP 地址
使用 TPROXY 方案,获取原目标 IP 地址只需简单调用 getsockname(),与下文通过 IPFW 的实现透明代理方案一样。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
static int set_transparent(int fd) {
int opt = 1;
if (setsockopt(fd, SOL_IP, IP_TRANSPARENT, &opt, sizeof(opt))) {
log_printf("%s - setsockopt(IP_TRANSPARENT): %s\n", __func__, strerror(errno));
return -1;
}
return 0;
}
static int
get_dst_addr(int fd, struct sockaddr *dst_addr, socklen_t *dst_addrlen) {
if (getsockname(fd, dst_addr, dst_addrlen)) {
log_printf("%s - getsockname(dst_addr): %s\n", __func__, strerror(errno));
return -1;
}
return 0;
}
|
使用普通方案,则可通过调用 getsockopt(SO_ORIGINAL_DST)(IPv4 地址)和 getsockopt(IP6T_SO_ORIGINAL_DST)(IPv6 地址)获取。
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
|
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <linux/netfilter_ipv4.h>
#include <linux/netfilter_ipv6/ip6_tables.h>
static int
get_dst_addr(int fd, struct sockaddr *dst_addr, socklen_t *dst_addrlen, sa_family_t sa_type) {
if (sa_type == AF_INET) {
if (getsockopt(fd, SOL_IP, SO_ORIGINAL_DST, dst_addr, dst_addrlen)) {
log_printf("%s - getsockopt(SO_ORIGINAL_DST): %s\n", __func__, strerror(errno));
return -1;
}
} else {
#ifdef IP6T_SO_ORIGINAL_DST
if (getsockopt(fd, SOL_IPV6, IP6T_SO_ORIGINAL_DST, dst_addr, dst_addrlen)) {
log_printf("%s - getsockopt(IP6T_SO_ORIGINAL_DST): %s\n", __func__, strerror(errno));
return -1;
}
#else
log_printf("The Netfilter does not support IPv6 NAT lookup.\n");
return -1;
#endif
}
return 0;
}
|
Packet Filter
出自 OpenBSD,应用于 OpenBSD 3.0+、FreeBSD 5.3+、NetBSD 3.0+、Solaris 11.3+、macOS 10.7+、iOS 和 QNX。
过滤规则
以下规则应用于 TCP 过滤,UDP 过滤可以用同样方式设置,PF 规则具体设置见 pf.conf(5)、FreeBSD 相关文档。FreeBSD 中使用的 PF 是 OpenBSD 4.5 的版本。OpenBSD 可以在 pass 规则增加 rdr-to 规则,可省去第一条规则,可参阅 OpenBSD 文档。macOS 中的 PF 与 FreeBSD 的更加相似。
1
2
3
4
5
6
7
8
9
10
11
|
rdr on lo0 proto tcp from en0 to any -> 127.0.0.1 port $PROXY_PORT
pass out quick on en0 proto tcp from any to $SERVER_IP
pass out quick on en0 route-to lo0 proto tcp from any to any
|
获取目标 IP 地址
调用 open() 以只读方式打开 PF 设备文件,如果需要动态地加入过滤规则可以使用可读写方式。之后,主要通过 ioctl(DIOCNATLOOK) 实现,其它与 PF 设备相关动态的操作可参阅 pf(4)。实际上文档中的内容还是不够详细,直接在 net/pfvar.h 看结构体的声明反而更加直观。
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
85
86
87
88
89
90
91
92
93
94
|
#include <errno.h>
#include <string.h>
#include <strings.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#ifdef __APPLE__
#define PRIVATE
#endif
#include <net/pfvar.h>
#ifdef __APPLE__
#undef PRIVATE
#endif
static int pffd = -1;
static int
pf_init(void) {
pffd = open("/dev/pf", O_RDONLY);
if (pffd) {
log_printf("%s - open(\"/dev/pf\"): %s\n", __func__, strerror(errno));
return -1;
}
if (fcntl(pffd, F_SETFD, fcntl(pffd, F_GETFD) | FD_CLOEXEC)) {
log_printf("%s - fcntl(F_SETFD): %s\n", __func__, strerror(errno));
return -1;
}
return 0;
}
static int
get_dst_addr(int fd, struct sockaddr *dst_addr, socklen_t *dst_addrlen,
struct sockaddr *client_addr, struct sockaddr *local_addr,
sa_family_t sa_type) {
#ifdef __APPLE__
#define sport sxport.port
#define dport dxport.port
#define rdport rdxport.port
#ifdef v4addr
#define v4 v4addr
#define v6 v6addr
#endif
#endif
struct pfioc_natlook pnl;
bzero(&pnl, sizeof(pnl));
pnl.af = sa_type;
if (sa_type == AF_INET) {
struct sockaddr_in *src_addr = (struct sockaddr_in *)client_addr;
struct sockaddr_in *bind_addr = (struct sockaddr_in *)local_addr;
bcopy(&src_addr->sin_addr, &pnl.saddr.v4, sizeof(pnl.saddr.v4));
pnl.sport = src_addr->sin_port;
bcopy(&bind_addr->sin_addr, &pnl.daddr.v4, sizeof(pnl.daddr.v4));
pnl.dport = bind_addr->sin_port;
} else if (sa_type == AF_INET6) {
struct sockaddr_in6 *src_addr = (struct sockaddr_in6 *)client_addr;
struct sockaddr_in6 *bind_addr = (struct sockaddr_in6 *)local_addr;
bcopy(&src_addr->sin6_addr, &pnl.saddr.v6, sizeof(pnl.saddr.v6));
pnl.sport = src_addr->sin6_port;
bcopy(&bind_addr->sin6_addr, &pnl.daddr.v6, sizeof(pnl.daddr.v6));
pnl.dport = bind_addr->sin6_port;
}
pnl.proto = IPPROTO_TCP;
pnl.direction = PF_OUT;
if (ioctl(pffd, DIOCNATLOOK, &pnl)) {
log_printf("%s - ioctl(DIOCNATLOOK): %s\n", __func__, strerror(errno));
return -1;
}
if (sa_type == AF_INET) {
struct sockaddr_in *dst_addr_in = (struct sockaddr_in *)dst_addr;
dst_addr_in->sin_family = sa_type;
bcopy(&pnl.rdaddr.v4, &dst_addr_in->sin_addr, sizeof(dst_addr_in->sin_addr));
dst_addr_in->sin_port = pnl.rdport;
} else if (sa_type == AF_INET6) {
struct sockaddr_in6 *dst_addr_in6 = (struct sockaddr_in6 *)dst_addr;
dst_addr_in6->sin6_family = sa_type;
bcopy(&pnl.rdaddr.v6, &dst_addr_in6->sin6_addr, sizeof(dst_addr_in6->sin6_addr));
dst_addr_in6->sin6_port = pnl.rdport;
}
return 0;
#ifdef __APPLE__
#undef sport
#undef dport
#undef rdport
#ifdef v4addr
#undef v4
#undef v6
#endif
#endif
}
|
IPFilter
应用于 FreeBSD 2.2+、NetBSD 1.2+、Solaris 10+、illumos 和 QNX。
IPFilter 过滤规则语法看起来与 PF 差不多,我还没仔细看,FreeBSD 文档也可以作为参考。
获取目标 IP 地址流程也差不多,打开设备文件,然后 ioctl(SIOCGNATL) ,参阅 ipnat(4)。
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
|
#include <errno.h>
#include <string.h>
#include <strings.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/ipl.h>
#include <netinet/ip_compat.h>
#include <netinet/ip_fil.h>
#include <netinet/ip_nat.h>
static int ipfilter_fd = -1;
static int
ipfilter_init(void) {
ipfilter_fd = open(IPNAT_NAME, O_RDONLY);
if (ipfilter_fd) {
log_printf("%s - open(IPNAT_NAME): %s\n", __func__, strerror(errno));
return -1;
}
if (fcntl(ipfilter_fd, F_SETFD, fcntl(ipfilter_fd, F_GETFD) | FD_CLOEXEC)) {
log_printf("%s - fcntl(F_SETFD): %s\n", __func__, strerror(errno));
return -1;
}
return 0;
}
static int
get_dst_addr(int fd, struct sockaddr *dst_addr, socklen_t *dst_addrlen,
struct sockaddr *client_addr, struct sockaddr *local_addr,
sa_family_t sa_type) {
struct natlookup nl;
struct ipfobj ipfo;
bzero(&nl, sizeof(nl));
if (sa_type == AF_INET) {
struct sockaddr_in *src_addr = (struct sockaddr_in *)client_addr;
struct sockaddr_in *bind_addr = (struct sockaddr_in *)local_addr;
nl.nl_outip = src_addr->sin_addr;
nl.nl_outport = src_addr->sin_port;
nl.nl_inip = bind_addr->sin_addr;
nl.nl_inport = bind_addr->sin_port;
} else {
log_printf("The IPFilter does not support IPv6 NAT lookup.\n");
return -1;
}
nl.nl_flags = IPN_TCP;
bzero(&ipfo, sizeof(ipfo));
ipfo.ipfo_rev = IPFILTER_VERSION;
ipfo.ipfo_size = sizeof(nl);
ipfo.ipfo_ptr = &nl;
ipfo.ipfo_type = IPFOBJ_NATLOOKUP;
if (ioctl(ipfilter_fd, SIOCGNATL, &ipfo)) {
log_printf("%s - ioctl(SIOCGNATL): %s\n", __func__, strerror(errno));
return -1;
}
struct sockaddr_in *dst_addr_in = (struct sockaddr_in *)dst_addr;
dst_addr_in->sin_family = AF_INET;
dst_addr_in->sin_addr = nl.nl_realip;
dst_addr_in->sin_port = nl.nl_realport;
return 0;
}
|
IPFW
出自 FreeBSD,应用于 FreeBSD、macOS 10.6-(macOS 10.10 完全移除),也曾被用于 Linux kernel 1.1,为 Linux 第一代防火墙。
IPFW 过滤规则暂时还没看,可参考 FreeBSD IPFW 相关文档。
原目标 IP 地址的获取与上文 Netfilter 的 TPROXY 方案一样。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
static int
get_dst_addr(int fd, struct sockaddr *dst_addr, socklen_t *dst_addrlen) {
if (getsockname(fd, dst_addr, dst_addrlen)) {
log_printf("%s - getsockname(dst_addr): %s\n", __func__, strerror(errno));
return -1;
}
return 0;
}
|
NPF
出自 NetBSD,应用于 NetBSD 6.0+。
规则设置文档
提供的部分 API 可参考文档 libnpf(3),但是并不齐全,以下用到的 npf_nat_lookup() 就没有列举在其中,直接看源码——lib/libnpf/npf.h、lib/libnpf/npf.c。
以下 get_dst_addr() 中的 dst_addr 为 inout 参数,应传入 client_addr,调用函数后其值为所需的 dst_addr。
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
|
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <net/pfil.h>
#include <npf.h>
static int
get_dst_addr(int fd, struct sockaddr *dst_addr, struct sockaddr *local_addr, sa_family_t sa_type) {
npf_addr_t *addr[2];
in_port_t port[2];
if (sa_type == AF_INET) {
struct sockaddr_in dst_addr_in = (struct sockaddr_in *)dst_addr;
struct sockaddr_in local_addr_in = (struct sockaddr_in *)local_addr;
addr[0] = (npf_addr_t *)&dst_addr_in->sin_addr;
port[0] = dst_addr_in->sin_port;
addr[1] = (npf_addr_t *)&local_addr_in->sin_addr;
port[1] = local_addr_in->sin_port;
if (npf_nat_lookup(fd, AF_INET, addr, port, IPPROTO_TCP, PFIL_OUT)) {
log_printf("%s - npf_nat_lookup(): %s\n", __func__, strerror(errno));
return -1;
}
dst_addr_in->sin_port = port[0];
local_addr_in->sin_port = port[1];
} else if (sa_type == AF_INET6) {
struct sockaddr_in6 dst_addr_in6 = (struct sockaddr_in6 *)dst_addr;
struct sockaddr_in6 local_addr_in6 = (struct sockaddr_in6 *)local_addr;
addr[0] = (npf_addr_t *)&dst_addr_in6->sin6_addr;
port[0] = dst_addr_in6->sin6_port;
addr[1] = (npf_addr_t *)&local_addr_in6->sin6_addr;
port[1] = local_addr_in6->sin6_port;
if (npf_nat_lookup(fd, AF_INET, addr, port, IPPROTO_TCP, PFIL_OUT)) {
log_printf("%s - npf_nat_lookup(): %s\n", __func__, strerror(errno));
return -1;
}
dst_addr_in6->sin6_port = port[0];
local_addr_in6->sin6_port = port[1];
}
return 0;
}
|
WFP
Windows 平台暂时没发现简单易用的透明代理方案,但是或许可以通过 Windows Filtering Platform 提供的 API 解决。
WFP 相关文档