Sei que este não é um título muito descritivo (sugestões são bem-vindas), mas o fato é que estou mexendo nos cabelos há horas e não tenho ideia de onde possa estar a raiz do problema.
Escrevi um script Bash simples para bate-papo CLI entre pares em uma rede local:
#!/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"
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"
A máquina no IP 172.16.0.2 é um VPS rodando Debian 11, e em 172.16.0.100 está meu computador local rodando Arch.
Ao executar os comandos manualmente no prompt em ambos os lados, obtenho o resultado desejado, que confirma que não há problema com a comunicação da rede e que a lógica do script está correta.
## 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
quando executo ambos os lados como um script, porém, apenas o lado local (Arch) imprime mensagens do servidor (Debian). O servidor não imprime nada do meu computador local. Quando rastreio a execução com set -x
, tudo em ambos os lados se parece exatamente com os comandos que insiro manualmente, com os valores corretos no lugar das variáveis.
Agora, o estranho é que se eu executar o script no lado do Arch e os comandos no prompt (como acima) no lado do Debian, tudo funcionará bem novamente. Além disso, se eu executar o script no lado do Arch, mas fornecê -lo no lado do Debian, isso também funcionará bem.
Adicionar saída detalhada a ambas as chamadas nc no lado do Arch até imprime Connection to 172.16.0.2 1234 port [tcp/*] succeeded!
. No entanto, adicionar a tee log.txt
à chamada para nc no modo de escuta no lado Debian não captura nada:
#...
while true; do
nc -n -l -q 0 -s "$LOCAL_IP" -p "$LOCAL_PORT" | tee log.txt > "$RECV_FIFO";
done &
#...
Tentei estabelecer a conexão em todas as ordens possíveis entre os dois pares. Até reiniciei o servidor e minha máquina local para ter certeza de que não havia instâncias órfãs ou zumbis de nc abraçando o soquete que de alguma forma escapou da detecção.
Agora, o Debian e o Arch executam versões diferentes do nc . Então, aparentemente, parece que essa poderia ser uma explicação possível. Mas o fato de obter o script do lado do Debian funcionar bem não exclui essa possibilidade?
O que diabos está acontecendo aqui?
Testei seu script no Debian 12 (localhost para localhost, diretórios de trabalho separados) e confirmo o problema. Meu
nc
é denetcat-traditional 1.10-47
( ou seja, não denetcat-openbsd
).O problema está
-q 0
na escutanc
. Deman 1 nc
:Parece que a escuta
nc
espera por uma conexão de entrada antes de encerrar por causa de-q 0
, mas não espera pela entrada de dados. Estabelecer uma conexão e transmitir dados são eventos separados e, por causa-q 0
da ferramenta, geralmente é encerrada no meio. É uma corrida; em meus testes, a escutanc
às vezes retransmitiu os dados recebidos para o canal.O EOF que desencadeia o comportamento inesperado acontece imediatamente porque quando um shell sem controle de job executa um comando assíncrono (terminado por
&
, é assim que você executa o loop com listennc
), ele é obrigado a redirecionar seu stdin para/dev/null
ou para um arquivo equivalente.Quando você origina o script, seu shell interativo o interpreta. Provavelmente é o bash com o controle de trabalho ativado (o comportamento padrão para um bash interativo). Nesse caso, ele executa o loop em segundo plano em um grupo de processos separado, mas seu stdin ainda está conectado ao terminal (em geral, isso nos permite fazer
fg
um trabalho em segundo plano e digitar nele). Para um trabalho em segundo plano, a incapacidade de roubar informações do terminal vem do SIGTTIN, o EOF nunca acontece. Dessa forma, quando o script está sendo originado, a escutanc
não sofre com-q 0
esse problema quando você executa o script sem fonte.Especificar
-q 1
para ouvirnc
ajudará na prática (embora ainda seja atrevido na teoria, eu acho), mas acho melhor usar-q -1
(esperar para sempre) ou simplesmente omitir-q
(em meus testes o comportamento padrão parece ser "esperar para sempre").-q 0
para a conexãonc
(aquela dentro do tmux) faz sentido, você deseja que issonc
seja encerrado imediatamente após o envio da carga útil.nc
no seu Arch se comportou de maneira diferente, talvez porque seja diferente, ou talvez porque o estresse geral no sistema operacional naquele momento afetou a corrida.A lição é: no caso de um par
nc
+nc -l
que envia dados em apenas uma direção (você usa um par para cada linha),-q 0
é uma opção útil para o remetente; mas para o receptor é desnecessário e, em algumas circunstâncias, até prejudicial.Observe que esta resposta aborda a questão explícita, nada mais. Por exemplo:
./lanchat <local_ip>:<local_port> <remote_ip>:<remote_port>"'; rogue command'"
);nc
de um lado ou de outro;nc
s pode ser suficiente para lidar com toda a "sessão".A resposta não aborda isso, mas posso fornecer um esboço de um script alternativo:
O script requer bash (para si e dentro do tmux). Você o executa com argumentos que deseja fornecer
nc
, por exemplo./lanchat -n -l -s 192.168.11.22 -p 2345
,./lanchat 192.168.11.22 2345
.Uma conexão única
nc
lidanc
com toda a comunicação em ambas as direções. O script usats
carimbos de data e hora (você pode remover ambas as instâncias,| ts %H:%M
se quiser) erlwrap
para edição de linha com readline (você pode remover,rlwrap
se quiser).sed -u
não é portátil;sed
sem-u
causará problemas de buffer, a menos que você também se livre dots
.Testado no bash 5.2.15, tmux 3.3a.