bashfifonetcat

Persistent connection in Bash script


I'm trying to create a persistent connection using bash. On terminal 1, I keep a netcat running as a server:

$ nc -vlkp 3000
Listening on [0.0.0.0] (family 0, port 3000)

On terminal 2, I create a fifo and keep a cat:

$ mkfifo fifo
$ cat > fifo

On terminal 3, I make the fifo as an input to a client netcat:

$ cat fifo | nc -v localhost 3000
Connection to localhost 3000 port [tcp/*] succeeded!

On terminal 4, I send whatever I want:

$ echo command1 > fifo
$ echo command2 > fifo
$ echo command3 > fifo

Going back to terminal 1, I see the commands being received:

$ nc -vlkp 3000
Listening on [0.0.0.0] (family 0, port 3000)
Connection from [127.0.0.1] port 3000 [tcp/*] accepted (family 2, sport 41722)
command1
command2
command3

So, everything works. But when I put that in a script (I called that fifo.sh), bash is not able to write into fifo:

On terminal 1, same listening server:

$ nc -vlkp 3000
Listening on [0.0.0.0] (family 0, port 3000)

On terminal 2, I run the script:

#!/bin/bash

rm -f fifo
mkfifo fifo
cat > fifo &
pid1=$!
cat fifo | nc -v localhost 3000 &
pid2=$!

echo sending...
echo comando1 > fifo
echo comando2 > fifo
echo comando3 > fifo

kill -9 $pid1 $pid2

The output in terminal 2 is:

$ ./fifo.sh 
Connection to localhost 3000 port [tcp/*] succeeded!
sending...

On terminal 1 I see only the connection. No commands:

$ nc -vlkp 3000
Listening on [0.0.0.0] (family 0, port 3000)
Connection from [127.0.0.1] port 3000 [tcp/*] accepted (family 2, sport 42191)
Connection closed, listening again.

Any idea on why it only works interactively? Or is there any other way to create a persistent connection using only Bash? I don't want to go for Expect because I have a bigger Bash script that does some work after sending the command1, and command2 depends on command1 output, etc.

Thank you!


Solution

  • When a process is started in the background in a script, standard input is redirected from /dev/null. This means the first cat command will read and emit EOF as soon as it executes, which will cause netcat to exit immediately after starting, so the output later in the script will never make it to the fifo, because there isn't an active listener at that time.

    In this case, when cat > fifo is evaluated, the shell forks a child process, redirects standard input from /dev/null, and attempts to open fifo for write. The child remains in a blocking open call at this time. Note that cat is not executed until after the open call completes.

    Next, cat fifo | nc -v localhost 3000 is spawned. cat opens fifo for read, which allows the blocking open from the first child to complete and the first cat to execute.

    The first cat inherits its parent's file descriptors, so its standard input is attached to /dev/null and it thus reads and emits an EOF immediately. The second cat reads the EOF and passes that to the standard input of nc, which causes netcat to exit.

    By the time the echo statements are evaluated, the processes identified by $pid1 and $pid2 are finished. Since there is no longer a listener on fifo, the first echo will block forever.


    I don't have a pure-shell fix, but you can use an external program like perl to open the placeholder writer to fifo instead of using shell redirection. Aside, please note that there is a race with nc starting after the echo statements (where the kill happens before netcat has a chance to process input/send output), so here I added a delay after the cat | nc expression. There is almost certainly a better solution out there, but here's what I came up with:

    #!/bin/bash
    
    rm -f fifo
    mkfifo fifo
    perl -e 'open(my $fh, ">", "fifo"); sleep 3600 while 1' &
    pid1=$!
    cat fifo | nc -v localhost 3000 &
    pid2=$!
    
    sleep 2
    
    echo sending...
    echo comando1 > fifo
    echo comando2 > fifo
    echo comando3 > fifo
    
    kill -9 $pid1 $pid2
    

    Hope this helps, great question!