Search code examples
network-programmingudpnetcat

Netcat exits on UDP response


Could you explain the "weird" behaviour? I'm running netcat on Linux, as a UDP echo server:

ncat -4 --exec /bin/cat -u --listen 2000

Next, run client:

$ ncat -s 192.168.1.2 -u 127.0.0.1 2000

Machine has a real network adapter with the address: 192.168.1.2. After I typed something, the server just exited:

$ ncat -4 --exec /bin/cat -u --listen 2000 ; x=$?; echo $x                                                                                                                                                                                                                   
0

Why?

Wireshark


Solution

  • This is a ncat limitation.

    Let's use strace to look into what ncat is up to. Conveniently, strace has -e option to filter system calls, e.g. -e %net for logging network-related syscalls. Let's start the server first:

    $ strace -e %net ncat -4 --exec /bin/cat -u --listen 2000
    socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) = 3
    setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
    bind(3, {sa_family=AF_INET, sin_port=htons(2000), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
    

    Note the bind call. The socket is bound to 0.0.0.0, meaning that it will receive packets sent to 127.0.0.1:2000 as well as 10.0.2.15:2000 (my eth0 address), as well as any other address belonging to the machine.

    Now let's start a client:

    $ strace -e %net  ncat -s 10.0.2.15 -u 127.0.0.1 2000
    socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) = 3
    setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
    bind(3, {sa_family=AF_INET, sin_port=htons(0), sin_addr=inet_addr("10.0.2.15")}, 16) = 0
    setsockopt(3, SOL_SOCKET, SO_BROADCAST, [1], 4) = 0
    connect(3, {sa_family=AF_INET, sin_port=htons(2000), sin_addr=inet_addr("127.0.0.1")}, 16) = 0
    getsockopt(3, SOL_SOCKET, SO_ERROR, [0], [4]) = 0
    

    At this point, the client is waiting for input.

    Notice the connect call. UDP is connectionless, so the call simply remembers the remote's address. It is convenient as send will use the recorded address. Otherwise, it would be necessary to specify the remote address in every send call. There's another implication which we'll see shortly.

    Now let's type some input. Client:

    sendto(3, "Hello, world!\n", 14, 0, NULL, 0) = 14
    

    Server:

    recvfrom(3, "Hello, world!\n", 131072, MSG_PEEK, {sa_family=AF_INET, sin_port=htons(35197), sin_addr=inet_addr("10.0.2.15")}, [128->16]) = 14
    connect(3, {sa_family=AF_INET, sin_port=htons(35197), sin_addr=inet_addr("10.0.2.15")}, 16) = 0
    recvfrom(3, "Hello, world!\n", 8192, 0, NULL, NULL) = 14
    sendto(3, "Hello, world!\n", 14, 0, NULL, 0) = 14
    recvfrom(3, 0x7fff8fcc9d70, 8192, 0, NULL, NULL) = -1 ECONNREFUSED (Connection refused)
    

    The server sees a message from 10.0.2.15. The first recvfrom call is done with MSG_PEEK flag, which allows to peek at the message without actually consuming it. Then connect (for convenience), another recvfrom to consume the message, and sendto to send a reply.

    Now remember that the server socket is bound to 0.0.0.0. The client sent a message to 127.0.0.1:2000. The reply must include the server socket's address as the source. But the system can't use 0.0.0.0 as it doesn't generally make sense on the wire. Both 127.0.0.1 and 10.0.2.15 are possible, but ncat doesn't tell explicitly which one to use. So the system ends up using 10.0.2.15 as the source.

    Now back to the client. It sent a message to 127.0.0.1:2000, but the response came from 10.0.2.15:2000. Another effect of connect on a UDP socket is that only datagrams originating from the specified address:port will be accepted. If the source doesn't match, the system responds with ICMP port unreachable. This is reported as ECONNREFUSED error in the server.

    Possible fix

    Specify the intended source address in the server using IP_PKTINFO/IPV6_PKTINFO ancillary message.