Network Redirections in Bash

A few months ago, while reading the man page for recvmmsg() , I came across this snippet:

$ while true; do echo $RANDOM > /dev/udp/127.0.0.1/1234; sleep 0.25; done

And as advertised, it sends a UDP datagram containing a random number to port 1234 every 250 ms. I didn’t recall ever seeing a /dev/udp and so was a bit surprised that it worked. And as it happens, ls was not able to access the file that I had just written to:

ls: cannot access '/dev/udp/127.0.0.1/1234' : No such file or directory

Puzzled and intrigued, I echoe d Foo Bar Baz to /dev/udp/127.0.0.1/1337 and reached for strace :

... 2423 socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) = 4 12423 connect( 4 , {sa_family = AF_INET, sin_port = htons( 1337 ), sin_addr = inet_addr( "127.0.0.1" )}, 16 ) = 0 12423 fcntl( 1 , F_GETFD) = 0 12423 fcntl( 1 , F_DUPFD, 10 ) = 10 12423 fcntl( 1 , F_GETFD) = 0 12423 fcntl( 10 , F_SETFD, FD_CLOEXEC) = 0 12423 dup2( 4 , 1 ) = 1 12423 close( 4 ) = 0 12423 fstat( 1 , {st_mode = S_IFSOCK | 0777 , st_size = 0 , ...}) = 0 12423 write( 1 , "Foo Bar Baz

" , 12 ) = 12 ...

Seemingly, a normal UDP socket was being created and written to using the regular sycall interface. That refuted my initial suspicion that some kind of a special file backed by the kernel was involved. But who was actually creating the socket?

A peek at Bash’s code answered that question:

redir.c:

/* A list of pattern/value pairs for filenames that the redirection code handles specially. */ static STRING_INT_ALIST _redir_special_filenames[] = { #if !defined (HAVE_DEV_FD) { "/dev/fd/[0-9]*" , RF_DEVFD }, #endif #if !defined (HAVE_DEV_STDIN) { "/dev/stderr" , RF_DEVSTDERR }, { "/dev/stdin" , RF_DEVSTDIN }, { "/dev/stdout" , RF_DEVSTDOUT }, #endif #if defined (NETWORK_REDIRECTIONS) { "/dev/tcp/*/*" , RF_DEVTCP }, { "/dev/udp/*/*" , RF_DEVUDP }, #endif { ( char * ) NULL , - 1 } };

So, redirection involving /dev/udp/ is handled specially by Bash and it uses BSD Sockets API to create a socket:

lib/sh/netopen.c:

/* * Open a TCP or UDP connection to HOST on port SERV. Uses the * traditional BSD mechanisms. Returns the connected socket or -1 on error. */ static int _netopen4 (host, serv, typ) char * host, * serv; int typ; { struct in_addr ina; struct sockaddr_in sin; unsigned short p; int s, e; if (_getaddr(host, & ina) == 0 ) { internal_error (_( "%s: host unknown" ), host); errno = EINVAL; return - 1 ; } if (_getserv(serv, typ, & p) == 0 ) { internal_error(_( "%s: invalid service" ), serv); errno = EINVAL; return - 1 ; } memset (( char * ) & sin, 0 , sizeof (sin)); sin.sin_family = AF_INET; sin.sin_port = p; sin.sin_addr = ina; s = socket(AF_INET, (typ == 't' ) ? SOCK_STREAM : SOCK_DGRAM, 0 ); if (s < 0 ) { sys_error ( "socket" ); return ( - 1 ); } if (connect (s, ( struct sockaddr * ) & sin, sizeof (sin)) < 0 ) { e = errno; sys_error( "connect" ); close(s); errno = e; return ( - 1 ); } return (s); }

Which means we can actually make HTTP requests using Bash:

exec 3<> /dev/tcp/checkip.amazonaws.com/80 printf "GET / HTTP/1.1\r

Host: checkip.amazonaws.com\r

Connection: close\r

\r

" >& 3 tail -n1 <& 3

No curl needed! /jk

Apart from Bash, in the versions and configurations packaged in Ubuntu 18.04, only ksh supports network redirections – ash , csh , dash , fish , and zsh do not.

I don’t think I will actually have any use for network redirections but this was a fun little rabbit hole to dive into.

NOTE: Code snippets from Bash are licensed under GPLv3, the snippet from the man page is licensed differently