之前我利用redsocks项目,在网关上实现了透明SOCKS5代理。

redsocks要求在网关上利用iptables将TCP流量改写给redsocks端口,这样redsocks收到流量后就会按照SOCKS5协议向代理服务器转发数据。

实现redsocks的唯一难点就是redsocks如何知道原始的TCP目标地址,因为经过iptables做REDIRECT改写后,数据包的目标地址已经被修改为redsocks自身的监听地址了。

既然redsocks能做到,我们也一定有办法做到,答案就是linux的getsockopt系统调用,做过linux网络编程的同学一定都非常熟悉了。getsockopt支持一个选项,可以获取到socket改写前的原始目标地址,包括ip和port。

我用Golang实现了一个demo:https://github.com/owenliang/go-orig-dst/blob/master/main.go,下面我简单讲解一下。

体验demo

这个demo类似于redsocks,只不过它收到TCP连接后只是打印了一下原始目标IP地址,然后就关闭了连接。

首先运行程序,它监听在8080端口:

Shell
1
go run main.go

然后telnet直连它,将打印出目标IP地址就是demo的监听地址:

Shell
1
2
3
4
5
6
pi@raspberrypi:~ $ telnet localhost 8080
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Connection closed by foreign host.

连接被立即断开,这符合demo程序的逻辑。

在demo服务端,打印出了目标地址:

Shell
1
2
pi@raspberrypi:~/raid1/golang/go-orig-dst $ go run main.go
原始目标地址:127.0.0.1:8080

接下来,我们要测试利用iptables做REDIRECT拦截TCP流量到Demo服务端,再看一下能否获取到原始的目标地址。

因此,先后添加如下2条iptables规则:

Shell
1
2
sudo iptables -t nat -A OUTPUT -p tcp -d 127.0.0.1 --dport 8080 -j ACCEPT
sudo iptables -t nat -A OUTPUT -p tcp -j REDIRECT --to-ports 8080
  • 从本机发往本机8080端口的TCP流量直接放行,也就是telnet localhost 8080将直连到Demo。
  • 而从本机发往本机非8080端口的流量,将做REDIRECT改写给8080。

因此,Demo能收到2种流量:

  • telnet localhost 8080的直连流量。
  • telnet localhost 7000这样的流量,将被改写给localhost 8080。

这时候,我们尝试尝试telnet localhost 7000:

Shell
1
2
3
4
5
6
pi@raspberrypi:~ $ telnet localhost 7000
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Connection closed by foreign host.

会发现Demo打印了如下内容:

Shell
1
2
pi@raspberrypi:~/raid1/golang/go-orig-dst $ go run main.go
原始目标地址:127.0.0.1:7000

也就是说,Demo成功获取了REDIRECT之前的原始目标地址,而不是127.0.0.1:8080这个改写后的地址。

实现Demo

首先就是监听TCP连接,一旦得到TCP连接就获取socket文件描述符(fd),因为后续要调用getsockopt获取fd的原始目标地址:

Go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if listener, err = net.Listen("tcp4", "127.0.0.1:8080"); err == nil {
var conn net.Conn
for {
// 接收连接
if conn, err = listener.Accept(); err != nil {
break
}
 
// 反射到TCP连接
var tcpConn *net.TCPConn
tcpConn, _ = conn.(*net.TCPConn)
 
// 获取socket文件描述符
var file *os.File
file, _ = tcpConn.File()
fd := file.Fd()

阅读后续内容?

你必须付费加入我的知识星球,为有效知识付费是对作者最好的回报。

二维码见下方 或者 右侧边栏。