whatwg / url

URL Standard
https://url.spec.whatwg.org/
Other
531 stars 140 forks source link

Addressing HTTP servers over Unix domain sockets #577

Closed rkjnsn closed 2 years ago

rkjnsn commented 3 years ago

It is often desirable to run various HTTP servers that are only locally connectable. These could be local daemons that expose an HTTP API and/or web GUI, a local dev instance of a web server, et cetera.

For these use cases, using Unix domain sockets provides two major advantages over TCP on localhost:

  1. Namespacing. If two users on a system are running the same service, TCP requires them both to pick, configure, and remember different port numbers. With Unix domain sockets, each socket can live in the respective user's runtime directory and be named after the service.
  2. Access control. Even if the service is diligent only to bind to localhost, TCP still allows any (non-sandboxed) process or user on the machine to connect. Any access control has to be implemented by the service itself, which often involves implementing (hopefully with sufficient security) its own password authentication mechanism. Unix domain sockets, on the other hand, can take advantage of the access control functionality provided by the filesystem, and thus can easily be restricted to a single user or set of users. In the event that a service wants to allows multiple users to connect and discriminate between them, several operating systems provide a means of querying the UID of the connecting process, again without requiring it's own authentication scheme.

Indeed, due to these advantages, many servers/services already provide options for listening via a Unix domain socket rather a local TCP port. Unfortunately, there is not currently an agreed-upon way to address such a service in a URL. As a result, clients who choose to support it end up creating there own bespoke approach (e.g., a special command-line flag, or a custom URL format), while others choose not to support it so as not to bring their URL parsing out-of-spec (among other potential concerns).

Here are some of the various URL formats I've seen used or suggested:

References: Archived Google+ post suggesting the socket-as-port approach:
https://web.archive.org/web/20190321081447/https://plus.google.com/110699958808389605834/posts/DyoJ6W6ufET
My request for this functionality if Firefox, which sent me here:
https://bugzilla.mozilla.org/show_bug.cgi?id=1688774
Some previous discussion that was linked in the Firefox bug:
https://daniel.haxx.se/blog/2008/04/14/http-over-unix-domain-sockets/
https://bugs.chromium.org/p/chromium/issues/detail?id=451721

thx1111 commented 10 months ago

@mnot:

Creating a new TLD for one protocol isn't good architecture, and a lot of people are going to push back on it.

On reflection, I'm going to totally agree with that.

Changing the URL syntax requires coming up with a solution for all URLs, not just HTTP. ... That makes defining a new URL scheme the approach that's most likely to succeed.

There is nothing in any of my, or several other, proposals that is specific to only the http/https "schemes", as the term is defined in RFC 3986. Again, RFC 8820, Section 2.1, "URI Schemes", strongly discourages the introduction of new "schemes".

I have suggest three alternatives for - to put it generally - Address Family "port" addressing.

Extending the overloaded use of the colon ":" delimiter:

 http://:/path/to/socket:/path/to/resource.html...
 http://user@[::1]:/path/to/socket:/path/to/resource.html...

Extending the square bracket hack:

 http://:[/path/to/socket]/path/to/resource.html...
 http://user@[::1]:[/path/to/socket]/path/to/resource.html...

Using alternate delimiters, eliminating the double slash "//", the square bracket hack "["..."]", and the overloaded use of the colon ":" delimiter, as for instance:

 http:&/path/to/socket+/path/to/resource.html...
 http:user@::1&/path/to/socket+/path/to/resource.html...

More generally, any specific delimiter between RFC 3986 "authority" and "path" would solve the URI issue raised here. To illustrate, where RFC 3986 has defined:

      URI         = scheme ":" hier-part [ "?" query ] [ "#" fragment ]

      hier-part   = "//" authority path-abempty
                  / path-absolute
                  / path-rootless
                  / path-empty

      authority   = [ userinfo "@" ] host [ ":" port ]

This would instead become:

      URI = scheme ":" [ userinfo "@" ] host [ ":" port ] "your-favorite-delimiter-here" path-something [ "?" query ] [ "#" fragment ]

