你的Linux上有一个超酷的 TCP 代理!

TL;DR 本文只用 Linux 自带的工具实现了一个可以将 TCP 流量打印出来的中间人代理程序(透明的)。脚本来源于 这里 。本文是对其工作原理的解释。

一般我们想知道一个 TCP 连接收发的内容时,会用 wireshark/tcpdump 这种工具,来把包抓下来分析查看。我开发 IRedis 的时候,就经常有这种需求,需要看看我的客户端对 server 到底发送了什么和接收了什么。用抓包工具不太方便,信息比较杂乱,操作也比较繁琐。在服务器上部署抓包工具的时候,还要给这些工具特殊权限。

其实这种需求可以不抓包的。因为 Redis 这种程序(RESP3协议)是基于 TCP 的,我们可以做一个代理,将发往 redis-server 的流量打印出来(输出到 stdout),然后再发给 redis-server。将 redis-server 的 Response 打印出来,再返回给客户端。相当于是一个中间人。

实现正向代理和反向代理都可以,反向代理不需要客户端支持,只是开一个端口,将所有发往这个端口的流量转发到 redis-server,所以我们这里用反向代理来实现。

在这篇文章中,我们只用 Linux 自带的工具, nc 来实现这个代理。还要用到 named pipe (因为 Linux 的 pipe 只支持单项传递,这里我们需要一个双向的代理),sed(用来对输出稍作格式化), trap (负责在退出的时候清理 named pipe)。

工作原理

我可以用一个最小化的版本解释这个中间人代理的工作原理,然后我们再来处理格式化输出。

这个中间人代理只需要两个 nc ,一个负责接收客户端那边的输出,将这个输入传给另一个 nc ;另一个 nc 接收前一个  nc 的输入,然后传给 Server(比如 redis-server)。Server 传回来的信息也一样,先进入后一个 nc (后面称呼它为 nc2 好了),然后传给前一个 nc (后面叫做 nc1)。在 nc1 -> nc2 和 nc2 -> nc1 的过程中,就可以把传递的东西给打印出来了。

我们先写一个简单的版本:

$ nc -l 8800 | nc kawabangga.com 80

这一行是监听 8800 端口,把收到的内容通过管道发给 nc2,然后 nc2 将内容发给 kawabangga.com 的 80 端口。其实就是一个 HTTP 代理啦。

用 curl 来试一下:

