Systemd-resolved + wireguard + docker + ipv6

Предыстория

Решил я как-то попробовать сервис fly.io, с целью расширения кругозора по инфраструктурным провайдерам (а вовсе не потому что fly.io предоставляет free allowances, включающий 3 виртуалки по 1 cpu и 256 mb ram каждая)

На такой виртуальной машине можно, например, поднять сервер postgres для пет-проекта (такого как chipa). Это делается по короткой инструкции. В результате получаем запущенный postgres на виртуальной машине внутри fly.io:

Postgres app in 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 с ноутбука.

Чтобы открыть сервис для внешнего мира, существует несколько способов:

  1. Добавить внешний IP https://fly.io/docs/postgres/connecting/connecting-external
  2. Подключиться к внутренней сети через туннель 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

Здесь:

Самое интересное, конечно, как systemd-resolved DNS перевел dry-glade-1553.internal в fdaa:1:ca13:a7b:2658:832a:15ea:2, но это долгая история », я лишь пробегусь кратко по своему конфигу.

Немного про systemd-resolved

Archlinux вики рекомендует использовать systemd-resolved, и я свято следую рекомендациям archlinux wiki Dbeaver

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 сервер согласно правилам: Dbeaver

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: Alyways DNS

Почему 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: Dbeaver

Но это полдела, в конечном счете 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 stub inside container

Нельзя просто взять, и написать в конфиге “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

Resources