The essential problem for Address Family "port" addressing comes down to RFC 3986 failing to just define a specific delimiter between its "authority" and "path" components, or, stating this another way, failing to define a specific delimiter which precedes its "path" component. And then, RFC 3986 struggles desperately to overcome this failure in Section 3.3. Path, explaining "The ABNF requires five separate rules to disambiguate these cases, only one of which will match the path substring within a given URI reference."

Section 3.3. even provides an unconvincing example of "path" while trying to "paper-over" this failure:

   A path consists of a sequence of path segments separated by a slash
   ("/") character.  A path is always defined for a URI, though the
   defined path may be empty (zero length).  Use of the slash character
   to indicate hierarchy is only required when a URI will be used as the
   context for relative references.  For example, the URI
   <mailto:fred@example.com> has a path of "fred@example.com", whereas
   the URI <foo://info.example.com?fred> has an empty path.

Why try to "shoehorn" mailto:fred@example.com into an example of "path"? "fred@example.com" looks like a perfectly good example of 'userinfo "@" host' to me. There is no need to call it something else, attempting to justify the missing useless double slash "//", which otherwise requires "mailto://fred@example.com".

mnot commented 10 months ago

Again, RFC 8820, Section 2.1, "URI Schemes", strongly discourages the introduction of new "schemes".

I wrote that RFC. That is not what Section 2.1 says.

thx1111 commented 10 months ago

Hmm - copying the text: https://www.rfc-editor.org/rfc/rfc8820

Abstract
...  While it is common for schemes to further
delegate their substructure to the URI's owner, publishing independent standards that mandate particular
forms of substructure in URIs is often problematic.
...
2.1. URI Schemes
...
A Specification that defines substructure for URI schemes overall (e.g., a prefix or suffix for URI scheme
names) MUST do so by modifying [BCP35] (an exceptional circumstance).

and, https://www.rfc-editor.org/info/bcp35

Abstract
This document updates the guidelines and recommendations, as well as
the IANA registration processes, for the definition of Uniform
Resource Identifier (URI) schemes. It obsoletes RFC 4395.

Then, by "exceptional circumstance", you meant modifying, literally, the document BCP35 itself, and not the resulting list of registered "schemes" referencing BCP35? I stand corrected.

Still, there is the problem of modifying existing, or creating new, applications able to utilize any particular scheme. I don't expect that my web browser actually supports the currently 374 different registered schemes available. In fact, the trend has been for, for instance, web browsers to drop support for less commonly used schemes - no more gopher, ftp, or mailto - with some functionality being replaced by specialized scheme applications or by "groupware" suites.

I still don't agree that defining and registering a new scheme, exclusively to support html rendering from a local unix domain socket, is a good idea. Rather, that use case does serve to illuminate a deeper systemic fault in RFC 3986.

I did rather like gopher, though, ...

agowa commented 10 months ago

@mot: A while ago I already worte to the IETF mailing lists about such a change, but they just forwarded me here. I don't remember all the details, as the whole thing started ages ago (ok, probably more like about one year), but I could try to look for these related mails.

You already have seen my initial suggestion in another ticket? https://github.com/whatwg/url/issues/778#issue-1796979425

It would be backwards compatible by allowing for default values to be omitted. It would work with everything that currently uses the URL Schema (in a standards compliant way, at least). And it would also allow for the very verbose way of specifying all the protocols down to the wire....

mnot commented 10 months ago

@thx1111:

Then, by "exceptional circumstance", you meant modifying, literally, the document BCP35 itself, and not the resulting list of registered "schemes" referencing BCP35? I stand corrected.

Understand that RFC 8820 is best current practice for applications that use HTTP (what some people call "HTTP APIs" or "REST APIs") -- it's saying that it's exceptional that a one of them would require a new scheme.

Still, there is the problem of modifying existing, or creating new, applications able to utilize any particular scheme. I don't expect that my web browser actually supports the currently 374 different registered schemes available. In fact, the trend has been for, for instance, web browsers to drop support for less commonly used schemes - no more gopher, ftp, or mailto - with some functionality being replaced by specialized scheme applications or by "groupware" suites.

Browsers are going to have to change if they want to support anything that happens here, so that isn't a decisive factor regarding syntax.

