PF NAT für IPv4- und IPv6-Adresse

SolarCatcher

Well-Known Member
Hallo,

normalerweise kann ich in meiner pf.conf die aktuelle öffentliche IP-Adresse durch runde Klammern um das externe Netzwerk-Interface referenzieren, z.B. "(em0)".

Das geht auch für NAT. Die OpenBSD FAQs für die FreeBSD-Version von PF sagen dazu:
you can tell PF to automatically update the translation address by putting parentheses around the interface name:

nat on tl0 from dc0:network to any -> (tl0)

This method works for translation to both IPv4 and IPv6 addresses.

Mein externes Netzwerk-Interface hat nun eine IPv4- und eine IPv6-Adresse. Also wollte ich folgendes machen und damit IPv4 und IPv6 erschlagen, um Traffic von meinen Jails zu NAT'ten:
Code:
nat pass on $ext_if inet proto { tcp udp icmp } from $jails_ip4 to any -> ($ext_if)
nat pass on $ext_if inet6 proto { tcp udp icmp6 } from $jails_ip6 to any -> ($ext_if)

Das klappte aber nicht gut und eine "pfctl -sa" zeigte, dass an die Zeile automatisch noch ein "round-robin" angehängt wurde. PF hat die zwei unterschiedlichen Adressen also als Pool aufgefasst, die es reihum verwenden soll. Das war natürlich nicht mein Ziel!

Daher fällt mir nix anderes ein, als die beiden öffentlichen Adressen (IPv4 und IPv6) doch hart reinzuschreiben. Damit hatte ich mir jetzt fast schon ins Bein geschossen: Das ganze ist nämlich ein Droplet (VPS) bei DigitalOcean. Und wenn ich eine neue Instanz mit dieser Konfiguration starte, erhalte ich neue IP-Adressen und diese passen nicht mehr zur pf.conf. Und dann komme ich nicht einmal per SSH hinein!

Gibt es da eine elegantere Lösung?

Hier zur Verdeutlichung meine (deutlich verkürzte) pf.conf:
Code:
ext_if = "vtnet0" # externes Netzwerk-Interface
int_if = "lo1" # internes Netzwerk-Interface

ip4_pub = ... # öffentliche IPv4-Adresse
ip6_pub = ... # öffentliche IPv6-Adresse

jails_ip4 = "{ 10.0.0.0/28 }" # (privater) IPv4-Adressbereich der Jails
jails_ip6 = "{ fd0c:800b:c17d:b1f0::0/116 }" # (privater) IPv6-Adressbereich der Jails

(...) 

nat pass on $ext_if inet proto { tcp udp icmp } from $jails_ip4 to any -> $ip4_pub
nat pass on $ext_if inet6 proto { tcp udp icmp6 } from $jails_ip6 to any -> $ip6_pub
 
Ich würde mal prüfen, wie die tatsächliche Funktion dann aussieht. Das NATen von IPv4 auf eine IPv6-Adresse sollte ja gar nicht funktionieren. Wenn er da keine Fehler bringt und IPv4 immer auf IPv4 geht, dann wäre die Anzeige rein kosmetisch.
 
Nein, leider ist das nicht kosmetisch, sondern blockiert tatsächlich. Nicht 100% aber mindestens mehrere Sekunden, teilweise aber auch solange, dass ich irgendwann abgebrochen habe (fetch einer öffentlichen Webseite - versucht per -4 und -6). Mit der echten IPv4- bzw. IPv6-Adresse klappt's dagegen reibungslos.
 
Ich kann mir grad nicht vorstellen, dass pf bei einer Regel mit "inet", in der IPv6-Adressen auftauchen, irgendwie Mist baut. Ich kann doch z.B. auch gemischte IPv4/IPv6-Tabellen haben, die dann in inet6-Regeln halt die IPv4-Adressen einfach nicht beachten. Soll das bei NAT anders sein?
 
@double-p Die Idee klingt gut. Aber es ändert nichts am Ergebnis. Offenbar geht ein großer Teil der Pakete "verloren". Das ganze war mir ja aufgefallen, als ich vom Host-System aus ein "pkg -j <jail> upgrade" machen wollte. Es geht... aber jeder Download dauert extrem lange. Das änder ($ext_if:0) leider nichts.

@TCM Es erstaunt mich auch, aber das ist das, was ich in der Praxis beobachte.

Diese NAT/RDR-Regeln...
Code:
nat pass on $ext_if inet proto { tcp udp icmp } from $jails_ip4 to any -> ($ext_if:0)
nat pass on $ext_if inet6 proto { tcp udp icmp6 } from $jails_ip6 to any -> ($ext_if:0)
rdr pass on $ext_if inet proto tcp from any to (vtnet0) port $jail_services -> 10.0.0.1
rdr pass on $ext_if inet6 proto tcp from any to (vtnet0) port $jail_services -> fd0c:800b:c17d:b1f0::1
rdr pass on $ext_if inet proto tcp from any to (vtnet0) port 2020 -> 10.0.0.1 port ssh

