Na EdgeRouterze 4 uruchomiłem HAProxy, które rozdziela ruch HTTP i HTTPS między dwa serwery z Traefikiem. Dzięki temu usługi na obu serwerach są dostępne z Internetu na standardowych portach — bez dopisywania :444 do adresu.
Certyfikaty i routing do aplikacji pozostały w Traefiku. Router odpowiada tylko za wybór serwera: dla HTTP na podstawie nagłówka Host, a dla HTTPS na podstawie SNI.
Poniżej opisuję powód tej zmiany, docelowy przepływ ruchu, kompilację HAProxy dla EdgeOS i konfigurację całego rozwiązania.
Domeny i adresacja zostały zmienione. Sieć lokalna w przykładach to
10.10.10.0/24. Nazwy polityk firewalla, interfejsy i numery reguł odpowiadają mojemu wdrożeniu — nie są uniwersalną konfiguracją do wklejenia na dowolnym routerze.
Problem: dwa serwery i różne adresy usług
Początkowo publiczne porty 80 i 443 były przekierowane na pierwszy serwer, 10.10.10.251. Drugi serwer, docker1 pod adresem 10.10.10.157, miał osobne przekierowanie z publicznego portu 444 na swój port 443.
Dostęp do usługi na drugim serwerze wyglądał więc tak:
https://usluga1.example.com:444/
W sieci lokalnej korzystam z widoków DNS. Ta sama nazwa rozwiązuje się bezpośrednio na prywatny adres właściwego serwera. Lokalnie używałem więc:
https://usluga1.example.com/
Usługa działała, ale jej adres zależał od miejsca połączenia:
- z Internetu: publiczny adres routera i port 444;
- z LAN: prywatny adres serwera i standardowy port 443.
Komplikowało to również SSO z Authelią. Callbacki i adresy powrotne musiały uwzględniać wariant z portem albo bez niego.
Chciałem, żeby użytkownik zawsze korzystał z jednego adresu:
https://usluga1.example.com/
Bez zmiany portu w przeglądarce i bez przenoszenia wszystkich usług na jeden serwer.
Docelowa architektura
HAProxy przyjmuje ruch z Internetu na routerze i wybiera serwer na podstawie nazwy domeny.
Internet / publiczny DNS
|
| publiczny adres routera, TCP/80 i TCP/443
v
EdgeRouter 4 / HAProxy
|
+-- wybrane domeny --> 10.10.10.157 --> Traefik --> usługi
|
+-- pozostałe ------> 10.10.10.251 --> Traefik --> usługi
LAN / lokalny widok DNS
|
+-- prywatny adres właściwego serwera --> Traefik --> usługi
Lokalny DNS nadal kieruje klientów bezpośrednio do serwerów. Standardowy ruch z LAN nie musi przechodzić przez HAProxy ani przez publiczny adres routera.
Z perspektywy użytkownika URL pozostaje jednak identyczny. Zmienia się odpowiedź DNS i ścieżka sieciowa, ale nie schemat, nazwa hosta ani port.
Podział odpowiedzialności
HAProxy i Traefik pełnią różne role:
| Element | Zadanie |
|---|---|
| HAProxy na EdgeRouterze | Wybór serwera docelowego według domeny |
| Traefik na serwerach | Obsługa certyfikatów, terminacja TLS i routing do aplikacji |
| Lokalny DNS | Bezpośrednie kierowanie klientów LAN do właściwego serwera |
Dla HTTP HAProxy odczytuje nagłówek Host.
Dla HTTPS odczytuje SNI z TLS ClientHello, a następnie przekazuje zaszyfrowany strumień do odpowiedniego Traefika. Router nie odszyfrowuje ruchu i nie przechowuje certyfikatów usług.
Między HAProxy a Traefikiem wykorzystuję dodatkowo PROXY protocol v2 do przekazywania informacji o połączeniu klienta.
Kompilacja HAProxy dla EdgeOS
Środowisko i architektura
Router działał na EdgeOS z następującym środowiskiem:
| Element | Wartość |
|---|---|
| Sprzęt | EdgeRouter 4 |
| Baza systemu | Debian 9 Stretch |
| Kernel | 4.9.79-UBNT |
| glibc | 2.24 |
| Architektura raportowana przez kernel | mips64 |
Architektura userspace według dpkg | mips |
Istotne było rozróżnienie architektury kernela i userspace. Samo mips64 w wyniku polecenia sprawdzającego kernel nie oznaczało, że potrzebuję 64-bitowej binarki.
HAProxy 3.4.0 zbudowałem poza routerem, na Ubuntu 20.04, przy użyciu mips-linux-gnu-gcc 9.4.0.
Wynikiem był statycznie linkowany ELF 32-bit big-endian, MIPS32 rel2. Na EdgeOS nie instalowałem kompilatora ani nie aktualizowałem bibliotek systemowych.
Wersja 3.4.0 jest wersją wykorzystaną w tym wdrożeniu, a nie zaleceniem pomijania późniejszych poprawek.
Przygotowanie środowiska budowania
Na komputerze kompilującym zainstalowałem narzędzia:
sudo apt-get update
sudo apt-get install --no-install-recommends \
make \
gcc-mips-linux-gnu \
binutils-mips-linux-gnu \
libc6-dev-mips-cross \
git
Miałem już klon repozytorium źródeł HAProxy w ~/haproxy. Z niego przygotowałem osobny katalog dla wybranego wydania:
cd ~/haproxy
git fetch --tags origin
git worktree add --detach ../haproxy-3.4.0 v3.4.0
cd ../haproxy-3.4.0
Jeżeli odtwarzasz ten proces od zera, najpierw przygotuj lokalny klon repozytorium źródeł. Powyższy fragment zaczyna się od istniejącego katalogu ~/haproxy.
Budowanie binarki
Końcowe polecenie kompilacji wyglądało następująco:
make SHELL='/bin/bash -o pipefail' clean
make -j2 \
SHELL='/bin/bash -o pipefail' \
TARGET=linux-glibc-legacy \
CPU=generic \
CC=mips-linux-gnu-gcc \
LD=mips-linux-gnu-gcc \
AR=mips-linux-gnu-ar \
NM=mips-linux-gnu-nm \
LDFLAGS='-static' \
ADDLIB='-lpthread' \
USE_THREAD=1 \
USE_OPENSSL= \
USE_LUA= \
USE_PCRE= \
USE_PCRE2= \
USE_ZLIB= \
USE_LIBCRYPT= \
USE_CRYPT_H= \
USE_DL= \
USE_BACKTRACE= \
USE_GETADDRINFO=
OpenSSL nie było potrzebne, ponieważ HAProxy nie kończy połączeń TLS. Router wybiera backend na podstawie ClientHello, a właściwy handshake i obsługa certyfikatu odbywają się w Traefiku.
Backendy wskazałem adresami IP. Statyczna glibc pozostawia ograniczenia związane z NSS, więc samo statyczne linkowanie nie oznacza pełnej niezależności każdej funkcji programu od bibliotek systemu docelowego.
Problemy podczas kompilacji
Do działającej binarki doprowadziły trzy istotne poprawki:
| Problem | Zmiana |
|---|---|
make: command: Command not found | Wymuszenie Basha przez SHELL='/bin/bash -o pipefail' |
Brak symboli pthread_* podczas statycznego linkowania | Dodanie ADDLIB='-lpthread' |
Odwołanie do thread_cpus_enabled_at_boot przy wyłączonych wątkach | Przebudowanie z USE_THREAD=1 |
Binarka ma obsługę wielu wątków, ale w konfiguracji uruchomieniowej ustawiłem nbthread 1. Zmiana tej wartości nie wymaga ponownej kompilacji.
Przeniesienie binarki na router
Wynikowy plik haproxy przeniosłem na router do katalogu domowego użytkownika.
Dalsze polecenia zakładają więc, że na EdgeRouterze istnieje plik:
$HOME/haproxy
Docelowo binarka trafi do /config/haproxy/bin/haproxy, razem z pozostałymi plikami tego wdrożenia.
Przygotowanie routera
Przeniesienie panelu administracyjnego
GUI EdgeOS zajmowało porty 80 i 443, na których miało nasłuchiwać HAProxy.
Panel przeniosłem na HTTP/8080 i HTTPS/8443:
configure
set service gui http-port 8080
set service gui https-port 8443
commit
save
exit
Po zmianie panel HTTPS jest dostępny pod adresem:
[https://10.10.10.254:8443/](https://10.10.10.254:8443/)
Portów administracyjnych nie dodałem do wyjątku WAN dla HAProxy.
Usunięcie starych przekierowań
Poprzedni układ opierał się na trzech regułach:
| Reguła | Poprzednia funkcja |
|---|---|
2 | HTTP/80 → 10.10.10.251:80 |
3 | HTTPS/443 → 10.10.10.251:443, reguła obejmowała TCP i UDP |
15 | HTTPS/444 → 10.10.10.157:443 |
Usunąłem je, ponieważ połączenia na portach 80 i 443 miał od tej pory przyjmować proces działający na samym routerze.
Dostęp dopuściłem w polityce local interfejsu WAN pppoe0. W mojej konfiguracji polityka ta nazywa się ibs-local.
configure
delete port-forward rule 2
delete port-forward rule 3
delete port-forward rule 15
set firewall name ibs-local rule 60 description 'HTTP HTTPS TCP to HAProxy'
set firewall name ibs-local rule 60 action accept
set firewall name ibs-local rule 60 protocol tcp
set firewall name ibs-local rule 60 destination port 80,443
set firewall name ibs-local rule 60 log disable
compare
commit
save
exit
Pozostałe przekierowania i reguły masquerade zostały bez zmian.
Usunięcie reguły 3 objęło również stare UDP/443. Nowa konfiguracja obsługuje TCP/80 i TCP/443, bez HTTP/3/QUIC.
Przeniesienie obsługi z DNAT do procesu na routerze zmienia miejsce filtrowania ruchu. W tym wdrożeniu potrzebny był dostęp w polityce
local, a nie tylko reguły dotyczące ruchu przekazywanego przez router.
Dostęp do HAProxy z LAN
W polityce home-local, przypiętej do interfejsu VLAN HOME, roboczą regułę zastąpiłem dostępem do TCP/80 i TCP/443:
configure
delete firewall name home-local rule 25
set firewall name home-local rule 25 description 'HTTP HTTPS to HAProxy'
set firewall name home-local rule 25 action accept
set firewall name home-local rule 25 protocol tcp
set firewall name home-local rule 25 destination port 80,443
set firewall name home-local rule 25 log disable
commit
save
exit
Ta reguła umożliwia dostęp do samego HAProxy z LAN. Nie zmienia jednak podstawowego przepływu ruchu: lokalne widoki DNS nadal wskazują prywatne adresy serwerów, więc standardowe połączenia do usług omijają routerowe proxy.
Numery 25 i 60 są właściwe dla mojego wdrożenia. Przed zastosowaniem podobnej konfiguracji trzeba dopasować nazwy polityk, kolejność i numery reguł do własnego firewalla.
Połączenie z Traefikiem
Przygotowanie odbioru PROXY protocol
Po dodaniu HAProxy przed Traefikiem chciałem zachować informację o rzeczywistym adresie klienta. Między routerem a Traefikiem wykorzystałem do tego PROXY protocol v2.
Nie jest to terminacja TLS. Informacja o połączeniu jest przekazywana do Traefika, ale handshake TLS i certyfikat nadal obsługuje serwer docelowy.
Najpierw przygotowałem odbiór po stronie Traefika. W jego statycznej konfiguracji TOML uzupełniłem entrypointy:
[entryPoints]
[entryPoints.web]
address = ":80"
[entryPoints.web.proxyProtocol]
trustedIPs = ["10.10.10.254/32"]
[entryPoints.websecure]
address = ":443"
[entryPoints.websecure.proxyProtocol]
trustedIPs = ["10.10.10.254/32"]
To fragment do połączenia z istniejącą konfiguracją, a nie zamiennik całego pliku Traefika.
Pozostały routing, przekierowanie HTTP na HTTPS oraz obsługa certyfikatów zostały zachowane.
Traefik ufa informacjom PROXY protocol wyłącznie od routera, czyli 10.10.10.254. Jednocześnie entrypointy przyjmują zwykłe połączenia bez tego nagłówka, co pozwala zachować bezpośredni dostęp klientów z lokalnej sieci.
Po zmianie konfiguracji statycznej zrestartowałem Traefika. Dopiero później włączyłem wysyłanie PROXY protocol w HAProxy.
Konfiguracja HAProxy
Plik /config/haproxy.cfg zawiera osobne frontendy dla HTTP i HTTPS.
Wybrane domeny kieruję na 10.10.10.157. Pozostały ruch trafia do domyślnego backendu na 10.10.10.251.
global
nbthread 1
maxconn 256
log stdout format raw local0
defaults
log global
timeout connect 5s
timeout client 60s
timeout server 60s
frontend https_in
bind :443
mode tcp
option tcplog
tcp-request inspect-delay 5s
tcp-request content accept if { req.ssl_hello_type 1 }
acl do_157 req.ssl_sni -i usluga1.example.com
acl do_157 req.ssl_sni -i usluga2.example.com
acl do_157 req.ssl_sni -i usluga3.example.com
acl do_157 req.ssl_sni -i usluga4.example.com
acl do_157 req.ssl_sni -i usluga5.example.com
acl do_157 req.ssl_sni -i usluga6.example.com
use_backend https_157 if do_157
default_backend https_251
backend https_157
mode tcp
server srv157 10.10.10.157:443 check send-proxy-v2
backend https_251
mode tcp
server srv251 10.10.10.251:443 check send-proxy-v2
frontend http_in
bind :80
mode http
option httplog
acl do_157 hdr(host),lower,field(1,:) -m str usluga1.example.com
acl do_157 hdr(host),lower,field(1,:) -m str usluga2.example.com
acl do_157 hdr(host),lower,field(1,:) -m str usluga3.example.com
acl do_157 hdr(host),lower,field(1,:) -m str usluga4.example.com
acl do_157 hdr(host),lower,field(1,:) -m str usluga5.example.com
acl do_157 hdr(host),lower,field(1,:) -m str usluga6.example.com
use_backend http_157 if do_157
default_backend http_251
backend http_157
mode http
server srv157 10.10.10.157:80 check send-proxy-v2
backend http_251
mode http
server srv251 10.10.10.251:80 check send-proxy-v2
Kilka szczegółów tej konfiguracji jest istotnych:
send-proxy-v2włącza przekazywanie informacji o kliencie do Traefika. Odbiorca musi być wcześniej przygotowany do jego obsługi.- Na publicznych frontendach nie dodawałem
accept-proxy. default_backendobejmuje także połączenia, które nie pasują do listy domen.- HAProxy nie wymusza przekierowania HTTP na HTTPS. Pozostawiłem to Traefikowi.
bind :80ibind :443nie przypinają usługi wyłącznie do WAN. Dostęp do listenerów na lokalnych adresach IPv4 kontroluje firewall.- Obsługa IPv6 nie jest częścią tej konfiguracji.
Uruchamianie jako usługa
Rozmieszczenie plików
Binarkę, konfigurację i definicję usługi umieściłem w /config:
/config/haproxy/bin/haproxy
/config/haproxy.cfg
/config/haproxy/haproxy-edge.service
Skopiowaną wcześniej do katalogu domowego binarkę zainstalowałem następująco:
sudo mkdir -p /config/haproxy/bin
sudo install -o root -g root -m 0755 \
"$HOME/haproxy" \
/config/haproxy/bin/haproxy
sudo chown root:root /config/haproxy.cfg
sudo chmod 0644 /config/haproxy.cfg
Polecenia zakładają, że konfiguracja HAProxy jest już zapisana w /config/haproxy.cfg.
Definicja systemd
Plik /config/haproxy/haproxy-edge.service:
[Unit]
Description=HAProxy on EdgeRouter
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
ExecStartPre=/config/haproxy/bin/haproxy -c -f /config/haproxy.cfg
ExecStart=/config/haproxy/bin/haproxy -W -db -f /config/haproxy.cfg
ExecReload=/config/haproxy/bin/haproxy -c -f /config/haproxy.cfg
ExecReload=/bin/kill -USR2 $MAINPID
Restart=on-failure
RestartSec=5s
KillMode=mixed
KillSignal=SIGUSR1
TimeoutStopSec=60s
StandardOutput=journal
StandardError=journal
SyslogIdentifier=haproxy-edge
[Install]
WantedBy=multi-user.target
HAProxy działa w trybie master-worker, na pierwszym planie pod kontrolą systemd.
Przed startem i przeładowaniem wykonywane jest sprawdzenie konfiguracji. Przeładowanie odbywa się przez sygnał USR2 do procesu master, a nieoczekiwane zakończenie procesu powoduje ponowienie startu.
Po zatrzymaniu instancji uruchamianej wcześniej ręcznie zarejestrowałem usługę:
sudo ln -sfn \
/config/haproxy/haproxy-edge.service \
/etc/systemd/system/haproxy-edge.service
sudo systemctl daemon-reload
sudo systemctl enable haproxy-edge.service
sudo systemctl start haproxy-edge.service
Odtwarzanie rejestracji po zmianie firmware
Pliki wdrożenia pozostają w /config, ale rejestracja usługi znajduje się również w /etc.
Do jej odtworzenia po zmianie obrazu firmware przygotowałem skrypt:
/config/scripts/post-config.d/90-haproxy-edge
Zawartość:
#!/bin/sh
set -eu
ln -sfn \
/config/haproxy/haproxy-edge.service \
/etc/systemd/system/haproxy-edge.service
systemctl daemon-reload
systemctl enable haproxy-edge.service
systemctl --no-block start haproxy-edge.service
Nadałem mu uprawnienia do wykonania:
sudo chmod 0755 \
/config/scripts/post-config.d/90-haproxy-edge
Skrypt odtwarza dowiązanie do definicji usługi, przeładowuje konfigurację systemd i uruchamia HAProxy.
Nie zmienia to faktu, że własna binarka i hook startowy są rozszerzeniem utrzymywanym samodzielnie, a nie wbudowaną funkcją EdgeOS.
Bieżąca obsługa
Przeładowanie konfiguracji:
sudo systemctl reload haproxy-edge
Stan usługi:
sudo systemctl status haproxy-edge --no-pager -l
Podgląd logów:
sudo journalctl -u haproxy-edge -f
Restart:
sudo systemctl restart haproxy-edge
Efekt i ograniczenia
Po zmianie oba serwery korzystają z jednego publicznego wejścia i standardowych portów.
Wybrane domeny trafiają na 10.10.10.157, a reszta na dotychczasowy serwer 10.10.10.251. Z adresów usług zniknął port :444.
Lokalne widoki DNS nadal zapewniają bezpośredni dostęp po prywatnych adresach. URL usługi jest jednak identyczny w LAN i w Internecie, co uprościło również adresy powrotne SSO.
Certyfikaty oraz routing do aplikacji pozostały w Traefiku. EdgeRouter dostał warstwę wyboru serwera bez dokładania kolejnej maszyny na wejściu do sieci.
Rozwiązanie ma jednak jasno określone granice:
- obsługuje TCP/80 i TCP/443, bez HTTP/3/QUIC;
- nie obejmuje konfiguracji IPv6;
- nie kończy TLS na routerze;
- wymaga zgodnej konfiguracji PROXY protocol po stronie HAProxy i Traefika;
- opiera się na własnej binarce i samodzielnie utrzymywanej integracji z EdgeOS;
- przetwarza strumień na CPU routera — nie jest sprzętowo offloadowanym DNAT.
W moim przypadku najważniejszy efekt to jeden spójny adres każdej usługi, niezależnie od miejsca połączenia, przy zachowaniu dwóch osobnych serwerów i dotychczasowej obsługi aplikacji w Traefiku.