Brandvägg med OpenBSD

Från Unix.se, den fria unixresursen.

Den här artikeln behandlar pf och ALTQ i OpenBSD 3.3, två verktyg som tillsammans slår det mesta på fingrarna vad gäller brandväggar. pf filtrerar och ALTQ manipulerar din nätverkstrafik (d v s begränsning och prioritering). Artikeln kan även vara till nytta för FreeBSD-användare.

I och med OpenBSD 3.3 har en hel del hänt på pf-fronten:

  • ALTQ har integrerats - Filtrera och hantera köer i ett svep.
  • Enklare och friare regelskrivning.
  • Tables - Enklare och snabbare hantering av en större mängd adresser.
  • Load balancing - Lastbalansering mellan flera anslutningar.
  • Småfixar och mindre tillägg.

Denna artikel är uppdelad i två delar; del 1 innefattar pf, del 2 ALTQ.

Innehåll

pf

Inledning

pf, "packet filter", är paketfiltreraren som följer med OpenBSD sedan version 3.0. pf:s huvuduppgift är att bestämma om trafik till och från din dator ska få passera eller inte.

I tidigare utgåvor av OpenBSD har ipf ("ip filter") använts, men eftersom skaparen av ipf, Darren Reed, inte ville släppa källkoden under en tillräckligt fri licens ville inte Theo De Raadt (grundaren av OpenBSD) veta av ipf längre. Istället bad han OpenBSD-utvecklarna att koda en egen paketfiltrerare. pf är alltså relativt ungt, men trots detta väldigt moget för sin ålder.

För att aktivera pf, använd verktyget pfctl med flaggan -e, enable:

# pfctl -e

För att pf automatiskt ska aktiveras vid boot, redigera /etc/rc.conf:

pf=YES

För att vår brandvägg även ska kunna agera router måste vi aktivera 'ip forwarding':

# sysctl -w net.inet.ip.forwarding=1

För att 'ip forwarding' automatiskt ska aktiveras vid boot, ta bort kommenteringen framför följande rad i /etc/sysctl.conf:

net.inet.ip.forwarding=1

Visa statistik med pfctl

För att visa aktuell statistik:

# pfctl -s nat                  Statistik för NAT.
# pfctl -s rules                Statistik för pf.
# pfctl -s state                Statistik för state-tabellen.
# pfctl -s info                 Statistik för paket och bitar.
# pfctl -s tables               Statistik för tabeller.
# pfctl -s memory               Statistik för minnesbegränsningen.
# pfctl -s all                  Statistik för allt.

Strukturen för pf.conf

Trots pf:s simplicitet vad gäller syntax är det aldrig fel att hålla ordning på sina regler. Följande struktur i pf.conf rekommenderas:

  • Macros - alias för nätverkskort etc.
  • Tables - (dynamiska) tabeller över ipadresser och nät.
  • Options - Diverse optimeringar.
  • Packet normalization - Stoppa trasig trafik.
  • Queueing - Ramverket för dina köer.
  • Translation, bl a NAT- och rdr-regler.
  • Filtering, dvs själva filtreringsreglerna. Denna del har även till uppgift att matcha trafik till tidigare skapade köer.

Vill man av någon anledning inte följa denna ordning kan man lägga till följande rad under punkt 3, Options:

set require-order no

Filtrering

Nu är det dags, öppna konfigurationsfilen för pf, /etc/pf.conf, med din favoriteditor. Själv använder jag vim:

# vim /etc/pf.conf

Den absolut enklaste brandväggen är den som släpper igenom all trafik både in och ut. Vi provar:

pass in    from any to any
pass out from any to any

För att göra det hela ännu enklare:

pass in all
pass out all

Eller varför inte:

pass

Dessa tre exempel filtrerar på exakt samma sätt, nämligen inte alls. För att nu göra de nya reglerna gällande:

# pfctl -f /etc/pf.conf

Denna brandvägg är totalt meningslös, eftersom den släpper igenom all trafik. Dags att börja filtrera! För att förenkla förfarandet börjar vi med att skapa alias för våra nätverkskort. Hos mig ser det ut på följande vis:

lo = lo0
int = rl0
ext = ne3
dmz = xl0

