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,...]
选项的指定根据选项的不同,而有 option
和 option=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 进程的生命周期分为四个节点:
- 初始化
解析命令行选项,并初始化日志处理;
- 打开
<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"
首先被打开。
- 数据传输
socat 通过 select 持续监听 <address1>
与 <address2>
两端的 fd,当某一端有数据可读,同时另一端可写时,将数据从一端读取并转发到另一端。
- 关闭
当 socat 检测到一端数据传输结束时(EOF),会尝试关闭另一端的写 fd,整个过程类似于应用层 TCP socket read 返回 0 时的关闭处理流程。
参考资料
socat
https://medium.com/@copyconstruct/socat-29453e9fc8a6
socat
http://www.dest-unreach.org/socat/doc/socat.html
最后修改于 2019-05-09