I still don't agree that defining and registering a new scheme, exclusively to support html rendering from a local unix domain socket, is a good idea.

HTTP isn't just for HTML.

To be clear, I don't think a new scheme is the only way to do this; it's just more straightforward than other suggestions so far.

@agowa:

You already have seen my initial suggestion in another ticket? https://github.com/whatwg/url/issues/778#issue-1796979425

I hadn't, but that seems like a lot of work (and abstraction) to get to the goals here.

Normally, protocols can negotiate transitions like this (see eg the evolution from HTTP 1-3). What's different here is that unix domain sockets have a completely different authority, and a subtly different transport (as opposed to TCP).

PHJArea217 commented 2 weeks ago

Just wanted to chime in here with my own opinion on this.

Unix domain sockets are an OS-specific transport. Windows has named pipes instead. Due to their local nature, simply embedding the Unix socket path or named pipe path would result in two different and somewhat incompatible representations for applications which must work on both Windows and Unix-like systems.

The ideal solution, would be to have some kind of alternative, ideally OS-neutral, namespace, perhaps under the IPv6 link-local or other reserved range, which maps directly to OS-specific network transports like Unix domain sockets or named pipes. Note that unlike as stated above, it is expected that the top level domain or IPv6 prefix, under which the Unix domain sockets or named pipes will be mapped, be configurable, to prevent collisions. For example, one could connect to fe8f::3:6:0 port 12345, and it would map to a Unix socket at /run/00006/00000_12345. This may look weird at first, but the resulting path can still be symlinked to the actual socket to connect to. More generally, such a namespace and mapping could be configured by the user in a way such that an agreed-upon domain name or IP address could appear to connect to a local service, in a way that could be consistent across different operating systems, such that the agreed-upon domain name or IP address can be used to access the services in an OS-independent manner.

The rationale for link local here is because at least on Linux, they fail "closed" i.e. will not result in any actual TCP connection if they are not recognized. The 127.0.0.0/8 range can also be used, in which case they can still leak a TCP connection, but the surface is still limited to the local host.

randomstuff commented 2 weeks ago

Unix domain sockets are an OS-specific transport. Windows has named pipes instead.

AFAIU, AF_UNIX has come to Windows.

For example, one could connect to fe8f::3:6:0 port 12345, and it would map to a Unix socket at /run/00006/00000_12345.

One benefit of filesystem sockets is that you can skip the numeric address part and directly map human-friendly names (virtual hostnames) into (human-friendly) paths. This way you avoid the cumbersome task of managing a mapping human-firendly virtual hostnames into numeric addresses.

PHJArea217 commented 2 weeks ago

Unix domain sockets are an OS-specific transport. Windows has named pipes instead.

AFAIU, AF_UNIX has come to Windows.

True, I know that AF_UNIX does exist on Windows. But the idea of mapping IPv6 addressing to Unix sockets would not be limited to Unix sockets, but rather also to other stream-based TCP-like transports like AF_VSOCK.

For example, one could connect to fe8f::3:6:0 port 12345, and it would map to a Unix socket at /run/00006/00000_12345.

One benefit of filesystem sockets is that you can skip the numeric address part and directly map human-friendly names (virtual hostnames) into (human-friendly) paths. This way you avoid the cumbersome task of managing a mapping human-firendly virtual hostnames into numeric addresses.

Hostnames tend to be stable, Unix domain sockets tend to be not.

The same effect could be accomplished by putting one of those IP addresses into the /etc/hosts file, resulting in a mapping from a human-friendly domain to the IP address which maps to the unix socket. This also means that there would not need to be any changes to SSL/TLS certificates either, one can continue to use DNS subject alt names.

The connection to fe8f::3:6:0 port 12345 is not an actual TCP connection, but is specially interpreted by a modification of the connect() system call in the TCP/IP socket API, causing a connection to a Unix domain socket rather than a TCP socket. This is implemented by my socketbox and u-relay-tproxy projects (see my github profile).