Där lo är loopback, int är vårt interna, ext vårt externa och dmz nätverkskortet anslutet till vår DMZ. (Läs mer i 1.2.5 Regler för DMZ)

pf fungerar på följande vis; Låt oss säga att någon utifrån gör en anslutningsförfrågan mot din port 80, httpd. pf kontrollerar source (källa) och destination, protokoll och om paketen har någon förbindelse med tidigare trafik som passerat, dvs kontrollerar state. Trafiken matchas mot reglerna i /etc/pf.conf, i detta fallet 'pass in all', och trafiken passerar. Om vi lägger till en rad i /etc/pf.conf:

pass  in  all
pass  out all
block in on $ext proto tcp from any to any port 80

Då hade alltså trafiken blockerats. Notera att 'on $ext' endast blockerar trafik som kommer via vårt externa nätverkskort, alltså kan vår httpd fortfarande nås lokalt.

Då kanske du undrar varför den sista regeln gäller och inte den första. pf läser första raden, accepterar all inkommande trafik tills vidare, läser andra raden och accepterar all utgående trafik tills vidare. När tredje raden läses bryr den sig inte längre om att första raden accepterade all inkommande trafik. Den sista matchande regeln är alltså den som bestämmer.

Du kan dock få pf att inte läsa vidare efter matchande regler. Låt oss säga att vi garanterat vill blockera inkommande tcp-trafik mot port 80, och därmed få pf att sluta leta matchande rader efter denna:

block in quick on $ext proto tcp from any to any port 80

Man använder alltså 'quick', vilket man bör använda så mycket som möjligt, då tillfälle ges. Detta eftersom pf då slipper leta efter andra matchande regler, och vi får ett regelverk som både agerar snabbare och belastar mindre.

Portintervall

Följande exempel behandlar portintervallet 100-200:

pass in quick on $ext proto tcp from any to any port { 99 >< 201 }

Med " { 99 >< 201 } " menas "större än 99 och mindre än 201".

keep state och tillhörande flaggor

pf är en s.k. 'stateful' paketfiltrerare. Detta innebär att pf minns trafik som är besläktad med trafik som tidigare passerat. Detta underlättar konfigureringen avsevärt. Med dessa 2 rader blockerar man inkommande trafik, och accepterar all trafik utåt, och trafik inåt med förbindelse till trafik skapad inifrån:

block in  all
pass  out all keep state

Vill vi nu acceptera inkommande trafik som inte är i förbindelse, utan acceptera ett s.k. SYN (Syncronizing/Start) paket, och även ACK (Acknowledge), dvs handskakning lägger vi till följande rad:

block in   all
pass  in on $ext proto tcp from any to any port 22 flags S/SA keep state
pass  out  all keep state

Möjligheterna med flags är stora, för vidare läsning hänvisar jag till pf.conf(5).

modulate state

En säkrare form av 'keep state' är 'modulate state'. Den sistnämnda förhindrar "kapningar" av tcp-anslutningar, genom att anslutningen får ett bättre slumpat sekvensnummer. Regler med modulate state bör alltid kombineras med 'flags S/SA', detta eftersom problem annars kan uppstå t ex vid omstarter, när anslutningar försöker återsynkroniseras. 'modulate state' fungerar alltså enbart på tcp-trafik, till skillnad från 'keep state' Exempel:

pass  in on $ext proto tcp from any to any port 22 flags S/SA modulate state

Blockera förfalskad(spoofad) trafik

För att försäkra oss om att trafik med källa (eng. source) från våra interna ipadresser inte smiter in på något nätverkskort som inte är vårt interna kan vi skriva följande:

block drop in on ! $int from ($int)/24 to any
block drop in from ($int) to any

Men det är jobbigt, istället skriver vi:

antispoof for $int

Regler för DMZ

DMZ, som är en förkortning av "DeMilitarized Zone", är en säkrad zon i ditt nätverk. I denna zon placeras dina servrar, samtidigt som din brandvägg skiljer ditt övriga nätverk från din DMZ. Om nu dina servrar skulle bli attackerade och övertagna av en s.k. cracker eller script-kiddie, är de fast i din DMZ. För att förbättra säkerheten ytterligare kan man använda sig av flera DMZ:er.

