Opened 2 years ago
Closed 2 years ago
#2400 closed enhancement (fixed)
listen on localhost can fail (sort of a docker and glibc issue, worth working around?)
Reported by: | Owned by: | ||
---|---|---|---|
Priority: | minor | Milestone: | |
Component: | nginx-core | Version: | 1.23.x |
Keywords: | docker localhost listen glibc getaddrinfo | Cc: | |
uname -a: |
Linux be43d003a089 5.10.109-0-virt #1-Alpine SMP Mon, 28 Mar 2022 11:20:52 +0000 aarch64 GNU/Linux
(note, this is not actually alpine, it's debian from the docker library nginx image): # cat /etc/os-release PRETTY_NAME="Debian GNU/Linux 11 (bullseye)" NAME="Debian GNU/Linux" VERSION_ID="11" VERSION="11 (bullseye)" VERSION_CODENAME=bullseye ID=debian |
||
nginx -V: |
nginx version: nginx/1.23.1
built by gcc 10.2.1 20210110 (Debian 10.2.1-6) built with OpenSSL 1.1.1k 25 Mar 2021 (running with OpenSSL 1.1.1n 15 Mar 2022) TLS SNI support enabled configure arguments: --prefix=/etc/nginx --sbin-path=/usr/sbin/nginx --modules-path=/usr/lib/nginx/modules --conf-path=/etc/nginx/nginx.conf --error-log-path=/var/log/nginx/error.log --http-log-path=/var/log/nginx/access.log --pid-path=/var/run/nginx.pid --lock-path=/var/run/nginx.lock --http-client-body-temp-path=/var/cache/nginx/client_temp --http-proxy-temp-path=/var/cache/nginx/proxy_temp --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp --http-scgi-temp-path=/var/cache/nginx/scgi_temp --user=nginx --group=nginx --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-g -O2 -ffile-prefix-map=/data/builder/debuild/nginx-1.23.1/debian/debuild-base/nginx-1.23.1=. -fstack-protector-strong -Wformat -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -fPIC' --with-ld-opt='-Wl,-z,relro -Wl,-z,now -Wl,--as-needed -pie' |
Description
Steps to reproduce:
Run nginx with the following conditions:
- Inside docker
- On a distro that uses glibc (typically anything but alpine)
- Do not enable ipv6 on the container (this is the default)
- Have a listen directive that specifies localhost (e.g.
listen localhost:80;
Observed Behavior:
Nginx fails with the following error message (this can be seen just by validating the config with -t as well):
nginx: [emerg] a duplicate listen 127.0.0.1:80 in /etc/nginx/conf.d/default.conf:2
Desired behavior:
We only attempt to bind to 127.0.0.1 once, and nginx starts up successfully
Explanation
Two longstanding issues from glibc and docker are at play here:
- Docker writes an ipv6 entry for localhost to /etc/hosts but does not assign an ipv6 address to the loopback interface
- Under that condition getaddrinfo returns a duplicate ipv4 address for the loopback interface
Immediate workarounds for users
- Specify
127.0.0.1
instead oflocalhost
- this makes supporting systems with different combinations of ipv4/ipv6 annoying - Don't limit to the loopback, instead use
allow
anddeny
directives to limit traffic from only that container network - Have some other process resolve
localhost
uniquely and generate the nginx config, inserting one or morelisten
directives as necessary
Possible workarounds that nginx could implement
- De-duplicate the addresses returned by
getaddrinfo
- Rewrite
"localhost"
tonull
in thegetaddrinfo
call - in this case the unique loopback addresses appear to be returned correctly- I'm not sure if this could break other cases where
/etc/hosts
resolves localhost to something different - This requires passing a port, fortunately we have one
- I'm not sure if this could break other cases where
Additional notes
- This is very easy to reproduce using the docker library nginx images. Just change the listen directive in
/etc/nginx/conf.d/default.conf
and it appears immediately. - Other container runtimes don't appear to have this issue - they assign a working ipv6 address to the loopback interface
- Alpine linux does not use glibc and does not appear to have this issue -
getaddrinfo
returns the single unique ipv4 loopback address in this case - Neither of the issues in question seem to have gotten any traction for multiple years
Change History (5)
comment:1 by , 2 years ago
Type: | defect → enhancement |
---|
comment:2 by , 2 years ago
For what it's worth I do think Docker is doing something wrong here - either it should assign ::1 to the loopback interface or not write ::1 to /etc/hosts, doing one but not the other seems like misconfiguration. But you're also right that there are possibly a lot of ways that misconfiguration could arise.
If removing the duplicate addresses seems reasonable then I'm for that. But I would also understand if the nginx maintainers don't feel like this is worth fixing. At least now the cause and workarounds are documented.
comment:3 by , 2 years ago
The /etc/hosts
file is usually something static provided by OS, and it is expected to work with different OS settings without requiring modifications. For example, /etc/hosts
with both ::1
and 127.0.0.1
works flawlessly on FreeBSD with IPv6 both enabled and disabled. Similarly, it works fine on Alpine with musl libc.
While Docker probably can implement a workaround to conditionally remove ::1
if IPv6 is not enabled, I personally don't think it is something Docker should do. Rather, it is something to be addressed on the OS level, or, more specifically, in glibc.
Either way, I tend to think it would be good to implement a workaround in nginx, especially considering it is more or less trivial. When a name in the listen
directive resolves into two identical addresses, it is clear enough that the intention is to listen on the address, and complaining about duplicate listen addresses is mostly meaningless.
comment:5 by , 2 years ago
Resolution: | → fixed |
---|---|
Status: | new → closed |
Workaround committed, thanks for prodding this.
This looks like an issue in glibc to me. It shouldn't try to convert
127.0.0.1
to::1
, these are different addresses in different address families. And this wrong conversion causes issues with Docker images with IPv6 disabled which use standard/etc/hosts
with both127.0.0.1
and::1
forlocalhost
.The issue seems to be in nss/nss_files/files-hosts.c, blame points to Ulrich Drepper's commit in 2006, and the only details available seems to be the following:
This wasn't important for nginx till
listen
directive was changed to use all IP addresses returned bygetaddrinfo()
in nginx 1.15.10 (4f9b72a229c1):An obvious workaround on the OS configuration side would be to remove
::1 localhost
from/etc/hosts
on systems without IPv6. I tend to think it's not something Docker can or should do though, as the issue equally affects non-Docker systems without IPv6.On nginx side, the most obvious workaround would be to use explicit addresses to listen on instead of using the
localhost
name. In particular, using just127.0.0.1
might be a good option unless there are IPv6-only hosts.As for proposed workarounds to be implemented in nginx, I'm certainly against any special handling of the
localhost
name.Filtering out duplicate addresses from a particular name might be indeed a reasonable option though. I don't see any cases where it can break things, given that all IP addresses are from the particular name resolution. Further, it might help in other cases when a name maps to multiple identical IP addresses for some reason.
Below is a quick patch which implements filtering out duplicate addresses.