The BSD Sockets API is Annoyingly Inflexible

The Sockets API requires the application to make too many decisions. An application needs to choose the socket domain and the protocol. (The libc does choose a protocol if the application doesn’t, but that choice isn’t meaningful for us.) This forbids the user to make a choice.

Consider an application talking some protocol. Usually, that protocol might be spoken over TCP over IPv4 or IPv6. Layers lower than that are hidden. So the application might create a AF_INETSOCK_STREAM socket with protocol IPPROTO_TCP. What it is likely to let the user configure is the URI, including a host and a port. The host can be either an IP address or a host/domain name. The port is an integer always, though fault for that lies with the URI RFC. The host name and the port number (or, if the application allows service names, that) the application will resolve using getaddrinfo, whose result it can then connect to.

Abstracting resolving the host and service names using getaddrinfo already allows some degree of configuration: Host name resolving you can configure using the resolv.conf system (e.g., by writing overrides into /etc/hosts). Service name resolving you can configure using /etc/services.

Now what happens when the user wants to use a UDS?

While the socket API allows the application to control the connection method well and while the libc name resolver allows the admin to tightly control the resolution (although the libc resolving interface is far from without issues), it does not by itself encourage giving the user much choice for the connection method.

Perhaps the most UNIX-like (in the positive sense, not in that of cat -v) solution is the good old using file descriptors.

$ mkfifo send recv
$ <send >recv openssl s_client -connect &
$ gopherclient -r 3 3<recv -s 4 4>send
$ gopherclient -r 3 3<uds -s 4 4>uds

A different alternative is having each application that wants to do networky stuff use libraries or local services that do the connecting. Plan 9, for example, has the “connection server” ndb/cs, which takes a triple of protocol directory (e.g., /net.alt/tcp for TCP over an alternate network interface or just net for “whatever”), host (e.g., a domain name), and a service (name or number) and returns a list of similar triples that are more specific. The dial stringnet!host.example!9fs might resolve to tcp!!564 and il!!17008 (IL is an old Plan 9-native transport protocol). Note how the service number (port) is different for different transports. When the application has got its more specific dial string from cs, it uses a system library to parse it and connect to the host using the protocol directory returned in the dial strings.

To use, say, Tor transparently you might then write a file server serving an interface directory similar to /net—say, /net.tor, and tell cs how to handle a new “protocol,” say, onion. For that, it might not try to resolve domain names: onion!example.onion!9fs might be resolved to onion!example.onion!564 and for /net.tor/tcp!host.example!9fs it might use DNS (over /net.tor/udp) to resolve it to /net.tor/tcp!!564.

What about URIs?

They don’t fit well. Different URI schemata are handled in quite different ways. The authority component of a URI is far too inflexible.

URIs are universal not only in that they work everywhere, but also in that they cannot make use of things that do not work everywhere. There is no way to say with a http URI “Talk HTTP to file /dev/ttyS5 and set the Host header to host.example.” They are suitable for being given to other people. They are not suitable as sole mechanism for telling applications where to connect to.