...werden dann so von PF aufgelöst (Translation-Teil aus "pfctl -sa")
Code:
TRANSLATION RULES:
nat pass on vtnet0 inet proto tcp from 10.0.0.0/28 to any -> (vtnet0:0)
nat pass on vtnet0 inet proto udp from 10.0.0.0/28 to any -> (vtnet0:0)
nat pass on vtnet0 inet proto icmp from 10.0.0.0/28 to any -> (vtnet0:0)
nat pass on vtnet0 inet6 proto tcp from fd0c:800b:c17d:b1f0::/116 to any -> (vtnet0:0)
nat pass on vtnet0 inet6 proto udp from fd0c:800b:c17d:b1f0::/116 to any -> (vtnet0:0)
nat pass on vtnet0 inet6 proto ipv6-icmp from fd0c:800b:c17d:b1f0::/116 to any -> (vtnet0:0)
rdr pass on vtnet0 inet proto tcp from any to (vtnet0) port = http -> 10.0.0.1
rdr pass on vtnet0 inet proto tcp from any to (vtnet0) port = https -> 10.0.0.1
rdr pass on vtnet0 inet proto tcp from any to (vtnet0) port = 8080 -> 10.0.0.1
rdr pass on vtnet0 inet6 proto tcp from any to (vtnet0) port = http -> fd0c:800b:c17d:b1f0::1
rdr pass on vtnet0 inet6 proto tcp from any to (vtnet0) port = https -> fd0c:800b:c17d:b1f0::1
rdr pass on vtnet0 inet6 proto tcp from any to (vtnet0) port = 8080 -> fd0c:800b:c17d:b1f0::1
rdr pass on vtnet0 inet proto tcp from any to (vtnet0) port = xinupageserver -> 10.0.0.1 port 22

Und genau wie mit ($ext_if), führt das zu massiver Verlangsamung.

Stelle ich wieder die IP-Adressen fix ein. Geht auch alles wieder genau so: fix!
 
moin
round-robin beim nat ist default , als option source-hash oder sttic-port siehe man pf.conf

nat und ipv6 ? wozu ?

6to4 nat geht wohl 4to6 nicht, ups bin bei openbsd ... bei freebsd bin ich mir nicht sicher.


holger
 
@mark05 Vielen Dank. source-hash und static-port lese ich mir gleich mal in der manpage durch.

Da mir gesagt wurde, dass PF unter FreeBSD kein Redirect zwischen IPv4 und IPv6 beherrscht, hatte ich angenommen, dass das auch für NAT gilt...

Warum überhaupt IPv6 NAT'ten? Da gibt's verschiedene Gründe.

Der wichtigste ist, dass es sich um ein Droplet (VPS) bei DigitalOcean handelt. Man kann sich davon ein Image anlegen und dann neue Instanzen starten - sogar über Kundenaccounts hinweg. Dabei bekommt man natürlich neue IP-Adressen zugewiesen. Ich glaube, man bekommt auch immer 20 öffentliche IPv6-Adressen (wenn man IPv6 denn anwählt). Jetzt könnte ich jedes Mal von Hand die Jails mit einer dieser Adressen konfigurieren. Das ist mühselig... Oder irgendwelche Shell-Script-Magie nutzen, um das mehr oder weniger automatisch zu tun... aber da wüsste ich erstmal nicht, wie ich das am sinnvollsten mache... So, wie ich das jetzt habe, nutzen die Jails (Webserver/PHP-FPM, MariaDB, MongoDB) auf jeder Instanz dieselben privaten Adressen für IPv4 und IPv6. Da brauche ich dann weder an der jail.conf noch an der pf.conf etwas zu schrauben.

Ein anderer - weniger wichtiger - Grund: Da die Jails auch IPv4-Adressen haben, finde ich die pf.conf schlichtweg übersichtlicher, wenn die Regeln für IPv4 und IPv6 so ähnlich wie möglich sind. Und da die Mehrheit der Jails von außen eh nicht erreichbar sein soll, bietet es sich geradezu an, mit privaten IP-Adressen zu arbeiten. Ja, da gab und gibt es große Glaubensstreitigkeiten, ob das mit IPv6 nicht obsolet sein sollte... aber wozu gibt es dann diese privaten IPv6-Adressen? Viele machen es so wie ich und ich habe hier erklärt, warum ich es für mich so praktischer finde.
 
round-robin beim nat ist default , als option source-hash oder sttic-port siehe man pf.conf