För en DMZ krävs ett extra nätverkskort i din brandvägg. Därefter sätter vi upp väldigt strikta regler för din DMZ:s utgående trafik. En lösning är att blockera all utgående trafik sett från DMZ:ens ögon, och därefter släppa in trafik till exempelvis din httpd i din DMZ, med 'keep state':

block in on $dmz all
pass out on $dmz from any to any port 80 flags S/SA keep state

Självklart måste denna port portforward:as till rätt dator i din DMZ, detta mha 'rdr', läs mer om 'rdr' i 1.3.2 .

Options

För att begränsa minnet dina regler använder.

set limit { states 20000, frags 20000 }

Ovanstående rad begränsar antalet platser i minnespoolen (eng. memory pool) till 20000 för states, vilka är skapade av 'keep state'- och 'modulate-state'-regler, och 20000 för frags, skapade av 'scrub'-regler.

För att föra statistik över paket och bitar för ett nätverkskort:

set loginterface rl0

För att visa loggad statistik:

# pfctl -s info

För att bestämma livslängden på en inaktiv anslutning:

set optimization <typ>

De tillgängliga optimeringstyperna är:

  • normal - passar de flesta
  • high-latency - för ex. satellitanslutningar
  • satellite - detsamma som high-latency
  • aggressive - denna typ kan reducera minnesanvändningen kraftigt, samtidigt som inaktiva (eng. idle) anslutningar kan stängas ned tidigt.
  • conservative - motsatsen till aggressive, använder mer minne, eftersom anslutningar kan vara inaktiva relativt länge innan de stängs ned.

Packet normalization

För att förhindra eventuella attacker mot operativsystem med svag tcp/ip-stack (läs win 95/98), bör man normalisera inkommande trafik, dvs ta hand om ipfragment, dvs trasiga anslutningar. Vill du läsa mer om ämnet hänvisar jag hit.

scrub in on $ext all

Eftersom denna normalisering inte är kompatibel med Linux nfs brukar jag använda flaggan no-df till de nätverkskort jag använder till att mount:a linux nfs. Man-sidan rekommenderar även att man använder flaggan random-id tillsammans med no-df:

scrub in on $int all no-df random-id

Tables

Tables, i fortsättningen tabeller, är namngivna samlingar av ipadresser och nät. Tabeller är också dynamiska, du kan förändra en tabell under drift utan att behöva ladda om pf.conf. Andra fördelar med tabeller är att de hanteras snabbare än macros av pf, dessutom loggas statistik separat för tabeller. Ett exempel:

# En konstant (eng. constant) tabell skapas innehållande privata nät.
# ("10/8" är samma sak som "10.0.0.0/8")
table <private> const { 10/8, 172.16/12, 192.168/16 }
# En dynamisk tabell skapas, dvs badhosts. 'persist' innebär att kärnan
# tvingas att behålla tabellen trots att den är tom. 
table <badhosts> persist
# Dags att blockera trafik mellan våra elakartade tabeller och
# vårt externa nätverkskort:
block on $ext from { <private>, <badhosts> } to any

Dags att lägga till en adress i tabellen badhosts, den dynamiska:

# pfctl -t badhosts -T add 123.123.123.123

På samma sätt raderar vi en post i tabellen:

# pfctl -t badhosts -T delete 123.123.123.123

För att visa innehållet i tabellen badhosts:

# pfctl -t badhosts -T show

Man kan även ha ipadresser och nät sparade i en fil, och därefter läsa in dessa med pf, förutsatt att de är ordnade en per rad, t ex:

# cat /etc/badhosts
123.123.123.123
234.234.234.234

Dags att lägga innehållet i dessa filer i tabellen badhosts:

table <badhosts> persist file "/etc/badhosts" file "/etc/evenmorebadhosts"

Därefter blockerar vi dessa ipadresser/nät:

block on $ext from <badhosts> to any

Loggning

Exemplet nedan blockerar och loggar inkommande tcppaket mot port 80 på det externa nätverkskortet, utan hänsyn till andra matchande regler.

block in log quick on $ext proto tcp from any to any port 80

För att nu visa loggad trafik:

# tcpdump -n -e -ttt -r /var/log/pflog

För att visa i realtid:

# tcpdump -i pflog0

Det går inte att läsa loggarna med cat eller valfri editor, detta p.g.a. att trafiken dumpats i binär form.