The advantage of this mapping is that the set of allowed Unix domain sockets that could be connected to is naturally restricted to the end-user-defined mapping of IPv6 prefixes to filesystem path prefixes. Only the unix sockets under path prefixes mentioned in a user-defined mapping would be visible to the application. (To put things another way, the file:// URL scheme could also be sandboxed to a chroot, and the view of the filesystem as observed through file:// URLs can still be totally valid.)

randomstuff commented 2 weeks ago

The same effect could be accomplished by putting one of those IP addresses into the /etc/hosts file, resulting in a mapping from a human-friendly domain to the IP address which maps to the unix socket.

That's what I was saying by "you don't have to manage numeric addresses". In your proposition, you would still have to maintain a (/etc/hosts file) mapping host names into IP addresses and your filesystem is now filled with numeric Unix socket addresses which are a lot less clearer than eg. /run/user/1000/dev/app.foo.localhost.

Only the unix sockets under path prefixes mentioned in a user-defined mapping would be visible to the application.

You can achieve the same effect by directly mapping some domain names into Unix socket paths (without the intermediate IPv6 address).

Note: if you map host names to special IP addresses which are mapped to Unix sockets, you then have to decide what happens when you receive one of those special IPv6 address from the DNS. Accepting them might be a vulnerability (and for example open up your service to DNS-rebinding attacks). Loopback IP addresses and private IP addresses are often filtered for this reason.

PHJArea217 commented 2 weeks ago

The same effect could be accomplished by putting one of those IP addresses into the /etc/hosts file, resulting in a mapping from a human-friendly domain to the IP address which maps to the unix socket.

That's what I was saying by "you don't have to manage numeric addresses". In your proposition, you would still have to maintain a (/etc/hosts file) mapping host names into IP addresses and your filesystem is now filled with numeric Unix socket addresses which are a lot less clearer than eg. /run/user/1000/dev/app.foo.localhost.

Only the unix sockets under path prefixes mentioned in a user-defined mapping would be visible to the application.

You can achieve the same effect by directly mapping some domain names into Unix socket paths (without the intermediate IPv6 address).

Sure, and I guess you might be kind of right about that.

The mapping of IPv6 addressing to unix domain sockets in this manner was something that could be easily done in an LD_PRELOAD library, which ensured that it would also work with applications that resolved the domain name to an IP and connected to that IP without having to change the application. But the mapping might be a little bit more flexible because you can map multiple domains to a single IP, which can be useful for testing name based virtual hosting.

Note: if you map host names to special IP addresses which are mapped to Unix sockets, you then have to decide what happens when you receive one of those special IPv6 address from the DNS. Accepting them might be a vulnerability (and for example open up your service to DNS-rebinding attacks). Loopback IP addresses and private IP addresses are often filtered for this reason.

Any sane application will use security features like checking the Host header or rejecting an SSL certificate to prevent this. Besides, it doesn't need to be under fe8f::3:0:0/96, it could also be under an IPv4 loopback prefix.

minfrin commented 2 weeks ago

Just wanted to chime in here with my own opinion on this.

Unix domain sockets are an OS-specific transport. Windows has named pipes instead. Due to their local nature, simply embedding the Unix socket path or named pipe path would result in two different and somewhat incompatible representations for applications which must work on both Windows and Unix-like systems.

All of this is orthogonal to the problem, which is we need a standardised way to express an http(s) URL that points to a unix domain socket. There is no reason why an arbitrary (and legacy) difference between two arbitrary operating systems should place a limit on a standard like an URL definition.

Other types of sockets on other platforms are discussions that should go under a separate issue.

minfrin commented 2 weeks ago

To be clear, I don't think a new scheme is the only way to do this; it's just more straightforward than other suggestions so far.

What is the next step, a draft RFC?

I keep hitting this problem at https://github.com/apache/httpd, if an RFC is the way forward I can make some time for it.

PHJArea217 commented 2 weeks ago

You're right, this is the url repo on GitHub, and as such would be the place to define a URL standard for encoding a Unix domain socket path.

The Unix socket paths would of course be interpreted in an OS-dependent manner, which is generally not a problem at all. Consider file:/// URLs in Windows, which are typically of the form file:///C:/Users/Username/Example. Even though Windows uses drive letters rather than a root hierarchy like on Unix-like systems, it is still able to make use of file:/// URLs, by encoding the pathname in a clear and straightforward manner, representing C:\Users\Username\Example as /C:/Users/Username/Example.

All of the comments above would effectively be means of encoding a file path string in place of the domain name or IP address of a URL string. This is not much different from the specification of an "interface" in a programming language like Java or Go, which generally specify what is available to a user and what functions an implementation has to implement, but they generally do not specify how they should be implemented. And as discussed above, Windows file:/// URLs are different from Unix file:/// URLs. That is, while both Windows and Unix use the common interface of a file:/// URL, they implement them differently. And that's not a problem at all.

Which means that we can effectively generalize this issue from "encoding a Unix socket path" to "encoding a filesystem path like string which acts as the equivalent of a hostname, which could be interpreted in a system-dependent manner". And just like how I mentioned that file:/// URLs can be chrooted, all of the following could theoretically be possible for the "Unix domain socket URL scheme":

So I'm quite supportive of the issue at hand, so long as it's not restricted to AF_UNIX sockets, because if we need to support other types of transport, then we won't need to have all of this discussion again.

PHJArea217 commented 1 week ago

But one thing I sort of need to point out in this context is still the fact that the URL standard is still an "interface". Which means that we need to differentiate between attempts to modify the "interface" of a URL by changing the URL standard, and attempts to modify the "implementation" of URLs by changing individual implementations, the former of which is merely a means of abstractly expressing a Unix domain socket or similar string in a URL with few constraints on the actual implementation, and the latter of which is the actual means of connecting to a Unix domain socket i.e. x = socket(AF_UNIX, SOCK_STREAM, 0); x.connect({AF_UNIX, "/some_unix_path"});.

All of the above syntax proposals would effectively be modifying the "interface" of a URL to support an extra method of connecting to a Unix domain socket. In many cases, interface implementations are not required to implement every possible method, if it is known that users of the interface will not use that method, and that is certainly true in other contexts (such as the Java Collections API with immutable or unmodifiable collections used with functions that only attempt to read from the collection). Sorry, but the Liskov substitution principle is not very applicable, otherwise every client that implements URLs would have to support every single URL scheme, and that is simply infeasible. Which means that even if we do have a standard for encoding a Unix domain socket path or similar string in a URL string, we cannot guarantee that every implementation of URLs (such as in browsers) will honor it.

This effectively means that many of the linked issues regarding non-support of Unix domain sockets in various clients that take in URLs might be considered to be wishful thinking, that is, even if a scheme for encoding a Unix domain socket path is devised, there is no guarantee that every app will end up supporting it.

On the other hand, my and @randomstuff's proposals of proxy servers or LD_PRELOAD libraries to support the connection of clients to Unix domain sockets are means of changing the implementation of URLs. It is similar to adding support for a new filesystem in an operating system kernel: the new filesystem can be used by applications transparently, by referencing paths on that filesystem in file access APIs, without having to change the application, because all the different filesystems all share the same interface. This is generally much more feasible to accomplish.

LD_PRELOAD might not be possible for Go binaries at this moment, but this is being worked on.

Ultimately, this means that the mere act of connecting to a Unix domain socket is not necessarily something that requires changing the URL standard, if it is possible to shoehorn it into some existing interface. It may seem very hacky or unsightly, but the major advantage is that client applications do not need to be changed, considering how many HTTP clients or web browsers there exists out in the wild.

A similar issue exists in issue #392 where there is discussion on encoding an IPv6 link local zone identifier in a URL. The mere act of connecting to an ipv6 link local address is something that can simply be done by changing the implementation. For example, interpreting subdomains of the ipv6-literal.example domain as "resolving" a string encoding an IPv6 link local with scope into a sockaddr_in6 which fills sin6_scope_id. It is not necessary to change the interface of a URL in doing so, because it reuses the existing "domain" interface.

A more relatable example is the fact that the URL syntax did not need to be changed in order for connections to domain names to go over IPv6. If the URL standard did not have the square bracket notation, then it would have still been possible to connect to IPv6 websites on the network layer, the only limitation would have been that it would have required the use of a domain name to do so. The main reason why the URL standard ultimately did need to be changed in that case is because of the legitimate interest in connecting to IPv6 literal websites in the same way we could have done it with IPv4.