执行和获取 Bash 脚本之间出现奇怪的不一致

执行和获取 Bash 脚本之间出现奇怪的不一致

我知道这不是一个描述性很强的标题(欢迎提出建议),但事实是我已经为此烦恼了好几个小时,而且我不知道问题的根源可能在哪里。

我为本地网络上的对等点之间的 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当一端或另一端没有监听时,存在很短的时间窗口;
  • 一对ncs 足以处理整个“会话”。

我不会在这里讨论这些问题,但我可以给你一个替代脚本的草图:

#!/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:%Mrlwraprlwrapsed -used-uts

在 bash 5.2.15、tmux 3.3a 中测试。

相关内容