Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ RUN apt-get -q -y update \
openssl \
rsyslog \
net-tools \
iproute2 \
procps \
\
mariadb-client \
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,12 @@ __OFFICIAL MAIL ENVIRONMENT VARIABLES__
- POSTFIX_MYDESTINATION
- specify the domains which this mail-box handles

- INET_PROTOCOLS
- which IP versions postfix listens on and uses: `all`, `ipv4` or `ipv6`
- _default: auto_ — `all` if the container has IPv6 (Docker network with `enable_ipv6`), otherwise `ipv4`
- AUTO_TRUST_NETWORKS
- add all networks this container is connected to and trust them to send mails
- includes IPv6 networks (as `[prefix]/len`) when the container has IPv6
- _set to any value to enable_
- ADDITIONAL_MYNETWORKS
- add this specific network to the automatically trusted onces
Expand Down Expand Up @@ -171,6 +175,27 @@ _some characters might brake your configuration!_

_for example: to set_ ___mynetworks_style = subnet___ _just add a environment variable_ ___POSTFIX_RAW_CONFIG_MYNETWORKS_STYLE=subnet___


## IPv6

Postfix and Dovecot listen on IPv6 whenever the container has it. With
plain Docker defaults a container is IPv4-only, and published ports reach it
over IPv6 through `docker-proxy`, which connects to the container over IPv4 —
so every IPv6 client shows up as the Docker network gateway (`172.x.0.1`).
To see real IPv6 client addresses, give the network IPv6 and let Docker NAT
IPv6 itself (`/etc/docker/daemon.json`: `"ip6tables": true`, plus
`"experimental": true` on Docker < 27):

```yaml
networks:
default:
enable_ipv6: true
ipam:
config:
- subnet: 172.19.0.0/16
- subnet: fd00:d0c:25::/64
```

## Volumes

- /etc/postfix/tls
Expand Down
36 changes: 32 additions & 4 deletions scripts/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,25 @@ EOF
#
chown -R vmail:vmail /var/vmail

##
# POSTFIX IP PROTOCOLS
##

# Listen on IPv6 too whenever the container has it (Docker network with
# enable_ipv6). IPv4-only containers stay on ipv4 — Postfix would otherwise
# warn about missing IPv6 support on every start. INET_PROTOCOLS overrides.
if [ -z ${INET_PROTOCOLS+x} ]; then
if grep -q . /proc/net/if_inet6 2>/dev/null; then
INET_PROTOCOLS=all
else
INET_PROTOCOLS=ipv4
fi
fi
echo ">> postfix inet_protocols: $INET_PROTOCOLS"
postconf -e "inet_protocols=$INET_PROTOCOLS"

AVAILABLE_NETWORKS="127.0.0.0/8"
[ "$INET_PROTOCOLS" = "ipv4" ] || AVAILABLE_NETWORKS="$AVAILABLE_NETWORKS,[::1]/128"
if [ ! -z ${AUTO_TRUST_NETWORKS+x} ]; then
AVAILABLE_NETWORKS=$(list-available-networks.sh | tr '\n' ',' | sed 's/,$//g')
echo ">> trust all available networks: $AVAILABLE_NETWORKS"
Expand Down Expand Up @@ -75,7 +93,7 @@ if [ ! -f "$INITIALIZED" ]; then
export MYSQL_USER=dbuser
export MYSQL_PASSWORD=dbpassword

/usr/bin/mysqld_safe &
/usr/bin/mariadbd-safe &
echo ">> waiting for mysql socket."
while [ ! -e "/var/run/mysqld/mysqld.sock" ]; do sleep 1; echo -n "."; done
echo ""; echo ">> mysql socket found :)"
Expand All @@ -88,7 +106,12 @@ if [ ! -f "$INITIALIZED" ]; then

sh -c "mysql < /tmp/autocreatedb.mysql && echo '>> db '$MYSQL_DBNAME' successfully installed'; rm /tmp/autocreatedb.mysql; update-database.sh"

killall mysqld
# stop the setup instance before runit starts the supervised one.
# (MariaDB 11 runs as "mariadbd" — the old `killall mysqld` matched
# nothing, left this instance running unsupervised, and runit's mysqld
# service then looped on "A mysqld process already exists" forever.)
mariadb-admin shutdown
while [ -e "/var/run/mysqld/mysqld.sock" ]; do sleep 1; done
else
echo ">> using '$MYSQL_HOST' as Database Host"

Expand Down Expand Up @@ -369,8 +392,8 @@ EOF
echo ">> RUNIT - create services"
mkdir -p /etc/sv/rsyslog /etc/sv/postfix /etc/sv/dovecot /etc/sv/mysqld

