nginx: stream modul — TCP i UDP proxy

Ciljevi lekcije

Nakon ove lekcije trebalo bi da:

  • razumeš šta stream modul radi i po čemu se razlikuje od http bloka
  • znaš gde konfiguracija fizički ide na Ubuntu-u, jer to nije očigledno
  • postaviš proxy ispred baze podataka ili bilo kog TCP servisa
  • usmeravaš HTTPS saobraćaj po SNI imenu, bez dešifrovanja
  • proslediš pravu IP adresu klijenta kroz PROXY protokol
  • podesiš logovanje, koje u stream bloku podrazumevano ne postoji

1. Šta je stream

Sve dosad u kursu odnosilo se na http blok — nginx koji razume HTTP, čita zaglavlja, bira location, kešira odgovore.

stream radi na četvrtom sloju. Prosleđuje bajtove sa jedne strane na drugu, bez ikakvog razumevanja sadržaja. Nema URI-ja, nema zaglavlja, nema location blokova, nema statusnih kodova.

Zauzvrat radi sa bilo čim što ide preko TCP-a ili UDP-a: PostgreSQL, MySQL, Redis, SMTP, IMAP, DNS, MQTT, SSH, serveri za igre, sopstveni binarni protokoli.

Kada koristiti šta:

Situacija Modul
Web saobraćaj, API, bilo šta HTTP http
Baza podataka, mail, DNS, MQTT stream
HTTPS koji treba proslediti bez dešifrovanja stream uz ssl_preread
Potrebno keširanje, kompresija, rutiranje po putanji http

2. Provera i instalacija modula

nginx -V 2>&1 | tr ' ' '\n' | grep -i stream
ls /etc/nginx/modules-enabled/

Ako with-stream nema, na Ubuntu-u instaliraj:

sudo apt install libnginx-mod-stream

