1 - 重定向和管道

stdin stdout 和 stderr

stdin stdout 和 stderr 是 Linux 中的三个特殊的文件描述符。Linux 会为每个运行的进程都分配这三个文件。

文件描述符 英文 中文
0 stdin 标准输入
1 stdout 标准输出
2 stderr 标准错误

stdin 从 keyboard 读取数据,stdout 和 stderr 缺省会显示到到 terminal window 上。

重定向

重定向操作符 >>> 可以把 stdout 从 terminal window 重定向到文件中。

  • > : 重定向 stdout 到文件,会覆盖已有文件的内容。
  • >> : 重定向 stdout 到文件,追加到已有文件内容上。

注意:缺省只会重定向 stdout,如果也需要重定向 stderr,可以把 stderr 重定向到 stdout,然后再重定向到文件中。

假如有一个 error.sh 脚本,内容如下:

#!/bin/bash

echo "About to try to access a file that doesn't exist"
cat bad-filename.txt

执行下面的命令:

➜  ~ ./error.sh>log
cat: bad-filename.txt: No such file or directory
➜  ~ cat log
About to try to access a file that doesn't exist

可以看到 stdout 被重定向到了 log 文件,而 stderr 还是显示在 terminal 中。该命令是 ./error.sh 1>log的简写形式。

如果也要重定向 stderr,可以这样做:

➜  ~ ./error.sh>log 2>&1
➜  ~ cat log
About to try to access a file that doesn't exist
cat: bad-filename.txt: No such file or directory

2>&1 表示把 stderr 重定向到 stdout。这里的 2 和 1 分别是 stderr 和 stdout 的文件描述符。为了区别普通文件,当将 stderr 和 stdout 作为重定向目的地时,需要在文件描述符前加上 &

该命令也可以写作 ./error.sh 1>log 2>&1./error.sh 1>>log 2>>log 也可以达到类似的目的,但不会覆盖文件内容。

如果要把 stdout 和 stderr 分别重定向到不同的文件,可以这样写:./error.sh 1>out.log 2>error.log

/dev/null 是一个特殊文件,任何写入该文件的内容都会被丢弃掉。将标准输出重定向到该文件可以用于清空 terminal 的命令行输出。./error.sh>/dev/null 2>&1

管道

管道 | 可以把前一个命令的标准输出(stdout)作为后一个命令的标准输入(stdin)。

例如通过 grep 来查找命令标准输出中的文本。

~ ./error.sh | grep -o file
file
cat: bad-filename.txt: No such file or director

如果要需要同时查找 stdout 和 stderr 中的文本,可以先把 stderr 重定向到 stdout。

➜  ~ ./error.sh 2>&1 | grep -o file
file
file
file

上面的命令也可以写作 ./error.sh |& grep -o file

并不是所有的文件都支持从标准输入中读取数据,对于不支持读取标准输入的命令,可以采用下面的命令将标准输出放到一个缓冲区中,然后再用后一个命令打开。

./error.sh | vi -

2 - tproxy(透明代理)

什么是透明代理

tproxy 即 transparent(透明) proxy。这里的 transparent(透明)有两层含义:

  1. 代理对于 client 是透明的,client 端无需进行任何配置。即无需修改请求地址,也无需采用代理协议和代理服务器进行协商。与之相对比的是 socks 代理或者 http 代理,需要在 client 端设置代理的地址,在发起请求时也需要通过代理协议告知代理服务器其需要访问的真实地址。
  2. 代理对于 server 是透明的,server 端看到的是 client 端的地址,而不是 proxy 的地址。

“非透明”代理

“非透明”代理的请求和响应数据路径如下。可以看到客户端需要在 IP 数据包中把目的 IP 地址指定为代理服务器。 代理需要得到客户端访问的真实目的地,才能正确地转发请求。因此客户端需要通过代理协议将真实地址告知代理服务器。

例如,SOCKS5 协议中,客户端会在协议握手期间向代理服务器发出下面的链接请求:

VER CMD RSV DSTADDR DSTPORT
字节 1 1 1 Variable 2

透明代理

透明代理的请求和响应数据路径如下。客户端的 IP 数据包的目的 IP 地址是其需要访问的真实目的地址。客户端不需要和代理服务器进行握手,就好像直接访问其真实目的地一样。

在透明代理的模式下,由于客户端请求数据包的目的地址不是代理服务器,因此需要通过路由和 iptables 规则将客户端的请求发送给代理服务器处理。

首先将代理服务器设置为客户端的缺省网关,以将请求发送到代理服务器。请求路由到代理服务器后,还需要通过 iptables 将请求重定向到代理进程进行处理。可以有两种办法:

  1. 采用 DNAT 将请求重定向到代理进程,这种方式会修改请求 IP 数据包的目的地址和端口。代理服务器收到客户端请求后,可以通过调用 getsockopt 的 SO_ORIGINAL_DST 参数拿到原始请求的目的地址。
  2. 采用 TPROXY 来将请求发送到代理进程,这种方式不会修改请求 IP 数据包的目的地址和端口。因此代理服务器可以直接从 socket 中拿到其请求目的地。

采用 TPROXY 模式时,需要路由规则、iptables 和代理进程进行配合。

路由规则

由于客户端发出的 IP 数据包的目的地址并不是代理服务器,因此请求缺省会被代理服务器内核 forward 出去。为了能将客户端请求重定向到代理进程,需要在代理服务器上创建下面的策略路由:

sudo ip rule add fwmark 1/1 table 1
sudo ip route add local 0.0.0.0/0 dev lo table 1

上面的第一行创建了一条策略路由,指定 fwmark 值为1的 IP 数据包采用路由表1进行路由。

第二行在路由表1中添加了一条路由规则,将所有数据包的下一跳都指向 loopback,这样数据包才能被本地代理进程的 listener 看到。

iptables

需要在 iptables 的 PREOURTING 链的 mangle 表中对收到的客户端数据包进行处理。

sudo iptables -t mangle -A PREROUTING -p tcp -j TPROXY --on-port 12345 --tproxy-mark 1/1

-j TPROXY 表示采用 TPROXY。

--on-port 12345 是代理进程的监听端口。

--tproxy-mark 1/1 为 IP 数据包打上一个标记,以应用上面创建的策略路由。

应用进程

应用进程必须为监听套接字设置 IP_TRANSPARENT 选项,否则不能监听非 local 的地址。

  int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
  assert(listen_fd != -1);

  int enable = 1;
  assert(setsockopt(listen_fd, SOL_IP, IP_TRANSPARENT, &enable, sizeof(enable)) != -1);
  assert(setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(enable)) != -1);

  struct sockaddr_in bind_addr;
  bind_addr.sin_family = AF_INET;
  bind_addr.sin_port = htons(555);
  inet_pton(AF_INET, "0.0.0.0", &bind_addr.sin_addr);
  assert(bind(listen_fd, (struct sockaddr *)&bind_addr, sizeof(bind_addr)) != -1);

  assert(listen(listen_fd, 128) == 0);

  ......

参考文档