Why can I open a connection on port 80 for 0.0.0.0 but not other forms? My understanding of Linux/MacOS (I think they are the same in this regard) port permissions is that ports 1-1024 are only available to root, but above that, they are available to anyone.

However, I've run into some unexpected behavior when opening ports on MacOS 10.14.2. I'm using php -S (version 7.3.0) to experiment.

When I run the command with the following cases:

127.0.0.1:80
[::1]:80
mylocalipv4:80
mylocalipv6:80


it returns Failed to listen on ::1:80 (reason: Permission denied) as expected (where the IP/port given is shown).

However, if I run it with 0.0.0.0:80, it successfully starts listening, but I can't connect to it with curl, either using 127.0.0.1 or the actual IP of my machine.

If I run the PHP server with [::0]:80 it successfully starts listening, and I can curl it with curl [::1] and with my local ipv6 address.

I would have expected the last two cases to fail to start with permission denied. Why don't they?
From testing I can see that it is possible for a desktop macOS users to bind() TCP port numbers below 1024 (the so-called well-known ports). It works on both IPv4 and IPv6, and on the localhost/unspecified addresses.

The Darwin kernel source code is freely available - from there I can see that it has the functionality built-in to check for processes trying to bind/listen on ports &lt;1024 and deny that. This is similar to the way Linux works. However, it is possible to disable this check when compiling the kernel by defining "IPNOPRIVPORTS". I assume this is what was done with the Mojave supplied kernel.

On Linux it used to be the case that it wasn't possible for non-root to bind low port numbers, but today you have various options of allowing non-root users to bind to low numbered ports. Such as for example using the capability NET_BIND_SERVICE.

On macOS you also have various options for restricting binding low-numbered ports. For example through kexts that does socket filtering, or through other means. Perhaps you have software installed, such as for example Little Snitch, that denied you permission when you were testing.

Please note that the various IP-addresses you have tried with have entirely different meanings:

 127.0.0.1 = IPv4 address that means the "localhost" (i.e. your own computer basically) - it is not available on the network
 [::1]     = the IPv6 equivalent of 127.0.0.1

 0.0.0.0   = IPv4 "unspecified address" - i.e. this is not an address in itself, rather it is a special value you give to bind() to let it know that you haven't specified a specific address that you want the port bind() on. Essentially this means that the port will be bound on all the IP-addresses you have.
 [::0]     = IPv6 equivalent of 0.0.0.0 (though usually written [::/128])


This can explain why some things failed and some things succeeded. For example you might not be able to bind to 127.0.0.1, but 0.0.0.0 works as it tries to bind the port not only on 127.0.0.1 but also on other IP-addresses you have.