Paket dodaje fajl u /etc/nginx/modules-enabled/ koji se učitava kroz include /etc/nginx/modules-enabled/*.conf; na vrhu nginx.conf. Nakon instalacije:

sudo nginx -t && sudo systemctl restart nginx

3. Gde konfiguracija ide — ovo je zamka

stream blok stoji na najvišem nivou, kao brat http bloka:

# nginx.conf
user www-data;
worker_processes auto;

events { ... }

http { ... }

stream { ... }        # ← ovde, NE unutar http

Sada dolazi praktičan problem specifičan za Ubuntu paket. Podsetnik iz lekcije 3: include /etc/nginx/sites-enabled/*; nalazi se unutar http bloka. To znači da ne možeš staviti stream konfiguraciju u sites-available — završila bi ugnježdena u http i dobio bi:

nginx: [emerg] "stream" directive is not allowed here

Ovo je jedna od najčešćih grešaka pri prvom radu sa stream-om, jer se čini prirodnim da sajtovi i stream idu na isto mesto.

Uredno rešenje

Napravi paralelnu strukturu:

sudo mkdir -p /etc/nginx/streams-available /etc/nginx/streams-enabled

U nginx.conf, van http bloka, dodaj na kraj:

stream {
    include /etc/nginx/streams-enabled/*;
}

Sada radiš isto kao sa sajtovima:

sudo ln -s /etc/nginx/streams-available/baza /etc/nginx/streams-enabled/

Fajlovi u streams-enabled sadrže server i upstream blokove, bez omotača stream { } — isto kao što fajlovi u sites-enabled ne sadrže http { }.

4. Osnovni TCP proxy

/etc/nginx/streams-available/postgres:

upstream postgres_backend {
    server 10.0.1.20:5432 max_fails=2 fail_timeout=30s;
    server 10.0.1.21:5432 backup;
}

server {
    listen 5432;

    proxy_pass postgres_backend;

    proxy_connect_timeout 3s;
    proxy_timeout 1h;

    proxy_socket_keepalive on;
}

Razlike u odnosu na http blok koje treba primetiti:

proxy_pass ovde nema shemu. Piše se proxy_pass ime_grupe; ili proxy_pass 10.0.1.20:5432;, bez http://. I nema pravila o kosoj crti — nema URI-ja o kome bi se radilo.

proxy_timeout je jedna direktiva, ne razdvojena na send i read. Odnosi se na period neaktivnosti u oba smera. Podrazumevanih 10 minuta je često prekratko za dugotrajne konekcije ka bazi — otud 1h.

proxy_socket_keepalive on uključuje TCP keep-alive na nivou socket-a, čime se otkrivaju mrtve konekcije kroz posrednike koji tiho prekidaju neaktivne veze.

Sigurnosna napomena

Izlaganje baze podataka kroz proxy zahteva pažnju. Ako je listen 5432; bez ograničenja, baza je dostupna sa celog interneta.

server {
    listen 10.0.0.5:5432;      # samo na internom interfejsu

    allow 10.0.0.0/8;
    deny all;

    proxy_pass postgres_backend;
}

allow/deny rade i u stream bloku, sa istim pravilom „prvo poklapanje pobeđuje" iz lekcije 14.

Najbolje je da baza ne bude dostupna spolja uopšte. stream proxy ima smisla za raspodelu opterećenja unutar mreže, ne kao način da se baza objavi na internetu.

5. Raspodela opterećenja

Isti metodi kao u http, uz jednu razliku:

upstream redis_backend {
    least_conn;
    server 10.0.1.30:6379 weight=2;
    server 10.0.1.31:6379;
    server 10.0.1.32:6379;
}

Dostupno je: round-robin (podrazumevano), least_conn, hash, random.

ip_hash ne postoji u stream bloku. Za vezivanje klijenta za isti server koristi:

upstream backend {
    hash $remote_addr consistent;
    server 10.0.1.30:6379;
    server 10.0.1.31:6379;
}

Parametri max_fails, fail_timeout, backup, down, max_conns i zone rade isto kao u lekciji 8.

6. UDP proxy

server {
    listen 53 udp;

    proxy_pass dns_backend;

    proxy_responses 1;
    proxy_timeout 5s;
}

upstream dns_backend {
    server 10.0.1.40:53;
    server 10.0.1.41:53;
}

proxy_responses 1 kaže nginx-u koliko paketa odgovora očekuje pre nego što zatvori sesiju. Za DNS je to jedan. Za protokole gde odgovor stiže u više paketa postavi odgovarajući broj, ili proxy_responses 0 ako se odgovor ne očekuje uopšte (kao kod syslog-a).

Ako je vrednost pogrešna, sesija ostaje otvorena do isteka proxy_timeout i troši resurse.

Za DNS možeš na istom portu podržati i TCP:

server {
    listen 53 udp;
    listen 53;              # TCP, za odgovore koji ne staju u UDP paket
    proxy_pass dns_backend;
    proxy_responses 1;
}

Ne zaboravi firewall — UDP se otvara zasebno:

sudo ufw allow 53/udp
sudo ufw allow 53/tcp

7. TLS terminacija u stream bloku

nginx može da preuzme TLS i ka backend-u govori običan protokol:

server {
    listen 6379 ssl;

    ssl_certificate     /etc/letsencrypt/live/redis.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/redis.example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    proxy_pass 127.0.0.1:6379;      # backend bez TLS-a, samo lokalno
}

Ovo je koristan obrazac: servis koji ne podržava TLS dobija ga spolja, a interno komunicira nešifrovano preko petlje.

Suprotan smer — nginx se ka backend-u povezuje preko TLS-a:

server {
    listen 5432;
    proxy_pass bezbedni_backend;
    proxy_ssl on;
    proxy_ssl_verify on;
    proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
}

8. SNI rutiranje sa ssl_preread

Ovo je najzanimljivija mogućnost stream modula.

Situacija: imaš jednu javnu IP adresu i port 443, a iza sebe više servera koji svaki drži svoje sertifikate. Ne želiš da nginx dešifruje saobraćaj — treba samo da ga prosledi na pravo mesto.

ssl_preread čita SNI ime iz TLS ClientHello poruke, koja ide u čistom obliku pre nego što šifrovanje počne, i na osnovu njega bira odredište. Sam saobraćaj ostaje šifrovan od kraja do kraja.

map $ssl_preread_server_name $https_backend {
    prvi.example.com    10.0.1.10:443;
    drugi.example.com   10.0.1.11:443;
    ~^.+\.app\.rs$      10.0.1.12:443;
    default             10.0.1.10:443;
}

server {
    listen 443;
    listen [::]:443;

    ssl_preread on;
    proxy_pass $https_backend;

    proxy_connect_timeout 3s;
    proxy_timeout 10m;
}

Kada je ovo pravi alat:

  • backend serveri moraju sami da drže sertifikate (recimo, klijenti donose svoje)
  • postoji zahtev da saobraćaj nigde ne bude dešifrovan
  • prosleđuješ protokole nad TLS-om koji nisu HTTP

Šta gubiš: keširanje, kompresiju, rutiranje po putanji, sigurnosna zaglavlja, rate limiting po zahtevu — sve što je vezano za razumevanje HTTP-a. Ako ti bilo šta od toga treba, koristi običan http reverse proxy.

Isti mehanizam radi i za razdvajanje protokola na jednom portu — recimo HTTPS i SSH preko 443:

map $ssl_preread_protocol $odrediste {
    ""        127.0.0.1:22;      # nije TLS → SSH
    default   127.0.0.1:8443;    # TLS → HTTPS
}

server {
    listen 443;
    ssl_preread on;
    proxy_pass $odrediste;
}

9. PROXY protokol — čuvanje IP adrese klijenta

Kod TCP proxy-ja backend vidi IP adresu nginx-a, ne klijenta. Nema X-Forwarded-For zaglavlja jer nema zaglavlja uopšte.

PROXY protokol rešava to tako što nginx pre samih podataka pošalje kratko zaglavlje sa originalnom adresom.

nginx strana (šalje):

server {
    listen 5432;
    proxy_pass postgres_backend;
    proxy_protocol on;
}

Backend mora da razume protokol. PostgreSQL, HAProxy, Postfix i mnogi drugi to podržavaju uz odgovarajuće podešavanje. Ako backend ne razume, videće PROXY zaglavlje kao smeće na početku konekcije i prekinuće je.

Ako je backend takođe nginx:

server {
    listen 80 proxy_protocol;

    set_real_ip_from 10.0.0.0/8;
    real_ip_header proxy_protocol;

    # $remote_addr sada sadrži pravu adresu klijenta
}

Za logovanje su dostupne i namenske promenljive: $proxy_protocol_addr i $proxy_protocol_port.

Upozorenje: port koji sluša sa proxy_protocol prihvata isključivo konekcije sa tim zaglavljem. Običan curl ka njemu neće raditi. To je čest uzrok zbunjenosti pri testiranju.

10. Logovanje

U stream bloku access log podrazumevano ne postoji. Moraš ga definisati sam, i za to nema gotovog formata.

stream {
    log_format tok '$remote_addr [$time_local] '
                   '$protocol $status $bytes_sent $bytes_received '
                   '$session_time upstream=$upstream_addr';

    access_log /var/log/nginx/stream.log tok;

    include /etc/nginx/streams-enabled/*;
}

Promenljive dostupne u stream kontekstu:

Promenljiva Sadržaj
$protocol TCP ili UDP
$status ishod sesije (vidi tabelu ispod)
$bytes_sent bajtova poslatih klijentu
$bytes_received bajtova primljenih od klijenta
$session_time trajanje sesije u sekundama
$upstream_addr koji backend je opslužio
$upstream_connect_time vreme uspostavljanja veze ka backend-u
$ssl_preread_server_name SNI ime, ako je ssl_preread uključen

Statusni kodovi u stream bloku nisu HTTP kodovi:

Kod Značenje
200 sesija uspešno završena
400 klijent nije poslao valjane podatke (npr. neuspeo ssl_preread)
403 pristup zabranjen (allow/deny)
500 interna greška
502 nije moguće povezati se sa backend-om
503 ograničenje broja konekcija

Error log radi normalno, kroz globalnu error_log direktivu.

11. Ograničavanje konekcija

stream {
    limit_conn_zone $binary_remote_addr zone=po_ip:10m;

    server {
        listen 5432;
        limit_conn po_ip 5;
        proxy_pass postgres_backend;
    }
}

Rate limiting po zahtevu (limit_req) ne postoji u stream bloku — nema pojma zahteva, samo konekcije. Dostupan je limit_conn.

12. Dijagnostika

Bez statusnih kodova i zaglavlja, dijagnostika izgleda drugačije.

Da li nginx uopšte sluša:

sudo ss -tlnp | grep 5432
sudo ss -ulnp | grep 53        # za UDP

Da li backend odgovara direktno:

nc -zv 10.0.1.20 5432
timeout 2 bash -c 'cat < /dev/tcp/10.0.1.20/5432' && echo "otvoreno"

Da li proxy prosleđuje:

nc -zv localhost 5432

Šta piše u stream logu:

sudo tail -20 /var/log/nginx/stream.log

$status 502 znači da backend nije dostupan. $status 403 da te je oborilo deny pravilo. $bytes_received 0 da klijent nije poslao ništa — često znak da nešto prekida konekciju pre nego što počne.

Za SNI rutiranje, proveri šta ssl_preread vidi:

openssl s_client -connect localhost:443 -servername prvi.example.com < /dev/null
sudo tail -1 /var/log/nginx/stream.log

Dodaj $ssl_preread_server_name u log format da bi video da li se ime uopšte pročitalo.

Najčešće greške

Simptom Uzrok
"stream" directive is not allowed here stream blok je unutar http — odeljak 3
unknown directive "stream" modul nije instaliran ili učitan
Konekcija se odmah prekida backend ne razume PROXY protokol, a proxy_protocol on je uključen
curl ka portu ne radi port sluša sa proxy_protocol i traži zaglavlje
UDP sesije se gomilaju pogrešan proxy_responses
Nema nijednog zapisa u logu access_log nije definisan u stream bloku

Praktična vežba

1. Proveri i instaliraj modul:

nginx -V 2>&1 | tr ' ' '\n' | grep -i stream || sudo apt install libnginx-mod-stream
sudo nginx -t && sudo systemctl restart nginx

2. Napravi grešku namerno. Stavi stream { } blok u fajl u sites-available, aktiviraj i pokreni nginx -t. Pročitaj poruku — to je greška koju ćeš prepoznati kasnije.

3. Postavi paralelnu strukturu (streams-available / streams-enabled) i include u nginx.conf van http bloka, kao u odeljku 3.

4. Napravi jednostavan TCP backend i proxy ispred njega:

# u jednom terminalu — backend koji ispisuje šta primi
nc -l -k -p 9001
# /etc/nginx/streams-available/test
server {
    listen 9000;
    proxy_pass 127.0.0.1:9001;
}
sudo ln -s /etc/nginx/streams-available/test /etc/nginx/streams-enabled/
sudo nginx -t && sudo systemctl reload nginx

# u drugom terminalu
echo "zdravo kroz proxy" | nc localhost 9000

Poruka mora da se pojavi u prvom terminalu.

5. Podesi logovanje po uzoru iz odeljka 10, pošalji nekoliko konekcija i pogledaj zapise:

sudo tail -5 /var/log/nginx/stream.log

Obrati pažnju na $session_time i $bytes_sent.

6. Izazovi 502. Ugasi nc backend, pokušaj konekciju i pogledaj log:

echo "test" | nc localhost 9000
sudo tail -1 /var/log/nginx/stream.log

7. Postavi raspodelu na tri backend-a:

nc -l -k -p 9001 & nc -l -k -p 9002 & nc -l -k -p 9003 &
upstream test_tcp {
    server 127.0.0.1:9001;
    server 127.0.0.1:9002;
    server 127.0.0.1:9003;
}

Pošalji šest konekcija i posmatraj $upstream_addr u logu — mora da se smenjuje.

8. Isprobaj SNI rutiranje. Napravi dva HTTPS server bloka na lokalnim portovima 8443 i 8444 (sa samopotpisanim sertifikatima iz lekcije 9), pa ispred njih stavi stream server sa ssl_preread:

map $ssl_preread_server_name $odrediste {
    prvi.local   127.0.0.1:8443;
    drugi.local  127.0.0.1:8444;
    default      127.0.0.1:8443;
}

server {
    listen 9443;
    ssl_preread on;
    proxy_pass $odrediste;
}
curl -k --resolve prvi.local:9443:127.0.0.1 https://prvi.local:9443/
curl -k --resolve drugi.local:9443:127.0.0.1 https://drugi.local:9443/

Dodaj $ssl_preread_server_name u log format i proveri da se ime ispravno čita.

9. Isprobaj allow/deny:

server {
    listen 9000;
    deny 127.0.0.1;
    allow all;
    proxy_pass 127.0.0.1:9001;
}
echo "test" | nc localhost 9000
sudo tail -1 /var/log/nginx/stream.log      # status 403

10. Demonstriraj zamku sa PROXY protokolom. Dodaj proxy_protocol on; ka nc backend-u i pogledaj šta backend primi — videćeš PROXY zaglavlje kao tekst pre tvoje poruke. To je tačno ono što backend koji ne razume protokol dobija.


Rezime

  • stream radi na četvrtom sloju: prosleđuje bajtove, bez razumevanja protokola.
  • Blok ide na najviši nivo, kao brat http bloka. Na Ubuntu-u to znači da ne sme u sites-available — napravi zasebnu streams-enabled strukturu.
  • proxy_pass u stream bloku nema shemu i nema pravila o kosoj crti.
  • proxy_timeout je jedna direktiva za oba smera; podrazumevanih 10 minuta je često prekratko.
  • ip_hash ne postoji — koristi hash $remote_addr consistent.
  • Kod UDP-a proxy_responses mora odgovarati protokolu, inače sesije vise do isteka tajmauta.
  • ssl_preread omogućava rutiranje HTTPS saobraćaja po SNI imenu bez dešifrovanja.
  • PROXY protokol prenosi pravu IP adresu klijenta, ali backend mora da ga razume — inače prekida konekciju.
  • Port koji sluša sa proxy_protocol ne prihvata obične konekcije; curl ka njemu neće raditi.
  • Access log u stream bloku ne postoji dok ga sam ne definišeš, i statusni kodovi nisu HTTP kodovi.
  • limit_req ne postoji u stream-u; dostupan je samo limit_conn.
  • Izlaganje baze podataka kroz proxy zahteva allow/deny i vezivanje za interni interfejs.

Pitanja za proveru

  1. Zašto stream konfiguracija ne sme da ide u sites-available na Ubuntu-u?
  2. U čemu se proxy_pass u stream bloku razlikuje od onog u http bloku?
  3. Kako vežeš klijenta za isti backend server kada ip_hash nije dostupan?
  4. Šta radi proxy_responses 1 i šta se dešava ako je vrednost pogrešna?
  5. Kada bi koristio ssl_preread umesto običnog HTTP reverse proxy-ja, i šta time gubiš?
  6. Uključio si proxy_protocol on, a backend odmah prekida svaku konekciju. Zašto?
  7. stream.log je prazan iako saobraćaj prolazi. Šta si propustio?
  8. $status u stream logu je 502. Šta to znači i gde tražiš uzrok?

Sledeća lekcija (ako se kurs nastavi): sistematska dijagnostika — alati, metodologija i tabela simptom → uzrok kroz sve dosad obrađene teme.

Comments

Popular posts from this blog

Početak u Linuxu — šta je i zašto se uči

Konverzija tipova podataka u Pythonu

groupadd