echo -e '#!/bin/sh\nexec /usr/bin/mysqld_safe' > /etc/sv/mysqld/run
echo -e '#!/bin/sh\nkillall mysqld' > /etc/sv/mysqld/finish
echo -e '#!/bin/sh\nexec /usr/bin/mariadbd-safe' > /etc/sv/mysqld/run
echo -e '#!/bin/sh\nmariadb-admin shutdown 2>/dev/null || killall -q mariadbd' > /etc/sv/mysqld/finish


echo -e '#!/bin/sh\nexec /usr/sbin/rsyslogd -n' > /etc/sv/rsyslog/run
Expand All @@ -391,6 +414,11 @@ EOF

fi

# dovecot's auth + lmtp sockets live in postfix's private dir; postfix only
# creates it when it starts, and runit starts both at once. Create it up
# front so dovecot doesn't fail its first start ("Failed to start listeners").
install -d -o postfix -g root -m 0700 /var/spool/postfix/private

##
# TLS Cert Renew Stuff
##
Expand Down
54 changes: 16 additions & 38 deletions scripts/list-available-networks.sh
Original file line number Diff line number Diff line change
@@ -1,40 +1,18 @@
#!/bin/bash
#!/bin/sh
# Print every network this container is directly attached to, one per line,
# in Postfix mynetworks notation (IPv4 a.b.c.d/n, IPv6 [prefix]/n).
# Used by AUTO_TRUST_NETWORKS.
#
# Connected routes are exactly the attached networks, with the host bits
# already zeroed — no netmask arithmetic, and IPv6 works the same way.
# Link-local and multicast IPv6 prefixes are never trusted.

function getNetworkFromAddressAndNetmask {
IFS=. read -r i1 i2 i3 i4 <<< "$1"
IFS=. read -r m1 m2 m3 m4 <<< "$2"
printf "%d.%d.%d.%d\n" "$((i1 & m1))" "$((i2 & m2))" "$((i3 & m3))" "$((i4 & m4))"
}
echo "127.0.0.0/8"
ip -4 route show 2>/dev/null \
| awk '$1 ~ /\// && $1 != "default" && !/ via / { print $1 }'

function getCidrSuffixFromNetmask {
nbits=0
IFS=.
for dec in $1 ; do
case $dec in
255) let nbits+=8;;
254) let nbits+=7;;
252) let nbits+=6;;
248) let nbits+=5;;
240) let nbits+=4;;
224) let nbits+=3;;
192) let nbits+=2;;
128) let nbits+=1;;
0);;
*) echo "Error: $dec is not recognised"; exit 1
esac
done
echo "$nbits"
}


IFS=$'\n' # make newlines the only separator
for j in $(ifconfig | grep inet | tr ' ' '\n' | grep 'Mask\|add' | tr '\n' ' ' | sed 's/addr/\naddr/g' | grep . | sed 's/[^0-9. ]//g')
do
ADDR=$(echo "$j" | cut -d' ' -f1)
MASK=$(echo "$j" | cut -d' ' -f2)

NETWORK=$(getNetworkFromAddressAndNetmask "$ADDR" "$MASK")
CIDR=$(getCidrSuffixFromNetmask "$MASK")

echo "$NETWORK/$CIDR"
done
grep -q . /proc/net/if_inet6 2>/dev/null || exit 0
echo "[::1]/128"
ip -6 route show 2>/dev/null \
| awk '$1 ~ /\// && $1 != "default" && !/ via / && $1 !~ /^(fe80|ff[0-9a-f][0-9a-f]):/ {
split($1, p, "/"); print "[" p[1] "]/" p[2] }'
8 changes: 5 additions & 3 deletions test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ echo ">> starting container"
docker rm -f "$CN" >/dev/null 2>&1 || true
docker run -d --name "$CN" -e MAIL_FQDN=mail01.test.tld "$IMG" >/dev/null

echo ">> waiting for dovecot to listen on 143 (up to 120s)"
# wait for both daemons: they start in parallel under runit, and dovecot is
# usually up a few seconds before postfix's `service postfix start` is done
echo ">> waiting for dovecot (143) and postfix (25) to listen (up to 120s)"
up=0
for _ in $(seq 1 60); do
if docker exec "$CN" bash -c 'exec 3<>/dev/tcp/127.0.0.1/143' 2>/dev/null; then up=1; break; fi
if docker exec "$CN" bash -c 'exec 3<>/dev/tcp/127.0.0.1/143 && exec 4<>/dev/tcp/127.0.0.1/25' 2>/dev/null; then up=1; break; fi
sleep 2
done
[ "$up" = 1 ] || fail "dovecot did not start listening on 143 in time"
[ "$up" = 1 ] || fail "dovecot/postfix did not start listening on 143/25 in time"

echo ">> assert: container is running"
[ "$(docker inspect -f '{{.State.Running}}' "$CN")" = true ] || fail "container not running"
Expand Down
Loading