$ curl --proxy 127.0.0.1:8800  kawabangga.com -v
*   Trying 127.0.0.1:8800...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8800 (#0)
> GET http://kawabangga.com/ HTTP/1.1
> Host: kawabangga.com
> User-Agent: curl/7.66.0
> Accept: */*
> Proxy-Connection: Keep-Alive
>

注意这个 curl 是卡住了,并没有信息返回。

而两个 nc 那一行的输出是:

$ tmp nc -l 8800 | nc kawabangga.com 80
HTTP/1.1 301 Moved Permanently
Server: wts/1.6.0
Date: Thu, 07 Nov 2019 12:09:52 GMT
Content-Type: text/html; charset=iso-8859-1
Content-Length: 299
Connection: keep-alive
Location: https://www.kawabangga.com/
X-Cache:  from WTS
 
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>301 Moved Permanently</title>
</head><body>
<h1>Moved Permanently</h1>
<p>The document has moved <a href="https://www.kawabangga.com/">here</a>.</p>
<hr>
<address>Apache Server at kawabangga.com Port 80</address>
</body></html>

可以看到,第二个 nc 把服务器的返回直接输出到 stdout 了,而并没有返回给 curl。这样的代理是不合格的,因为我们不想夺走本该属于 curl 的 response。

一个优秀的代理应该完整的将服务器的回复还给客户端(curl),像下面这样。至于 stdout 的问题,我们先不管。

Linux 的 pipe 只支持从一边传给另一边,是单向的。那怎么做到后面的进程往前面的进程传呢?答案就是 namedpipe。Named pipe,嗯……顾名思义,就是有名字的管道。在本文中,理解成有了这个名字,我就可以控制重定向的方向,就好了。

我们用这一行,就完全可以实现图2那种传输。即 nc2 通过 named pipe 传给 nc1 ,nc 默认会将自己的 stdin 的内容传回给连接到 nc 的 TCP 另一头。

$ nc -l 8800 < fifo | nc kawabangga.com 80 > fifo

用 curl 再来试一下:

curl --proxy 127.0.0.1:8800  kawabangga.com -v
*   Trying 127.0.0.1:8800...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8800 (#0)
> GET http://kawabangga.com/ HTTP/1.1
> Host: kawabangga.com
> User-Agent: curl/7.66.0
> Accept: */*
> Proxy-Connection: Keep-Alive
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 301 Moved Permanently
< Server: wts/1.6.0
< Date: Thu, 07 Nov 2019 12:27:56 GMT
< Content-Type: text/html; charset=iso-8859-1
< Content-Length: 299
< Connection: keep-alive
< Location: https://www.kawabangga.com/
< X-Cache:  from WTS
<
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>301 Moved Permanently</title>
</head><body>
<h1>Moved Permanently</h1>
<p>The document has moved <a href="https://www.kawabangga.com/">here</a>.</p>
<hr>
<address>Apache Server at kawabangga.com Port 80</address>
</body></html>
* Connection #0 to host 127.0.0.1 left intact

而 nc 那个命令行没有任何输出。

中间人打印TCP内容

OK,代理已经工作了,接下来要做的是将 TCP 内容打印出来。这里要做的是 nc 在将内容输出到 stdout (即管道)的同时,还要打印出来。

……当然是要用 tee 啦!

$ nc -l 8800 < fifo | tee in.txt | nc kawabangga.com 80 | tee out.txt > fifo

这样可以用 tee 将 TCP 流量分别保存到 in.txtout.txt 里面。可以用上面用过的 curl 测试一下。

因为 stdout 会被管道重定向,所以我们这里无法直接输出到屏幕了。那咋办呢?一不做二不休,再用 named pipe 将 tee 写入到 named pipe 中。

$ mkfifo logging
$ nc -l 8800 < fifo | tee logging | nc kawabangga.com 80 | tee logging > fifo

curl 之后,用 cat logging 就能看到 TCP 的内容啦。(这里如果不使用 cat 读出来 pipe 的内容,curl 会被 block 住)。

最终的脚本

最后,加点细节。写个脚本自动生成 named pipe,使用 trap 在退出的时候把创建的 named pipe 删掉。用 sed 格式化一下输出,分别加上 =><=

代码如下(BSD 的 nc,适用于 OS X):

#!/bin/sh -e
 
if [ $# != 3 ]
then
    echo "usage: $0 <src-port> <dst-host> <dst-port>"
    exit 0
fi
 
TMP=`mktemp -d`
BACK=$TMP/pipe.back
SENT=$TMP/pipe.sent
RCVD=$TMP/pipe.rcvd
trap 'rm -rf "$TMP"' EXIT
mkfifo -m 0600 "$BACK" "$SENT" "$RCVD"
sed 's/^/ => /' <"$SENT" &
sed 's/^/<=  /' <"$RCVD" &
nc -l "$1" <"$BACK" | tee "$SENT" | nc "$2" "$3" | tee "$RCVD" >"$BACK"

nc 的 BSD 版本和 GNU 版本不一样。如果你用的不是 Mac,要改下最后一行,加一个 -p . GNU 版本的代码如下:

#!/bin/sh -e
 
if [ $# != 3 ]
then
    echo "usage: $0 <src-port> <dst-host> <dst-port>"
    exit 0
fi
 
TMP=`mktemp -d`
BACK=$TMP/pipe.back
SENT=$TMP/pipe.sent
RCVD=$TMP/pipe.rcvd
trap 'rm -rf "$TMP"' EXIT
mkfifo -m 0600 "$BACK" "$SENT" "$RCVD"
sed 's/^/ => /' <"$SENT" &
sed 's/^/<=  /' <"$RCVD" &
nc -l -p "$1" <"$BACK" | tee "$SENT" | nc "$2" "$3" | tee "$RCVD" >"$BACK"

使用效果如下(还是用 curl 测试):

➜  tmp ./tcp_proxy.sh 8800 kawabangga.com 80
 => GET http://kawabangga.com/ HTTP/1.1
 => Host: kawabangga.com
 => User-Agent: curl/7.66.0
 => Accept: */*
 => Proxy-Connection: Keep-Alive
 =>
<=  HTTP/1.1 301 Moved Permanently
<=  Server: wts/1.6.0
<=  Date: Thu, 07 Nov 2019 12:50:38 GMT
<=  Content-Type: text/html; charset=iso-8859-1
<=  Content-Length: 299
<=  Connection: keep-alive
<=  Location: https://www.kawabangga.com/
<=  X-Cache:  from WTS
<=
<=  <!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<=  <html><head>
<=  <title>301 Moved Permanently</title>
<=  </head><body>
<=  <h1>Moved Permanently</h1>
<=  <p>The document has moved <a href="https://www.kawabangga.com/">here</a>.</p>
<=  <hr>
<=  <address>Apache Server at kawabangga.com Port 80</address>
<=  </body></html>

作为 redis-server 的代理效果:

➜  tmp ./tcp_proxy.sh 8800 127.0.0.1 6379
 => *2
 => $3
 => get
 => $3
 => foo
<=  $3
<=  bar
 => *2
 => $4
 => llen
 => $7
 => animals
<=  :0

Pretty cool uh? Now I am going to drink some tea!

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章