Documentation
Serving IPv4, IPv6, or both
Every Everlock frontend listens on whatever set of addresses you give it. This page covers the three shapes that set can take — one family, both families on every address, and both families on chosen addresses — in the two deployment modes: binding directly, and letting systemd bind and hand the sockets over.
The short version: a listen setting takes a comma-separated list, and a systemd
FileDescriptorName= covers as many sockets as you point at it. Both reach the
same place — one frontend serving several sockets identically.
The one thing to know about dual-stack
A socket belongs to exactly one address family. AF_INET carries IPv4,
AF_INET6 carries IPv6, and no socket is both. What looks like a dual-stack
socket is an IPv6 wildcard socket that also accepts IPv4 connections, which
arrive with their addresses mapped into IPv6 form (::ffff:198.51.100.7). That
trick only works for the wildcard [::], and only while the IPV6_V6ONLY
socket option is off.
So there are two ways to reach both families, and the choice follows from whether you are naming addresses:
| You want | How |
|---|---|
| Every address on the host, both families | one [::] socket with v4-mapping on |
| Chosen addresses | one socket per address — two, if you name one of each family |
Everlock supports both, because "every address on the host" is the wrong answer
on a machine that also runs an OS sshd or a systemd-resolved stub.
Binding directly
A listen setting accepts several addresses, separated by commas. This is the whole of it:
IPv6 literals are bracketed, exactly as in a URL — the colons inside an address are not the port separator. The same form works for every frontend's listen setting, on the command line, in the environment, and in the stored config:
EVERLOCK_FRONTEND_HTTP_LISTEN_HTTP='198.51.100.7:80,[2001:db8::1]:80'
Three shapes, spelled out:
# IPv4 only — the default everywhere.
# Both families, every address on the host.
# Both families, chosen addresses.
A bare IP literal with no port takes the frontend's default port, so
--frontend-http-listen-http '198.51.100.7,[2001:db8::1]' is the second line
above. A bare name cannot select an address, so it falls back to the IPv4
wildcard — write an address, not a hostname.
Why naming both wildcards works
The two families share one TCP and UDP port space, and an IPv6 wildcard socket
accepts IPv4 as well unless IPV6_V6ONLY is set — which by default it is not,
since it follows the host's net.ipv6.bindv6only. Binding 0.0.0.0:80 and then
[::]:80 would therefore be refused as "address already in use", and the
listener would quietly serve one family.
Everlock sets IPV6_V6ONLY on its IPv6 sockets whenever the same setting also
names an IPv4 address, so each address in the list means exactly itself. An
IPv6 address on its own is left as the host configures it, which keeps the
older single-address behaviour where [::]:80 reaches IPv4 through the
v4-mapped path.
One consequence worth knowing: on a lone dual-stack [::] socket, IPv4 clients
arrive as v4-mapped addresses (::ffff:198.51.100.7) and are logged that way.
Naming both wildcards avoids this — IPv4 clients then land on the IPv4 socket
and appear as themselves.
When one address will not bind
Each address binds on its own. If one fails — no IPv6 on the host, an address not configured yet, a port already taken — Everlock logs it and serves the rest:
WARN socket http: could not bind [2001:db8::1]:80: Cannot assign requested address
INFO HTTP server listening on http://198.51.100.7:80
The frontend only fails when nothing bound. Losing one family is worth a look in the log; it is not worth refusing to start.
Ports below 1024
Binding 80, 443, 25, 587, 993, 143 or 53 needs privilege. Either run as root,
grant the binary CAP_NET_BIND_SERVICE, or — the option this page turns to next
— let systemd bind them and hand them over, which is how an unprivileged
Everlock serves the standard ports.
Under systemd socket activation
Here systemd owns the sockets and Everlock adopts them. The listen settings in
ExecStart are not used for these frontends: the adopted sockets are the
configuration, and the unit files are where the addresses live. (Keep the flags
present anyway — for most frontends their presence is what enables the
frontend.)
Everlock finds its sockets by the FileDescriptorName= the unit declares. A
name identifies a role, not a socket, and one name can carry any number of
sockets. That is not a workaround: sd_listen_fds_with_names(3) states plainly
that "the names used are not unique in any way", and it is exactly how a service
that treats IPv4 and IPv6 alike is meant to be configured.
Both families in one unit
List both addresses under one name:
# /etc/systemd/system/everlock-http.socket
[Unit]
Description=Everlock HTTP socket
Wants=network-online.target
After=network-online.target
[Socket]
ListenStream=198.51.100.7:80
ListenStream=[2001:db8::1]:80
BindIPv6Only=ipv6-only
FreeBind=yes
FileDescriptorName=http
Service=everlock.service
[Install]
WantedBy=sockets.target
systemd passes both descriptors under the name http, and Everlock serves both
with the same router. Nothing else changes — the service unit is untouched.
Both families in separate units
Two units may declare the same FileDescriptorName. Everlock treats the
result identically, and it buys one operational property the single unit cannot:
# /etc/systemd/system/everlock-http6.socket
[Socket]
ListenStream=[2001:db8::1]:80
BindIPv6Only=ipv6-only
FreeBind=yes
FileDescriptorName=http
Service=everlock.service
Adding IPv6 to a running server is then systemctl start everlock-http6.socket
followed by systemctl restart everlock.service — the IPv4 socket is never
unbound and its connection backlog survives. Editing the single-unit version
means restarting everlock-http.socket, which drops both listeners for the
moment it takes to rebind.
Use one unit by default; reach for two when you are changing a live server.
The two settings that are easy to get wrong
BindIPv6Only= defaults to default, which follows the host's
net.ipv6.bindv6only sysctl — a value you inherit rather than choose. Set it
explicitly. On a unit that names a specific IPv6 address, ipv6-only also
documents the intent and keeps the meaning if someone later widens the address
to [::]. (On an IPv4 ListenStream= in the same unit it simply does not
apply, so one setting for the pair is fine.)
FreeBind=yes lets a socket bind an address the interface has not
configured yet. systemd's own documentation recommends it "whenever you bind a
socket to a specific IP address", and here it is load-bearing: socket units are
Requires= of everlock.service, so a socket that cannot bind at boot takes
the whole service down with it. Without FreeBind=, a boot where the network
comes up a moment late is a failed Everlock.
Which names exist
One name per behaviour, because that is what changes how a connection is handled. Several addresses — or several ports — for the same behaviour share one name:
| Frontend | Name | Behaviour |
|---|---|---|
| HTTP | http | plaintext; also serves the ACME http-01 challenge |
| HTTPS | https | TLS |
| Git/admin SSH | ssh | |
| SMTP | smtp-inbound | accepts mail for local domains |
| SMTP | smtp-submission | requires authentication |
| IMAP | imap-tls | TLS from the first byte |
| IMAP | imap-starttls | plaintext, upgrades on STARTTLS |
| DNS | dns-udp / dns-tcp | one per transport |
So HTTP on port 80 and 8080, across IPv4 and IPv6, is four ListenStream= lines
under FileDescriptorName=http. But port 465 is not another
smtp-submission socket — implicit TLS is different behaviour from STARTTLS on
587, and Everlock does not serve it.
One constraint no amount of socket flexibility removes: ACME http-01 validation
reaches port 80. If http is bound only to 8080, certificate issuance fails.
Containers
Inside a container the network namespace usually holds one address per family,
so naming addresses has little to do: bind the wildcard and let the host side
pick, which is what -p is for. The image's defaults already do this
(EVERLOCK_FRONTEND_HTTP_LISTEN_HTTP=0.0.0.0:80), and -e moves them.
What does need attention is whether IPv6 reaches the container at all, and what the container sees when it does.
Bridge networks carry no IPv6 by default. The daemon needs it turned on
("ipv6": true with a fixed-cidr-v6, plus "ip6tables": true), and the
network has to be created with IPv6 enabled. Until then, adding [::]:80 to a
listen setting binds a socket that no traffic ever arrives on.
The trap: published ports can rewrite the client's address. With the default
userland-proxy, a connection arriving over IPv6 to a container whose network
is IPv4-only is accepted by the proxy on the host and re-originated to the
container from the gateway. The container then sees the gateway address —
172.17.0.1 — as the client, for every IPv6 client. Everything Everlock does
per client address degrades accordingly:
- the SSH password throttle groups every such client into one bucket, so one attacker's failures delay everyone else
- DNS response rate limiting loses its per-source granularity
- SMTP sees one peer address for all of them
- site access logs record the gateway, not the visitor
This is not specific to Everlock — it is why the same setup is discouraged for mail servers generally — but Everlock's per-source defences are exactly the things it degrades.
Three ways out, in increasing order of bluntness:
- Give the container network real IPv6 and enable
ip6tables, so published ports are translated by the kernel rather than proxied, preserving the source address. - Disable the userland proxy (
"userland-proxy": false) so port publishing goes through the packet filter. - Run with
--network=host. The container then shares the host's stack: Everlock sees the real addresses, both families work as on a native host, and everything else on this page — including naming specific addresses — applies directly. This is also the mode discovery (mDNS and SSDP) needs.
Whichever you choose, confirm it from the logs rather than assuming — a
journalctl/docker logs line showing 172.17.0.1 as a client address is the
symptom.
Checking what you got
Everlock logs every listener it ends up with, one line each, with the address as the socket actually reports it:
|
# HTTP server listening on http://198.51.100.7:80
# HTTP server listening on http://[2001:db8::1]:80
Count those lines. With non-unique names and no ordering guarantee between socket units, this log is the authoritative answer to "what is actually being served" — not the unit files.
From the outside, force each family:
And from systemd's side:
|
|
When it goes wrong
One family times out while the other works. The socket is bound but nothing
is accepting on it. Under socket activation this is the classic symptom of a
FileDescriptorName= Everlock does not recognise — the socket listens under
systemd, the TCP handshake completes, and the connection waits in an accept
queue no one reads. Everlock reports the name at startup:
ERROR socket activation: inherited socket(s) named ["http6"] match no frontend
and will never be served — connections to them hang. Known names: [...]
Note that leaving FileDescriptorName= out entirely does not mean "no name": it
defaults to the unit name including the .socket suffix, so you get
everlock-http.socket, which matches nothing.
Everything is refused after a failed start. Check the socket units, not just
the service. systemd rate-limits socket activation via TriggerLimitBurst=,
which defaults to 20 activations per 2 seconds for these units, and a socket
that breaches it is latched into a failure state — it stays unconnectable
after you fix whatever was crashing. A service that crash-loops on a busy host
reaches that quickly.
A listener survives stopping its socket unit. systemd passes a duplicate of the descriptor; Everlock holds its own. Stopping the socket unit closes systemd's copy while Everlock keeps accepting. To actually drop a listener, stop the socket unit and restart the service.
IPv6 works from the host but not from outside. That is routing or firewall,
not Everlock. ss -tuln showing the bind means the socket is there; check
ip -6 route and any ip6tables/nftables rules, and remember that firewall
rules are per-family — an IPv4 rule set says nothing about IPv6.
Read next
- systemd with socket activation — the full unit setup
- Deployment options — standalone and container alternatives
- Frontends — every transport and its configuration