Systemd-resolved + wireguard + docker + ipv6
- Предыстория
- Подключение к postgres с ноутбука в сети Wireguard
- Docker app + posrgres в сети fly.io (способ через docker, сложный)
- Docker app + posrgres в сети fly.io (способ через nginx, простой)
- Resources
Предыстория
Решил я как-то попробовать сервис fly.io, с целью расширения кругозора по инфраструктурным провайдерам (а вовсе не потому что fly.io предоставляет free allowances, включающий 3 виртуалки по 1 cpu и 256 mb ram каждая)
На такой виртуальной машине можно, например, поднять сервер postgres для пет-проекта (такого как chipa). Это делается по короткой инструкции. В результате получаем запущенный postgres на виртуальной машине внутри fly.io:
Без дополнительных действий данная машина доступна только внутри сети fly.io. Команда fly ips list показывает только приватный (внутренний для сети fly.io) IP:
$ fly ips list --app dry-glade-1553 created
VERSION IP TYPE REGION CREATED AT
private_v6 fdaa:1:ca13:0:1::2 private global 2023-03-30T20:06:05Z
В обычном случае другие сервисы тоже разворачиваются в fly.io, и сервисы видят друг друга. Однако в моем случае сервисы будут вне fly.io, также меня интересует подключение к postgres с ноутбука.
Чтобы открыть сервис для внешнего мира, существует несколько способов:
- Добавить внешний IP https://fly.io/docs/postgres/connecting/connecting-external
- Подключиться к внутренней сети через туннель WireGuard https://fly.io/docs/reference/private-networking/#private-network-vpn
Рассмотрим способ через WireGuard: внутренние сервисы системы в таком варианте не открыты в интернет => снимается множество вопросов безопасности (сервисы настраиваются как для работы в одной внутренней сети, сложность настройки переложена на уровень инфраструктуры).
Рассматривается 2 способа: сложный через dns и ipv6, и простой через прокси.
Подключение к postgres с ноутбука в сети Wireguard
На ноутбуке надо установить wireguard. На моём arch linux это делается установкой пакета wireguard-tools.
Далее по инструкции fly.io создаем конфиг wireguard командой fly wireguard create
. Конфиг wireguard прекрасен своей минималистичностью (cat fly.conf):
[Interface]
PrivateKey = ***
Address = fdaa:1:ca13:a7b:8dd7:0:a:102/120
DNS = fdaa:1:ca13::3
[Peer]
PublicKey = ***
AllowedIPs = fdaa:1:ca13::/48
Endpoint = fra2.gateway.6pn.dev:51820
PersistentKeepalive = 15
Копируем конфиг в /etc/wireguard: sudo cp fly.conf /etc/wireguard/
Подключаемся к сети:
$ sudo wg-quick up fly
[#] ip link add fly type wireguard
[#] wg setconf fly /dev/fd/63
[#] ip -6 address add fdaa:1:ca13:a7b:8dd7:0:a:102/120 dev fly
[#] ip link set mtu 1420 up dev fly
[#] resolvconf -a fly -m 0 -x
[#] ip -6 route add fdaa:1:ca13::/48 dev fly
Проверяем что ноутбук видит машину по доменному имени внутри сети fly.io:
$ nslookup dry-glade-1553.internal
Server: 127.0.0.53
Address: 127.0.0.53#53
Non-authoritative answer:
Name: dry-glade-1553.internal
Address: fdaa:1:ca13:a7b:2658:832a:15ea:2
Здесь:
- dry-glade-1553 - имя app в fly.io
- internal - домен во внутренней сети fly.io, см. https://fly.io/docs/reference/private-networking/#discovering-apps-through-dns-on-an-instance
- 127.0.0.53 - локальный DNS-сервер, запущенный systemd-resolved systemd-resolved.
Самое интересное, конечно, как systemd-resolved DNS перевел dry-glade-1553.internal в fdaa:1:ca13:a7b:2658:832a:15ea:2, но это долгая история », я лишь пробегусь кратко по своему конфигу.
Немного про systemd-resolved
Archlinux вики рекомендует использовать systemd-resolved, и я свято следую рекомендациям archlinux wiki
systemd-resolved создает локальный DNS сервис (127.0.0.53 - local DNS stub listener) и перезаписывает /etc/resolve.conf (таким образом все локальные dns запросы должны идти через 127.0.0.53):
$ cat /etc/resolv.conf
# This is /run/systemd/resolve/stub-resolv.conf managed by man:systemd-resolved(8).
# Do not edit.
# ...
nameserver 127.0.0.53
options edns0 trust-ad
search .
Systemd проксирует на нужный DNS сервер согласно правилам:
Pipeline можно изобразить так:
resolve доменное имя -> dns server stub 127.0.0.53 -> link (пример далее) -> dns сервер per link -> ip
Проверяем резолвинг доменного имени приложения fly.io:
$ resolvectl query dry-glade-1553.internal
dry-glade-1553.internal: fdaa:1:ca13:a7b:2658:832a:15ea:2 -- link: fly
-- Information acquired via protocol DNS in 1.2198s.
-- Data is authenticated: no; Data was acquired via local or encrypted transport: no
-- Data from: network
Обращаем внимание на link: fly. Теперь можно найти какой DNS сервер был использован на самом деле:
$ resolvectl status | grep -A 10 fly
Link 43 (fly)
Current Scopes: DNS
Protocols: +DefaultRoute +LLMNR +mDNS -DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: fdaa:1:ca13::3
DNS Servers: fdaa:1:ca13::3
DNS Domain: ~.
fdaa:1:ca13::3 - это DNS Server из конфига wireguard; а fly - сеть wireguard.
Теперь появилась проблема: сайты открываются с большой задержкой…
Obligatory image:
Почему google.com резолвится 5 секунд с поднятой сетью wireguard
Проверяем как systemd-resolved резолвит google.com:
$ resolvectl query google.com
google.com: 142.250.186.142 -- link: fly
2a00:1450:4001:82a::200e -- link: fly
Проблема в том, что systemd-resolved теперь использует dns сервер сети fly для всех доменных имен.
Чтобы это исправить, необходимо указать Domains=~. в /etc/systemd/resolved.conf, подробнее почему это работает написано здесь ».
Мой /etc/systemd/resolved.conf:
$ cat /etc/systemd/resolved.conf
[Resolve]
DNS=8.8.8.8
FallbackDNS=9.9.9.9
Domains=~.
Теперь домен google.com резолвится моментально
$ resolvectl query google.com
google.com: 2a00:1450:4017:805::200e -- link: wlan0
216.58.213.110 -- link: wlan0
-- Information acquired via protocol DNS in 62.3ms.
-- Data is authenticated: no; Data was acquired via local or encrypted transport: no
-- Data from: network
Т.к. используется dns прописанный в моем роутере.
$ resolvectl status | grep -A5 wlan0
Link 4 (wlan0)
Current Scopes: DNS LLMNR/IPv4 LLMNR/IPv6
Protocols: +DefaultRoute +LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
Current DNS Server: 192.168.1.1
DNS Servers: 192.168.1.1
С ноутбука подключились
В результате я могу подключиться к postgres внутри fly.io, через dbeaver по доменному имени внутри сети fly.io:
Но это полдела, в конечном счете postgres будут использовать другие приложения, упакованные в docker.
Docker app + posrgres в сети fly.io (способ через docker, сложный)
Postgres будут использовать другие сервисы, запущенные в docker контейнерах вне сети fly.io:
Проверяем, видит ли контейнер домен внутри сети fly:
$ docker run --rm busybox nslookup dry-glade-1553.internal
Server: 8.8.8.8
Address: 8.8.8.8:53
** server can't find dry-glade-1553.internal: NXDOMAIN
Не видит и использует google dns 8.8.8.8, разбираемся почему…
DNS in docker
nslookup показывает, что в контейнере используется google dns, тогда как нужно чтобы использовался systemd-resolved DNS 127.0.0.53.
docker container читает DNS сервера из /etc/resolv.conf. У меня он выглядит так:
$ docker run --rm busybox cat /etc/resolv.conf
nameserver 8.8.8.8
nameserver 192.168.1.1
search .
В docker resolve.conf можно настроить через конфиг /etc/docker/daemon.json, указав в нем DNS сервер.
Нельзя просто взять, и написать в конфиге “dns”:[“127.0.0.53”], т.к. в сети контейнера нет такого хоста:
$ docker network inspect bridge
[
{
"Name": "bridge",
"Id": "f8c51787d478149b00975b9802ff372c373fd4bd4fd75696673182f5334044d9",
"Created": "2023-04-02T12:00:35.032172465+03:00",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.17.0.0/16",
"Gateway": "172.17.0.1"
}
Контейнер видит шлюз 172.17.0.1 - этот IP внутри сети docker ведет на host. Таким образом, чтобы контейнер использовал dns сервер systemd, настраиваем daemon.json так:
{
"dns":["172.17.0.1"]
}
Проверяем:
$ docker run --rm busybox nslookup dry-glade-1553.internal
nslookup: write to '172.17.0.1': Connection refused
;; connection timed out; no servers could be reached
Прописать в конфиге “dns”:[“172.17.0.1”] недостаточно, т.к. systemd DNS не слушает на 172.17.0.1:53:
$ sudo netstat -lepunt | grep systemd
[sudo] password for ac:
tcp 0 0 127.0.0.54:53 0.0.0.0:* LISTEN 978 21579 356/systemd-resolve
tcp 0 0 127.0.0.53:53 0.0.0.0:* LISTEN 978 21577 356/systemd-resolve
tcp 0 0 0.0.0.0:5355 0.0.0.0:* LISTEN 978 21568 356/systemd-resolve
tcp6 0 0 :::5355 :::* LISTEN 978 21571 356/systemd-resolve
udp 0 0 127.0.0.54:53 0.0.0.0:* 978 21578 356/systemd-resolve
udp 0 0 127.0.0.53:53 0.0.0.0:* 978 21576 356/systemd-resolve
udp 0 0 192.168.1.101:68 0.0.0.0:* 980 154894 287/systemd-network
udp 0 0 0.0.0.0:5353 0.0.0.0:* 978 21572 356/systemd-resolve
udp 0 0 0.0.0.0:5355 0.0.0.0:* 978 21567 356/systemd-resolve
udp6 0 0 fe80::daf3:bcff:fe4:546 :::* 980 24309 287/systemd-network
udp6 0 0 :::5353 :::* 978 21573 356/systemd-resolve
udp6 0 0 :::5355 :::* 978 21570 356/systemd-resolve
Решение подсмотрено здесь ».
Добавляем DNSStubListenerExtra=172.17.0.1 в /etc/systemd/resolved.conf
$ cat /etc/systemd/resolved.conf
[Resolve]
...
DNSStubListenerExtra=172.17.0.1
После перезапуска systemd-resolved, слушает на 172.17.0.1:53
$ sudo systemctl restart systemd-resolved
$ sudo netstat -lepunt | grep systemd
...
tcp 0 0 172.17.0.1:53 0.0.0.0:* LISTEN 978 166472 24384/
...
И теперь docker container резолвит домен сети fly через DNS systemd:
$ docker run --rm busybox nslookup dry-glade-1553.internal
Server: 172.17.0.1
Address: 172.17.0.1:53
Non-authoritative answer:
Name: dry-glade-1553.internal
Address: fdaa:1:ca13:a7b:2658:832a:15ea:2
Проверяем psql:
$ source secrets.txt && docker run -it --rm postgres psql postgres://postgres:${PASSWORD}@dry-glade-1553.internal:5432
psql: error: connection to server at "dry-glade-1553.internal" (fdaa:1:ca13:a7b:8dd7:0:a:102/120), port 5432 failed: Cannot assign requested address
Is the server running on that host and accepting TCP/IP connections?
Не работает, и причина в том, что в docker не включен ipv6, по которому мы пытаемся подключиться к postgres из контейнера:
$ docker run --rm -t busybox ping6 -c 4 google.com
PING google.com (2a00:1450:4017:805::200e): 56 data bytes
ping6: sendto: Cannot assign requested address
Настройка ipv6 в docker
https://docs.docker.com/config/daemon/ipv6/
Добавляем в /etc/docker/daemon:
{
"dns":["172.17.0.1"],
"ipv6": true,
"fixed-cidr-v6": "fd00::/80"
}
Рестарт docker: systemctl restart docker.service
Разрешаем контейнерам обращаться к внешним ip6 адресам (отсюда »):
ip6tables -t nat -A POSTROUTING -s fd00::/80 ! -o docker0 -j MASQUERADE
Docker app подключился
PASSWORD=****
docker run -it --rm postgres psql postgres://postgres:${PASSWORD}@dry-glade-1553.internal:5432
psql (15.2 (Debian 15.2-1.pgdg110+1))
Type "help" for help.
postgres=#
Таким образом клиент внутри docker контейнера подключился к postgres внутри сети fly.io.
Однако что если не хочется конфигурировать ipv6 в docker?…
Docker app + posrgres в сети fly.io (способ через nginx, простой)
С docker получилось, мягко скажем, сложновато.
Альтернативная идея в том, что в docker не настраиваем ничего (ни dns, ни ipv6), но проксируем запросы к postgres через nginx.
Nginx при этом должен быть запущен без докера на хосте.
Конфиг nginx:
stream {
upstream postgres {
server [fdaa:1:ca13:a7b:2658:832a:15ea:2]:5432;
}
server {
listen 5439 so_keepalive=on;
proxy_pass postgres;
}
}
nginx проксирует запросы на порт 5439 к postgres в сети fly.io по IP (он у приложения все равно статический, а по домену пока не получилось…)
контейнер запускаем так:
PASSWORD=***
docker run --add-host=host.docker.internal:host-gateway \
-it --rm postgres psql postgres://postgres:${PASSWORD}@host.docker.internal:5439
psql (15.2 (Debian 15.2-1.pgdg110+1))
Type "help" for help.
postgres=#
--add-host=host.docker.internal:host-gateway
служит для того, чтобы внутри контейнера в /etc/hosts появился домен, который ведет на хост. Тогда запрос к nginx пишется так: host.docker.internal:5439
И всё работает… Work smarter, not harder :D