Adressöversättning

Från och med OpenBSD 3.2 sker all NAT-, BINAT- och portforward-konfigurering i /etc/pf.conf.(Tidigare i /etc/nat.conf) NAT betyder "Network Address Translation", dvs flera interna datorer delar på en extern ipadress. BINAT används exempelvis vid översättning från ett internt till ett externt ip, dvs alla interna ipadresser har varsin extern ipadress.

NAT

Ett exempel för att NAT:a 192.168.0.* till den externa ipadress adresserad på vårt externa nätverkskort:

nat on $ext from 192.168.0.0/24 to any -> ($ext)

Här dyker ännu en finurlig detalj upp, som är mycket tacksam att arbeta med för er med dynamisk ipadress. ($ext) plockar nämligen fram din externa ipadress adresserad till ditt externa nätverkskort. För att förenkla ytterligare kan vi göra detsamma på vårt interna nätverkskort, förutsatt att vi väljer antingen inet eller inet6:

nat on $ext inet from ($int)/24 to any -> ($ext)

De senaste två exemplena förutsätter såklart att du skapat alias för dina nätverkskort.

rdr

Ett exempel på portforwarding, där anslutningar till port 8080 skickas vidare till en intern maskin:

rdr on $ext proto tcp from any to ($ext) port 8080 -> 192.168.0.2 port 80

Lika enkelt blir det med ett portintervall. Här skickar portarna 20000 - 20010 vidare till 192.168.0.2:

rdr on $ext proto tcp from any to ($ext) port 20000:20010 -> 192.168.0.2 port 20000:20010

Men eftersom det är jobbigt att ha en regel för att släppa in port 8080, och en till regel för att skicka vidare port 8080, så använder vi rdr pass:

rdr pass on $ext proto tcp from any to ($ext) port 8080 -> 192.168.0.2 port 80

Om vi däremot vill logga vilka paket som går igenom rdr-regeln så måste vi använda en tillhörande pass-regel:

pass in log on $ext proto tcp from any to ($ext) port 8080

BINAT

För att låta den interna ipadressen 192.168.0.10 översättas till ipaddressen på nätverkskortet xl0:

binat on xl0 from 192.168.0.10 to any -> (xl0)

Load balancing

För att t ex balansera anslutningar mot port 80 mellan två webservrar:

rdr on $ext proto tcp from any to any port 80 -> { 192.168.0.11, 192.168.0.12 } round-robin

För att utnyttja multipla anslutningar:

nat on $ext_1 inet from ($int)/24 to any -> ($ext_1)
nat on $ext_2 inet from ($int)/24 to any -> ($ext_2)

Nu NAT:ar vi alltså två anslutningar. Dags att routa trafik från vårt interna nätverkskort:

pass in on $int route-to { ($ext_1 $gw_1), ($ext_2 $gw_2) } round-robin from ($int)/24 to any keep state

Där $gw_1 och $gw_2 är ipnummer för respektive externa nätverkskorts gateway.

En komplett exempelkonfiguration

Nedan följer ett exempel på en komplett konfiguration av pf.conf:

# 1. Macros
lo = lo0
int = rl0
ext = ne3
dmz = xl0
reserved = " { 0.0.0.0/8, 10.0.0.0/8, 20.20.20.0/24, 127.0.0.0/8,
            169.254.0.0/16, 172.16.0.0/12, 192.0.2.0/24, 192.168.0.0/16,
            224.0.0.0/3, 255.255.255.255 } "
# 2. Tables
# Tomt.

# 3. Options
set limit { states 20000, frags 20000 }
# Ska paket som blockeras lämna svarstrafik eller ej?
# Jag föredrar att "droppa" blockerad trafik, 
# vilket är default om inget annat är angivet:
set block-policy drop
# För att istället lämna svarstrafik:
# set block-policy return

set optimization aggressive
# 4. Packet normalization
scrub in on $ext all

# 5. Queueing
# För exempel med ALTQ, se del 2.

# 6. Translation
nat on $ext inet from ($int)/24 to any -> ($ext)

