![执行和获取 Bash 脚本之间出现奇怪的不一致](https://linux22.com/image/229477/%E6%89%A7%E8%A1%8C%E5%92%8C%E8%8E%B7%E5%8F%96%20Bash%20%E8%84%9A%E6%9C%AC%E4%B9%8B%E9%97%B4%E5%87%BA%E7%8E%B0%E5%A5%87%E6%80%AA%E7%9A%84%E4%B8%8D%E4%B8%80%E8%87%B4.png)
我知道这不是一个描述性很强的标题(欢迎提出建议),但事实是我已经为此烦恼了好几个小时,而且我不知道问题的根源可能在哪里。
我为本地网络上的对等点之间的 CLI 聊天编写了一个简单的 Bash 脚本:
#!/usr/bin/env bash
# Usage: ./lanchat <local_ip>:<local_port> <remote_ip>:<remote_port>
# set -x
set -o errexit -o nounset -o pipefail
IFS=':' read -a socket <<< "$1"
LOCAL_IP=${socket[0]}
LOCAL_PORT=${socket[1]}
IFS=':' read -a socket <<< "$2"
REMOTE_IP=${socket[0]}
REMOTE_PORT=${socket[1]}
RECV_FIFO=".tmp.lanchat"
trap "rm '$RECV_FIFO'; kill 0" EXIT
mkfifo "$RECV_FIFO"
# EDIT: As per @Kamil Maciorowski's suggestion, removing the `-q 0` part below solves the issue.
while true; do nc -n -l -q 0 -s "$LOCAL_IP" -p "$LOCAL_PORT" > "$RECV_FIFO"; done &
TMUX_TOP="while true; do cat '$RECV_FIFO'; done"
TMUX_BOTTOM="while IFS= read -r line; do nc -n -q 0 '$REMOTE_IP' '$REMOTE_PORT' <<< \$line; done"
tmux new "$TMUX_TOP" \; split -v "$TMUX_BOTTOM"
IP 172.16.0.2 上的机器是运行 Debian 11 的 VPS,172.16.0.100 上是我运行 Arch 的本地计算机。
当我在两侧的提示符下手动运行命令时,我得到了想要的结果,这证实了网络通信没有问题,并且脚本的逻辑是正确的。
## VPS (Debian) side as follows; exchange IPs for local (Arch) side.
$ mkfifo .tmp.lanchat
$ while true; do nc -n -l -q 0 -s 172.16.0.2 -p 1234 > .tmp.lanchat; done &
$ tmux new "while true; do cat .tmp.lanchat; done" \; split -v "while IFS= read -r line; do nc -n -q 0 172.16.0.100 1234 <<< \$line; done"
## Test communication in both directions: all right; then CTRL-C twice to exit both tmux panels
$ kill %1; rm .tmp.lanchat
然而,当我将双方作为脚本运行时,只有本地端(Arch)打印来自服务器(Debian)的消息。服务器不从我的本地计算机打印任何内容。当我使用 跟踪执行时set -x
,两侧的所有内容看起来都与我手动输入的命令完全相同,用正确的值代替变量。
现在奇怪的是,如果我在 Arch 端运行脚本并在 Debian 端按照提示符(如上所示)运行命令,那么一切都会再次正常工作。此外,如果我在 Arch 端执行脚本但是来源在 Debian 方面,它也工作得很好。
向两者添加详细输出数控呼唤拱门一侧甚至有印记Connection to 172.16.0.2 1234 port [tcp/*] succeeded!
。但是,tee log.txt
在调用中添加数控在 Debian 端的监听模式下不会捕获任何内容:
#...
while true; do
nc -n -l -q 0 -s "$LOCAL_IP" -p "$LOCAL_PORT" | tee log.txt > "$RECV_FIFO";
done &
#...
我尝试在两个对等点之间以所有可能的顺序建立连接。我什至重新启动了服务器和本地计算机,以确保不存在孤立或僵尸实例数控拥抱以某种方式逃避检测的插座。
现在,Debian 和 Arch 运行不同版本的数控。所以,从表面上看,这听起来可能是一个可能的解释。但是,在 Debian 端获取脚本运行良好的事实是否排除了这种可能性?
这里到底发生了什么事?
答案1
我已经在 Debian 12 中测试了您的脚本(本地主机到本地主机,单独的工作目录),并确认了问题。我nc
的来自netcat-traditional 1.10-47
(即不是来自netcat-openbsd
)。
问题出-q 0
在听力上nc
。从man 1 nc
:
-q seconds
在 stdin 上的 EOF 后,等待指定的秒数,然后退出。如果秒为负数,则永远等待。
似乎监听nc
在退出之前等待传入连接-q 0
,但它不等待传入数据。建立连接和传输数据是单独的事件,因为-q 0
该工具通常会在中间退出。这是一场比赛;在我的测试中听力nc
有时确实将传入数据中继到管道。
触发意外行为的 EOF 会立即发生,因为当没有作业控制的 shell 运行异步命令时(以 终止&
,这就是您使用监听运行循环的方式nc
),它必须将其标准输入重定向到/dev/null
或重定向到等效文件。
当您获取脚本时,交互式 shell 会解释它。它可能是启用了作业控制的 bash(交互式 bash 的默认行为)。如果是这样,它会在单独的进程组中运行后台循环,但其标准输入仍然连接到终端(通常这允许我们进行fg
后台作业并键入它)。对于后台作业,无法从终端窃取来自 SIGTTIN 的输入,EOF 永远不会发生。这样,当脚本被获取时,监听nc
就不会受到-q 0
在没有获取的情况下运行脚本时出现的问题。
指定-q 1
聆听nc
将在实践中有所帮助(虽然理论上仍然很活跃,我猜),但我认为最好使用-q -1
(永远等待)或简单地省略-q
(在我的测试中默认行为似乎成为“永远等待”)。
-q 0
因为连接nc
(tmux 内的连接)是有意义的,您确实希望nc
在发送有效负载后立即退出。
nc
您的 Arch 上的行为有所不同,可能是因为它不同,或者可能是因为当时操作系统的整体压力影响了比赛。
教训是:如果nc
+nc -l
对仅在一个方向发送数据(每行使用一个这样的对),那么-q 0
对于发送方来说是一个有用的选项;但对于接收者来说这是不必要的,在某些情况下甚至是有害的。
还有更多需要改进的地方,例如:
- 存在代码注入漏洞(
./lanchat <local_ip>:<local_port> <remote_ip>:<remote_port>"'; rogue command'"
); nc
当一端或另一端没有监听时,存在很短的时间窗口;- 一对
nc
s 足以处理整个“会话”。
我不会在这里讨论这些问题,但我可以给你一个替代脚本的草图:
#!/usr/bin/env bash
target="$(tmux new -dP 'tail -f /dev/null')"
uptty="$(tmux display-message -p -F '#{pane_tty}' -t "$target")"
tmux split -t "$target" -v "
rlwrap tee >(sed -u 's/^/ < /' | ts %H:%M >${uptty@Q}) \
| nc ${*@Q} > >(sed -u 's/^/> /' | ts %H:%M >${uptty@Q})
"
tmux a -t "$target"
该脚本确实需要 bash(用于其自身和 tmux 内部)。您可以使用要提供给的参数来运行它nc
,例如
- 首先是聆听方:
./lanchat -n -l -s 192.168.11.22 -p 2345
, - 然后是连接边:
./lanchat 192.168.11.22 2345
。
单个nc
连接nc
处理双向的所有通信。该脚本用于时间戳(如果需要,ts
您可以删除两个实例)和使用 readline 进行行编辑(如果需要,您可以删除)。不便于携带;没有会导致缓冲问题,除非你也摆脱了。| ts %H:%M
rlwrap
rlwrap
sed -u
sed
-u
ts
在 bash 5.2.15、tmux 3.3a 中测试。