runsisi's

technical notes

socat 基础操作

2019-05-09 runsisilinux

netcat 和 socat 是两个很古老的网络工具,两者名字类似,但基本上是两个完全不一样的东西。

netcat

其中 netcat 比较简单,通常使用它的客户端模式来测试传输层的连通性,如:

~$ nc -v 10.123.123.123 80
Connection to 10.123.123.123 80 port [tcp/http] succeeded!

端口扫描:

~$ nc -zv 10.123.123.123 20-81
nc: connect to 10.123.123.123 port 20 (tcp) failed: Connection refused
nc: connect to 10.123.123.123 port 21 (tcp) failed: Connection refused
Connection to 10.123.123.123 22 port [tcp/ssh] succeeded!
nc: connect to 10.123.123.123 port 23 (tcp) failed: Connection refused
nc: connect to 10.123.123.123 port 24 (tcp) failed: Connection refused
nc: connect to 10.123.123.123 port 25 (tcp) failed: Connection refused
...
nc: connect to 10.123.123.123 port 52 (tcp) failed: Connection refused
Connection to 10.123.123.123 53 port [tcp/domain] succeeded!
nc: connect to 10.123.123.123 port 54 (tcp) failed: Connection refused
...
Connection to 10.123.123.123 80 port [tcp/http] succeeded!
nc: connect to 10.123.123.123 port 81 (tcp) failed: Connection refused

在某些情况下,比如测试 iptables 规则之类的,还可以通过 -s 选项指定源 IP 地址,-p 选项指定源端口,-u 选项指定使用 UDP 传输(默认为 TCP),-w 选项指定连接的超时时间。

此外,可以通过 -x 选项指定通过代理服务器连接,-X 选项指定代理服务器类型,-U 选项指定使用本地 unix socket。

netcat 的服务端模式没有太大的用途,同样,可以用来作为服务端监听地址,配置它的客户端模式来测试网络的连通性。

服务端:

~$ nc -lv 192.168.20.41 8181 
Ncat: Version 6.40 ( http://nmap.org/ncat )
Ncat: Listening on 192.168.20.41:8181
Ncat: Connection from 192.168.20.42.
Ncat: Connection from 192.168.20.42:39534.
abc

客户端:

~$ nc -v 192.168.20.41 8181
Ncat: Version 6.40 ( http://nmap.org/ncat )
Ncat: Connected to 192.168.20.41:8181.
abc

可以看到,在 netcat 客户端终端的输入,都会在服务端进行输出,因此可以使用 netcat 来进行文件传输:

服务端:

~$ nc -l 192.168.20.41 8181 > output
~$ cat output 
hello, netcat

客户端:

~$ echo "hello, netcat" > input
~$ nc -v 192.168.20.41 8181 < input 
Ncat: Version 6.40 ( http://nmap.org/ncat )
Ncat: Connected to 192.168.20.41:8181.
Ncat: 14 bytes sent, 0 bytes received in 0.01 seconds.

socat

socat 比 netcat 复杂非常、非常多。

命令行语法如下:

~$ socat [options] <address1> <address2>

address

其中 <address> 并不一定是我们通常意义上的 IP 地址,只是由于 socat 功能太多,为了命令行处理方便而做的统一,如果不这样做,将会导致命令行选项满天飞,导致 socat 的使用文档非常难写。

<address> 有固定的格式:

<type>[:parameter:parameter:...][,option,option,...]

其中类型 <type> 后面的的参数 <parameter> 和选项 <option> 字段根据类型的不同而可能是可选的。

此外,<address> 的指定还支持别名,如我们所熟知的 -STDIO 的别名。

socat 的 man 手册页的 ADDRESS TYPES 一节定义了所有的类型以及类型所支持的参数,ADDRESS OPTIONS 一节定义了所有的选项,不过需要注意的是,选项有明确的所属分组,每个类型可能关联多个选项分组,当然也只能使用这些分组中的选项。

显然,下面的一些类型与参数的组合我们应该非常容易理解:

TCP4:192.168.20.41:8181
TCP4-LISTEN:8181

类型也支持小写,因此,上面的组合也可以写成如下的形式:

tcp4:192.168.20.41:8181
tcp4-listen:8181

在某些情况下,类型字段也可以省略,因为 socat 可以根据后面的参数字段推测出来类型字段,如参数字段是数字表示类型字段为 FD,参数字段是斜杠开始的绝对路径表示类型字段为 GOPEN

0[,option,option,...]
->
FD:0[,option,option,...]
/tmp/file[,option,option,...]
->
GOPEN:/tmp/file[,option,option,...]

选项的指定根据选项的不同,而有 optionoption=value 两种指定方式,man 手册页中 ADDRESS OPTIONS 一节有明确的规定。

如果 <address> 中的类型字段没有显式的 IP 类型,则,那么 socat 的选项 -4, -6 可以用于决定选择那种类型的 IP 地址(默认为 -4),也可以通过选项字段 pf 指定 IP 类型,如下面的四种方式是等效的:

~$ socat tcp-listen:8181 STDOUT
~$ socat -4 tcp-listen:8181 STDOUT
~$ socat tcp-listen:8181,pf=ipv4 STDOUT
~$ socat tcp4-listen:8181 STDOUT

数据流

socat 在命令行指定的两个 <address> 之间进行双向数据转发,即两条数据流,也可以通过 -u 选项限定为 <address1><address2> 的是单向数据转发,或者 -U 选项限定为 <address2><address1> 的单向数据转发,即单条数据流。

socat 内部通过 select 检测两端的 fd 事件,不断读取一端的数据并写入至另一端。

通过下面这个例子,应该能够理解这两个数据流之间的关系。

节点A

~$  socat -v tcp-listen:8181,fork exec:"/bin/cat"                
> 2019/05/09 21:07:03.389889  length=12 from=0 to=11
hello socat
< 2019/05/09 21:07:03.390788  length=12 from=0 to=11
hello socat

注意 -v 不是 verbose 的意思,而是把两端传输的数据打印到 stderr 标准错误流中,其中 >, < 是传输的方向。

其中 <address1>tcp-listen:8181,fork<address2>exec:"/bin/cat"

<address1> 作为 TCP 服务端在 8181 端口上进行监听,当客户端连接 8181 端口建立 TCP 会话后,fork 一个子进程负责后续 <address1><address2>,即新创建的 Established 状态的 TCP socket 与 <address2> 之间的双向数据传输,而父进程负责继续监听 TCP 连接。fork 子进程完成标示着 <address1> 被打开,接下来将要打开 <address2>,由于这是一个 exec 类型的 address,因此会基于当前 socat 子进程再创建 /bin/cat 子进程,新创建的子进程的 stdin/stdout 标准 IO 流作为 <address2> 的数据收发端。

三个进程之间的关系变化如下:

TCP 会话建立之前

runsisi       96624   95797   96624  0    1 21:06 pts/0    00:00:00 socat -v tcp-listen:8181,fork exec:/bin/cat

TCP 会话建立之后

runsisi       96624   95797   96624  0    1 21:06 pts/0    00:00:00 socat -v tcp-listen:8181,fork exec:/bin/cat
runsisi       96627   96624   96627  0    1 21:06 pts/0    00:00:00 socat -v tcp-listen:8181,fork exec:/bin/cat
runsisi       96628   96627   96628  0    1 21:06 pts/0    00:00:00 /bin/cat

节点B

~$ socat -v - tcp4:192.168.20.41:8181
hello socat
> 2019/05/09 20:35:50.600249  length=12 from=0 to=11
hello socat
< 2019/05/09 20:35:50.602679  length=12 from=0 to=11
hello socat
hello socat

其中 <address1>-,即 STDIO<address2>tcp4:192.168.20.41:8181

socat 进程的 stdin/stdout 标准 IO 流作为 <address1> 的数据收发端,<address2> 作为 TCP 客户端连接服务端 192.168.20.41:8181,当 TCP 会话建立后标示着 <address2> 被打开。

因此在节点B 终端输入的 hello socat 字符串会被 socat 写入到 TCP 客户端的 socket,然后由 TCP 会话传输至节点A 的 TCP 服务端 socket,节点A 的 socat 子进程接收到该字符串后,写入 /bin/cat 进程的 stdin 标准输入流,然后 /bin/cat 再将其写入 stdout 标准输出流,此时由于 <address1><address2> 之间数据传输是双向的,因此该字符串又会写入服务端的 TCP socket,进而被节点B 的客户端 TCP socket 接收,然后写入 socat 进程的 stdout,即当前终端。

看上去有点复杂,但如果理解了 socat 只是在 <address1><address2> 进行数据传输(或者说数据转发),那么整个流程非常自然。

dual address specification

在某些情况下,可能想指定另外的数据接收端,此时需要使用如下的所谓 dual address specification 形式:

~$ socat <address1>\!\!<address3> <address2>

其中的 \!\! 只是为了屏蔽 shell 的解释而已,实际上就是 !! 分隔的两个 <address>,其含义也比较容易理解,<address1> 的数据写入 <address2>,但 <address2> 的数据写入 <address3> 而不是 <address2>

生命周期

socat 进程的生命周期分为四个节点:

  1. 初始化

解析命令行选项,并初始化日志处理;

  1. 打开 <address1><address2>

所谓“打开”,其实概念比较模糊,但如果对应到 Unix/Linux 下一切皆文件的哲学,又显得很自然。对于客户端 socket 自然是建立 socket 连接,对于文件系统上的文件就意味着调用 open 系统调用打开,当然前面提到的 exec 类型就对应着创建对应的进程,listen 类型则需要等到新的 socket 会话创建成功。

打开的顺序为先 <address1><address2>,且与选项 -u, -U 无关,如果将上面例子中提到的 <address1><address2> 位置调换一下:

节点A

~$  socat -v exec:"/bin/cat" tcp-listen:8181,fork
< 2019/05/09 21:13:10.035278  length=12 from=0 to=11
hello socat
> 2019/05/09 21:13:10.035702  length=12 from=0 to=11
hello socat

节点B

~$  socat -v - tcp4:192.168.20.41:8181
hello socat
> 2019/05/09 21:13:10.010265  length=12 from=0 to=11
hello socat
< 2019/05/09 21:13:10.012323  length=12 from=0 to=11
hello socat
hello socat

通过节点A 上进程之间的变化可以非常清晰的看出来这个顺序关系:

TCP 会话建立之前

runsisi       96639   95797   96639  0    1 21:12 pts/0    00:00:00 socat -v exec:/bin/cat tcp-listen:8181,fork
runsisi       96640   96639   96640  0    1 21:12 pts/0    00:00:00 /bin/cat

TCP 会话建立之后

runsisi       96639   95797   96639  0    1 21:12 pts/0    00:00:00 socat -v exec:/bin/cat tcp-listen:8181,fork
runsisi       96640   96639   96640  0    1 21:12 pts/0    00:00:00 /bin/cat
runsisi       96643   96639   96643  0    1 21:13 pts/0    00:00:00 socat -v exec:/bin/cat tcp-listen:8181,fork

显然这里的 <address1>,即 exec:"/bin/cat" 首先被打开。

  1. 数据传输

socat 通过 select 持续监听 <address1><address2> 两端的 fd,当某一端有数据可读,同时另一端可写时,将数据从一端读取并转发到另一端。

  1. 关闭

当 socat 检测到一端数据传输结束时(EOF),会尝试关闭另一端的写 fd,整个过程类似于应用层 TCP socket read 返回 0 时的关闭处理流程。

参考资料

socat

https://medium.com/@copyconstruct/socat-29453e9fc8a6

socat

http://www.dest-unreach.org/socat/doc/socat.html