# 7. Filtering
# Hindra inte loopback-trafik:
pass quick on $lo all
# Antispoof
antispoof for { $lo, $int, $ext }
# Vi låter trafik till och från $int flöda fritt:
pass quick on $int all
# Blockera all in-trafik från reserverade nät till vårt externa nätverkskort:
block in quick on $ext from $reserved to any
# Blockera all inkommande trafik på vårt externa nätverkskort som default:
block in on $ext all
# Blockera all ut-trafik på vårt externa nätverkskort som 
# _inte_ har vår externa ipadress som källadress (eng. source address):
block out quick on $ext inet from ! ($ext) to any

# Om du vill att man ska kunna ping:a dig utifrån, 'inet'
# gör att endast ping via ipv4-protokollet släpps in:
pass in on $ext inet proto icmp all icmp-type 8 code 0 keep state

# För kompabilitet med bl a ircservrar. Här går vi alltså förbi vår "block-policy":
block return in quick on $ext proto tcp from any to any port 113

# Släpp in samt logga anslutningar till vår ssh-demon:
pass in log quick on $ext proto tcp from any to any port 22 flags S/SA keep state
# Ännu ett exempel, där två protokoll kombineras:
#pass in quick on $ext proto { tcp, udp } from any to any port 53 flags S/SA keep state

# Släpp ut all trafik och kom ihåg state:
pass out on $ext proto {tcp, udp, icmp} all keep state

# Regler för din DMZ
block in on $dmz all
pass out on $dmz from any to any port 80 flags S/SA keep state
# EOF

Dags för del två!

ALTQ

Inledning

Vad är ALTQ? ALTQ, "ALTernating Queueing", är ett verktyg för att manipulera utåtgående trafik. För att ALTQ ska fungera krävs en köhanterare. Normalt används köhanteraren FIFO, "First In - First Out". I många fall är dock FIFO direkt olämplig som köhantarere. Förmodligen vill du t ex inte att ssh-anslutningar ska bli lidande p g a tung ftp-trafik.

Två alternativ till FIFO är PRIQ och CBQ; stöd för dessa finns i GENERIC-kärnan. Det arbetas för närvarande på att få stöd för köhanteraren HFSC, "Hierarchical Fair Service Curve".

I och med OpenBSD 3.3 har ALTQ integrerats med pf, vilket har flera fördelar. Trafik behöver nu inte matchas av både pf och ALTQ, utan matchas nu en gång istället för två, vilket medför snabbare agerande och minskad belastning. ALTQ är nu dessutom betydligt enklare att arbeta med, tidigare var det direkt besvärligt.

Vad är PRIQ? PRIQ, "PRIority Queueing", är som namnet antyder, en prioritetsbaserad köhanterare. Med PRIQ kan du skapa köer med en prioritet mellan 0 - 15, där 15 har högst prioritet. Trafik i den kö med högst prioritet behandlas först.

Vad är CBQ? CBQ, "Class Based Queueing" är en hierarkisk köhanterare, dvs har en trädliknande struktur. Man skapar alltså en kö, filtrerar önskad trafik till denna, och begränsar sedan kön, antingen procentuellt eller bandbreddsmässigt. Ofta använder man också 'borrowing', dvs kön kan låna bandbredd av förälderkön om överflödig bandbredd finns tillgänglig. Det är detta, den trädliknande hierarkitillämpningen, som gör CBQ så kraftfull.

Är PRIQ eller CBQ rätt för mig? De stora skillnaderna mellan PRIQ och CBQ är att i PRIQ kan trafik som prioriterats högst plocka åt sig all bandbredd om detta krävs, och trafik i en lågprioriterad kö kan få extrema fördröjningar eller dö. Detta inträffar inte i en CBQ-konfiguration eftersom man här skapar köer som man procentuellt eller bandbreddsmässigt begränsar. Denna begränsningen träder endast i kraft då kön inte längre kan låna bandbredd från förälderkön, om man låter kön låna bandbredd från sin förälder. Så i de flesta fall känns CBQ som ett bättre alternativ. CBQ kräver dock lite mer av dig som ska utföra konfigureringen.

Hur ska jag prioritera min trafik? När det gäller trafikprioritering får du helt enkelt ställa dig frågan vilken trafiktyp som är viktigast för just ditt nätverk. Ofta är det bandbreddssnål, känslig trafik såsom ssh, ev. telnet, dns, och mail man anser viktigare än bandbreddsslukande trafik såsom http och ftp. Även udp-trafik, som många nätverksspel använder sig av, kan vara trevligt att prioritera.