Ich habe mir das jetzt einmal etwas angeschaut. Erst einmal sehe ich nicht, wie mir static-port helfen könnte. Source-hash ist da interessanter, aber die exzellenten FAQs (in der Version für OpenBSD 4.5 passend für FreeBSD's PF) sagen dazu:
Except for the round-robin method, the address pool must be expressed as a CIDR (Classless Inter-Domain Routing) network block. The round-robin method will accept multiple individual addresses using a list or table.

Die zwei einzelnen Adressen (IPv4 und IPv6) sollten also nicht gehen. Ich hab's trotzdem einmal versucht:
Code:
nat pass on $ext_if inet proto { tcp udp icmp } from $jails_ip4 to any -> ($ext_if) source-hash
nat pass on $ext_if inet6 proto { tcp udp icmp6 } from $jails_ip6 to any -> ($ext_if) source-hash

Da kam dann aber erwartungsgemäß eine Fehlermeldung:
Code:
/etc/pf1.conf:21: interface (vtnet0) is only supported in round-robin redirection pools
/etc/pf1.conf:22: interface (vtnet0) is only supported in round-robin redirection pools
 
Dann fällt mir nur ein, Tabellen zu benutzen und die in /etc/rc.local mit den aktuellen Adressen zu befüllen.

Wie wird denn auf die Hosts zugegriffen, wenn eine neue Instanz mit vorher unbekannten Adressen hochkommt? Da muss doch irgendwas im DNS existieren?
 
Hallo,
hat sich in der Sache noch etwas ergeben? Hänge momentan genau an der selben Stelle fest und würde mich über Hinweise freuen.

MfG
 
Ich hab hier zunächst mal den Gedanken mit den Tabellen weiterverfolgt und meine /etc/pf.conf ein bisschen abgeändert:

Code:
if_ext = "bge0"
if_lo1 =  "lo1"

table <tbl_ip_ext4> {}
table <tbl_ip_ext6> {}

nat pass on $if_ext inet from ($if_lo1:network) to any -> <tbl_ip_ext4>
nat pass on $if_ext inet6 from ($if_lo1:network) to any -> <tbl_ip_ext6>

Anstelle von -> <tbl_ip_ext6> stand dort vorher -> ($if_ext). Hat vorher nur mit IPv4 funktioniert, und bei IPv6 nur alle N mal (wenn der round-robin die passende IPv6 Adresse getroffen hat).

Dann habe ich unter /root/bin/pf-tableset.sh ein Script geschrieben um die Tabellen zu befüllen:

Bash:
#!/usr/bin/env sh

IF=${IF:-"bge0"}
T4=${T4:-"tbl_ip_ext4"}
T6=${T6:-"tbl_ip_ext6"}

INFO=$(/sbin/ifconfig "$IF")

_table() {
    TBL=$1; shift
    /sbin/pfctl -t "$TBL" -T "$@"
}
_pull() {
    echo "$INFO" | /usr/bin/grep -e "$1" | /usr/bin/head -1 | /usr/bin/cut -d' ' -f2
}

V4=$(_pull "inet[^6]")
V6=$(_pull "inet6\ [^f].*temporary")

show() {
    echo "current v4:   $V4"
    echo "current v6:   $V6"
    echo "table v4:     $(_table "$T4" "show" | xargs)"
    echo "table v6:     $(_table "$T6" "show" | xargs)"
}

flush() {
    _table "$T4" "flush"
    _table "$T6" "flush"
}

run() {
    _table "$T4" "replace" "$V4"
    _table "$T6" "replace" "$V6"
}

usage() {
    echo "Usage: $0 ( show | flush | run )"
    echo "  show:   show current table and new entries"
    echo "  flush:  flush current table"
    echo "  run:    add new entries into table"
}

case $1 in
    [sS]*) show ;;
    [fF]*) flush ;;
    [rR]*) run ;;
    *) usage;;
esac

Wenn ich nun pf-tableset.sh run von Hand anstarte werden die Tabellen befüllt, und ein Zugriff auf das Netzwerk funktioniert aus den Jails raus korrekt! Hurra!

Jetzt ein paar Fragen:
  • Gibt es einen eleganteren Weg die IP-Adressen zu erhalten, anstatt ifconfig von Hand zu parsen?
  • Wie bekomme ich das hin, dass pf-tableset.sh bei jeder Änderung am Interface automatisch angestartet wird?
    • Von der Variante das in der /etc/rc.local zu verankern halte ich recht wenig - funktioniert nur einmalig nach dem Neustart... Was ist wenn ich das Netzwerkkabel umstecke, was ist, wenn sich die Privacy Extensions entscheiden sich eine neue Adresse zu generieren? ...
  • Ist pfctl -t table -T replace 1.2.3.4 der korrekte Weg eine Adresse zu setzen?
 
Ah, cool. War mir bis eben neu.
Hatte mir inzwischen was mit devd(8) gebastelt, was eigentlich auch so funktioniert (werde die Tage die Config hier auch nochmal hochladen).
Habe mich auf die LINK_UP und LINK_DOWN Messages von dem Interface registriert, dass dann mein Script anstupst. Geht soweit.

Allerdings kommt beim booten das LINK_UP Signal noch bevor pf gestartet wurde, somit schlägt pfctl erstmal fehl.
Gibt es in ifstated auch ein Signal dass die Firewall gestartet wurde? Dann wäre das perfekt..
 
Zuletzt bearbeitet:
Zurück
Oben