ALTQ, med PRIQ som köhanterare

Vi börjar med att bestämma köhanterare samt bandbredd och skapar sedan de köer vi behöver:

altq on $ext priq bandwidth 640Kb queue { def, high, low }

Min utgående bandbredd är i teorin 768 Kbit/s, i praktiken ligger den nära 640 Kbit/s. Här får man prova sig fram, väljer man för hög bandbredd fungerar inte ALTQ som det ska, väljer man för låg används inte all tillgänglig bandbredd. När man använder ALTQ krävs alltid en kö dit trafik som man inte matchat hamnar, dvs 'def', default.

Nu är det dags att sätta prioritet på våra köer, samt bestämma vilken av köerna som är vår default-kö:

queue high  priority 2
queue low   priority 0
queue def   priority 1 priq(default)

Då är vår struktur av köer klar, dags att börja matcha trafik till dessa köer, över till filtreringsdelen! Det enda man gör är alltså att i slutet på regeln berätta vilken kö trafiken ska matchas till. Både direkt och indirekt (via states) matchad trafik hamnar i vald kö.

pass     on $ext proto tcp from any to any port 22 keep state queue (def, high)
pass in  on $ext proto tcp from any to any port 80 keep state queue low

Normalt anges bara en kö, men om man vill att tomma tcp-ACK(acknowledge)-paket, dvs paket som säger "Hej, jag har fått vad jag skulle få", och annan känslig, "low-latency"-trafik hamna i en högre prioriterad kö, kan man matcha en regel till två köer, där den sista har högre prioritet.

Eftersom ALTQ endast behandlar utåtgående trafik, kan rad nummer två verka märklig. Här är det alltså utåtgående trafik besläktad med trafik matchad från denna rad som hamnar i vår kö med låg prioritet, low.

Övrig trafik, som vi inte matchar mha våra filtreringsregler, hamnar i default-kön, def. En komplett PRIQ-konfigurering bifogas i slutet av artikeln.

ALTQ, med CBQ som köhanterare

Vi börjar med att bestämma köhanterare, bandbredd och de klasser vi behöver:

altq on $ext cbq bandwidth 640Kb queue { def, ftp, udp, http, ssh, icmp }

Därefter är det dags att bestämma hur hög bandbredd varje klass ska få vid full belastning, samt ev. tillåta 'borrowing' från sin förälder. 'red', som står för "Random Early Detection", är en algoritm som ser till att en kö inte blir full.

queue def bandwidth 18% cbq(default borrow red)
queue ftp bandwidth 10% cbq(borrow red)
queue udp bandwidth 30% cbq(borrow red)
queue http bandwidth 20% cbq(borrow red)
queue ssh bandwidth 20% cbq(borrow red) { ssh_interactive, ssh_bulk }
queue ssh_interactive priority 7
queue ssh_bulk priority 0
queue icmp bandwidth 2% cbq

Default-kö här är def, alla köer förutom icmp får låna av sin förälder. Notera också att kö ssh:s barnköer, ssh_interactive och ssh_bulk, har olika prioritet. Prioriteten för ssh_interactive har egentligen inte någon betydelse, sålänge den är högre än ssh_bulk.

Nu är vår köstruktur färdig, dags att matcha trafik till våra köer!

pass    log quick on $ext proto tcp from any to any port 22 flags S/SA     keep state queue (ssh_bulk, ssh_interactive)
pass in     quick on $ext proto tcp from any to any port 20 flags S/SA  keep state queue ftp
pass in     quick on $ext proto tcp from any to any port 80 flags S/SA  keep state queue http
pass out          on $ext proto udp   all                               keep state queue udp
pass out          on $ext proto icmp  all                               keep state queue icmp

Övrig trafik, som vi inte matchar mha våra filtreringsregler, hamnar i default-kön, def. En komplett CBQ-konfiguration bifogas i slutet av artikeln.

Avslutning

Synpunker, klagomål och ev. beröm skickas till a.semborg at gmail.com

Vidare läsning

Fler exempel hittar du i /usr/share/pf.

Personliga verktyg