mirror of
https://github.com/lgcosta/pf-linux-port.git
synced 2026-11-15 13:54:45 -03:00
Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2cd13eb806 | ||
|
|
e03df9adc8 | ||
|
|
7d1e4b81e9 | ||
|
|
f3143a54a8 | ||
|
|
917d3e3fe0 | ||
|
|
d520172753 | ||
|
|
9c3c4b6bbc | ||
|
|
c449e01b9a | ||
|
|
525fe7ee28 | ||
|
|
18c0a69f74 | ||
|
|
3aa0c2d0d0 |
No files matched your search
@@ -1,7 +1,7 @@
|
||||
# PF-on-Linux top-level make
|
||||
KERNELDIR ?= $(HOME)/lab/linux_kernel/linux-7.3.4
|
||||
|
||||
.PHONY: module tools kernel test clean vm vm-destroy docs
|
||||
.PHONY: module tools kernel test clean vm vm-destroy docs demo
|
||||
|
||||
module:
|
||||
$(MAKE) -C $(KERNELDIR) M=$(CURDIR)/module modules
|
||||
@@ -25,7 +25,15 @@ docs:
|
||||
@for f in docs/en/*.md; do echo "EN $$f"; done
|
||||
@for f in docs/pt-BR/*.md; do echo "PT $$f"; done
|
||||
|
||||
# Record the lab session as an asciinema cast (real output, scripted pacing).
|
||||
demo:
|
||||
@mkdir -p out
|
||||
@command -v asciinema >/dev/null 2>&1 || { \
|
||||
echo "asciinema not installed: pip install asciinema"; exit 1; }
|
||||
asciinema rec out/demo-pf-linux.cast -c "bash scripts/demo-session.sh"
|
||||
@echo "-> out/demo-pf-linux.cast (play: asciinema play out/demo-pf-linux.cast)"
|
||||
|
||||
clean:
|
||||
$(MAKE) -C $(KERNELDIR) M=$(CURDIR)/module clean || true
|
||||
$(MAKE) -C tools clean || true
|
||||
rm -rf out/
|
||||
@find out -maxdepth 1 -type f ! -name '*.cast' -delete 2>/dev/null || true
|
||||
@@ -0,0 +1,41 @@
|
||||
# Demo (asciinema)
|
||||
|
||||
The lab can be recorded as an [asciinema](https://asciinema.org) cast. Every
|
||||
byte in the recording is the real output of the lab VMs (Alpine under libvirt
|
||||
running the kernel built from source with `pf.ko`); only the pacing is
|
||||
scripted.
|
||||
|
||||
## Record
|
||||
|
||||
```sh
|
||||
pip install asciinema # once
|
||||
bash scripts/vm-run-tests.sh # or: scripts/vm-create.sh + deploy
|
||||
asciinema rec out/demo-pf-linux.cast -c "bash scripts/demo-session.sh"
|
||||
```
|
||||
|
||||
The session walks through:
|
||||
|
||||
1. the lab topology and the freshly loaded `pf.ko` (`dmesg` proof),
|
||||
2. `pfctl -s info` over the OpenBSD ioctl ABI,
|
||||
3. a real `pf.conf` (table, stateful rules, explicit drop) and `pfctl -sr`,
|
||||
4. transit ping + `pfctl -ss` (stateful),
|
||||
5. `pfctl -t <table> -T show` (live tables),
|
||||
6. a blocked ping (counters with `pfctl -sr -vv`),
|
||||
7. `divert-to` with the `pdivert` relay (packet to userspace and back),
|
||||
8. shutdown.
|
||||
|
||||
## Play back
|
||||
|
||||
```sh
|
||||
asciinema play out/demo-pf-linux.cast
|
||||
```
|
||||
|
||||
## Upload
|
||||
|
||||
```sh
|
||||
asciinema upload out/demo-pf-linux.cast
|
||||
```
|
||||
|
||||
`scripts/demo-record.py` is an alternative recorder that produces the same
|
||||
asciicast v2 format without the asciinema binary (it drives the lab over
|
||||
ssh and writes the cast itself).
|
||||
@@ -882,3 +882,25 @@ Decisions taken (all verified in the libvirt lab):
|
||||
|
||||
`pdivert` (userspace relay, `tools/pdivert/`) is the reference client; it
|
||||
demonstrates the BSD "receive -> rewrite -> reinject" cycle.
|
||||
|
||||
## Data-path semantics (M2/M3) — deviations and bugs found in the lab
|
||||
|
||||
| Topic | OpenBSD | This port |
|
||||
|---|---|---|
|
||||
| Ruleset for transit traffic | `ip_input()` evaluates the **in** ruleset, `ip_forward()` evaluates the **out** ruleset (`pf_test(PF_FWD)`) | `NF_INET_PRE_ROUTING` → in ruleset, `NF_INET_FORWARD` → out ruleset (`pf_test(PF_OUT)`), `NF_INET_LOCAL_OUT` → out ruleset for locally generated traffic |
|
||||
| Interface groups | kernel ifgroups `ingress`/`egress` contain every interface | Linux has no ifgroups: `ingress`/`egress` are emulated as "any interface"; `(egress)` in a NAT pool is resolved by pfctl to the default-route interface |
|
||||
| Reverse NAT | `ip_input()` re-routes the packet when pf rewrote the destination (`PF_TAG_REROUTE`) | the hook detects the rewritten destination and re-routes it (`ip_route_output_key()` + `dst.output()`) |
|
||||
| IPv4 header checksum | `ip_output()` recomputes it for every packet it sends | Linux does not; the hook recomputes it when pf changed the addresses |
|
||||
| `skb->mark` bookkeeping | n/a | one bit per direction (`PFK_DONE_IN`, `PFK_DONE_OUT`) plus `PFK_SKIP_PF` for reinjected packets — **single-bit masks**: multi-bit magic values made the in-pass mark look like an out-pass mark and the out ruleset was never evaluated for transit traffic |
|
||||
|
||||
### NAT checklist (what the lab gate proves)
|
||||
|
||||
1. `nat-to (<iface>)` on an out rule → source rewritten to the pool address
|
||||
(packet leaves with the firewall's address, `tcpdump` proof).
|
||||
2. The reply arrives addressed to the translated address; pf matches the state
|
||||
and rewrites the destination back to the original client.
|
||||
3. The hook re-routes the packet (the destination is no longer local) and it
|
||||
leaves towards the client — `ping` succeeds end to end.
|
||||
|
||||
Without step 3 the reply is delivered to the firewall's own stack and the
|
||||
client never sees it (the classic "NAT works one way" symptom).
|
||||
@@ -0,0 +1,40 @@
|
||||
# Demo (asciinema)
|
||||
|
||||
O lab pode ser gravado como um cast do [asciinema](https://asciinema.org).
|
||||
Cada byte da gravação é a saída real das VMs do lab (Alpine sobre libvirt,
|
||||
rodando o kernel compilado do fonte com o `pf.ko`); só o ritmo é roteirizado.
|
||||
|
||||
## Gravar
|
||||
|
||||
```sh
|
||||
pip install asciinema # uma vez
|
||||
bash scripts/vm-run-tests.sh # ou: scripts/vm-create.sh + deploy
|
||||
asciinema rec out/demo-pf-linux.cast -c "bash scripts/demo-session.sh"
|
||||
```
|
||||
|
||||
A sessão percorre:
|
||||
|
||||
1. a topologia do lab e o `pf.ko` recém-carregado (prova no `dmesg`),
|
||||
2. `pfctl -s info` pela ABI de ioctl do OpenBSD,
|
||||
3. um `pf.conf` real (tabela, regras stateful, drop explícito) e `pfctl -sr`,
|
||||
4. ping em trânsito + `pfctl -ss` (stateful),
|
||||
5. `pfctl -t <tabela> -T show` (tabelas vivas),
|
||||
6. um ping bloqueado (contadores com `pfctl -sr -vv`),
|
||||
7. `divert-to` com o relay `pdivert` (pacote vai ao userspace e volta),
|
||||
8. desligamento.
|
||||
|
||||
## Reproduzir
|
||||
|
||||
```sh
|
||||
asciinema play out/demo-pf-linux.cast
|
||||
```
|
||||
|
||||
## Publicar
|
||||
|
||||
```sh
|
||||
asciinema upload out/demo-pf-linux.cast
|
||||
```
|
||||
|
||||
`scripts/demo-record.py` é um gravador alternativo que produz o mesmo formato
|
||||
asciicast v2 sem o binário asciinema (ele conduz o lab por ssh e escreve o
|
||||
cast diretamente).
|
||||
@@ -7,7 +7,7 @@
|
||||
long pfk_ioctl(struct file *f, unsigned int cmd, unsigned long addr);
|
||||
int pfk_enable(int on);
|
||||
void pfk_status(struct pf_status *s);
|
||||
#define PFK_SKIP_PF 0x50460040u /* skb->mark: reinjected, bypass pf */
|
||||
#define PFK_SKIP_PF 0x00000004u /* skb->mark: reinjected, bypass pf */
|
||||
|
||||
int pfk_hooks_register(void);
|
||||
void pfk_hooks_unregister(void);
|
||||
|
||||
@@ -90,6 +90,17 @@ pfi_kif_alloc(const char *kif_name, int mflags)
|
||||
kif->pfik_flags |= PFI_IFLAG_ANY;
|
||||
kif->pfik_flags_new |= PFI_IFLAG_ANY;
|
||||
}
|
||||
/*
|
||||
* PFLINUX: OpenBSD creates the "ingress" and "egress" interface
|
||||
* groups (ifgroups) automatically and both contain every interface;
|
||||
* pf's direction (in/out ruleset) carries the actual meaning. Linux
|
||||
* has no ifgroups, so emulate the groups as "any interface".
|
||||
*/
|
||||
if (!strcmp(kif->pfik_name, "ingress") ||
|
||||
!strcmp(kif->pfik_name, "egress")) {
|
||||
kif->pfik_flags |= PFI_IFLAG_ANY;
|
||||
kif->pfik_flags_new |= PFI_IFLAG_ANY;
|
||||
}
|
||||
|
||||
return (kif);
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ struct pfk_pkt {
|
||||
int dir;
|
||||
};
|
||||
|
||||
#define PFK_DONE_IN 0x50460010u /* skb->mark bit: PF_IN pass completed */
|
||||
#define PFK_DONE_OUT 0x50460020u /* skb->mark bit: PF_OUT pass completed */
|
||||
#define PFK_DONE_IN 0x00000001u /* skb->mark bit: PF_IN pass completed */
|
||||
#define PFK_DONE_OUT 0x00000002u /* skb->mark bit: PF_OUT pass completed */
|
||||
|
||||
static void
|
||||
pfk_pkt_reinject(struct pfk_pkt *pk)
|
||||
@@ -71,14 +71,75 @@ pfk_pkt_reinject(struct pfk_pkt *pk)
|
||||
kfree(pk);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Reverse NAT: pf rewrote the destination of an inbound packet (the reply
|
||||
* of a translated flow). OpenBSD re-routes such a packet in ip_input()
|
||||
* (PF_TAG_REROUTE); Linux has no such layer, so re-route it here and push
|
||||
* it out through the new egress device.
|
||||
*/
|
||||
static void
|
||||
pfk_reroute(struct sk_buff *skb)
|
||||
{
|
||||
struct flowi4 fl4 = { };
|
||||
struct rtable *rt;
|
||||
struct iphdr *iph = ip_hdr(skb);
|
||||
|
||||
fl4.daddr = iph->daddr;
|
||||
/*
|
||||
* Leave the source empty: the packet keeps the original source
|
||||
* (the peer's address), but the kernel would reject a route lookup
|
||||
* with a non-local source (ENETUNREACH).
|
||||
*/
|
||||
fl4.saddr = 0;
|
||||
fl4.flowi4_oif = 0;
|
||||
fl4.flowi4_mark = 0;
|
||||
fl4.flowi4_scope = RT_SCOPE_UNIVERSE;
|
||||
|
||||
rt = ip_route_output_key(&init_net, &fl4);
|
||||
if (IS_ERR(rt) || rt == NULL) {
|
||||
kfree_skb(skb);
|
||||
return;
|
||||
}
|
||||
skb_dst_set(skb, &rt->dst);
|
||||
skb->dev = rt->dst.dev;
|
||||
skb_reset_network_header(skb);
|
||||
rt->dst.output(&init_net, skb->sk, skb);
|
||||
}
|
||||
|
||||
/*
|
||||
* PFLINUX: pf rewrote the IPv4 addresses of the packet (NAT). OpenBSD's
|
||||
* ip_output() recomputes the IP header checksum on every packet it sends;
|
||||
* Linux does not, so fix it up here after pf touched the header.
|
||||
*/
|
||||
static void
|
||||
pfk_fix_ip_csum(struct sk_buff *skb)
|
||||
{
|
||||
struct iphdr *iph;
|
||||
|
||||
if (!pskb_may_pull(skb, sizeof(struct iphdr)))
|
||||
return;
|
||||
iph = ip_hdr(skb);
|
||||
iph->check = 0;
|
||||
iph->check = csum_fold(csum_partial(skb->data, iph->ihl * 4, 0));
|
||||
/* the transport checksum was already fixed by pf */
|
||||
skb->ip_summed = CHECKSUM_NONE;
|
||||
}
|
||||
|
||||
static void
|
||||
pfk_pkt_work(struct work_struct *w)
|
||||
{
|
||||
struct pfk_pkt *pk = container_of(w, struct pfk_pkt, work);
|
||||
struct mbuf *m, *m0;
|
||||
struct sk_buff *orig;
|
||||
__be32 orig_dst = 0, orig_src = 0;
|
||||
int action;
|
||||
|
||||
if (pk->af == AF_INET && pskb_may_pull(pk->skb, sizeof(struct iphdr))) {
|
||||
orig_dst = ip_hdr(pk->skb)->daddr;
|
||||
orig_src = ip_hdr(pk->skb)->saddr;
|
||||
}
|
||||
|
||||
m = pfk_m_wrap_skb(pk->skb, pk->af);
|
||||
if (!m) {
|
||||
pfk_pkt_reinject(pk); /* like "pf not consulted" */
|
||||
@@ -93,11 +154,27 @@ pfk_pkt_work(struct work_struct *w)
|
||||
* and packet walk run concurrently from here on — documented. */
|
||||
action = pf_test(pk->af, pk->dir, pk->ifp, &m);
|
||||
|
||||
if (pk->af == AF_INET && orig_src != 0 &&
|
||||
pskb_may_pull(pk->skb, sizeof(struct iphdr)) &&
|
||||
(ip_hdr(pk->skb)->saddr != orig_src ||
|
||||
ip_hdr(pk->skb)->daddr != orig_dst))
|
||||
pfk_fix_ip_csum(pk->skb);
|
||||
|
||||
|
||||
if (m == m0) {
|
||||
/* same buffer, possibly rewritten in place: our wrapper
|
||||
* held one extra skb reference — release it */
|
||||
m_freem(m);
|
||||
if (action == PF_PASS && orig_dst != 0 &&
|
||||
pk->af == AF_INET && pk->dir == PF_IN &&
|
||||
pskb_may_pull(pk->skb, sizeof(struct iphdr)) &&
|
||||
ip_hdr(pk->skb)->daddr != orig_dst) {
|
||||
/* pf translated the destination (reverse NAT):
|
||||
* re-route instead of delivering locally */
|
||||
pfk_reroute(pk->skb);
|
||||
kfree(pk);
|
||||
return;
|
||||
}
|
||||
if (action == PF_PASS) {
|
||||
pfk_pkt_reinject(pk);
|
||||
return;
|
||||
|
||||
+25
-3
@@ -49,6 +49,9 @@ pass in quick proto icmp from $CLI_IP to 10.0.1.1 divert-to "10.0.1.1" port 1000
|
||||
pass out quick
|
||||
CONF
|
||||
|
||||
EGRESS_IF=$(ssh pf0 "ip route get $SRV_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2" 2>/dev/null | tr -d '\r')
|
||||
[ -n "$EGRESS_IF" ] || EGRESS_IF=eth2
|
||||
|
||||
scp -q "$CONF" "$DIV" pf0:/tmp/ 2>/dev/null
|
||||
scp -q tools/out/pdivert pf0:/tmp/pdivert 2>/dev/null
|
||||
|
||||
@@ -89,11 +92,30 @@ p0 "/opt/pf/pfctl -s Tables"
|
||||
echo
|
||||
|
||||
bold "--- 6. the drop rule actually drops ---"
|
||||
vmc "ping -c2 -W2 10.0.9.99 | tail -3"
|
||||
p0 "cat > /tmp/demo-block.conf <<EOF
|
||||
block drop in quick proto icmp to $SRV_IP
|
||||
pass in quick
|
||||
pass out quick
|
||||
EOF"
|
||||
p0 "/opt/pf/pfctl -f /tmp/demo-block.conf && echo 'block rules loaded'"
|
||||
vmc "ping -c2 -W2 $SRV_IP | tail -3"
|
||||
p0 "/opt/pf/pfctl -sr -vv | head -6"
|
||||
p0 "/opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'back to the stateful ruleset'"
|
||||
echo
|
||||
|
||||
bold "--- 7. divert-to: the packet goes to userspace and comes back ---"
|
||||
bold "--- 7. NAT: the source is translated on the way out, restored on the way back ---"
|
||||
p0 "cat > /tmp/demo-nat.conf <<EOF
|
||||
pass out quick on egress from 10.0.1.0/24 to any nat-to ($EGRESS_IF)
|
||||
pass in quick on ingress proto icmp keep state
|
||||
pass out quick on egress proto icmp keep state
|
||||
EOF"
|
||||
p0 "/opt/pf/pfctl -f /tmp/demo-nat.conf && echo 'nat rules loaded'"
|
||||
vmc "ping -c2 -W2 $SRV_IP | tail -3"
|
||||
p0 "/opt/pf/pfctl -ss"
|
||||
p0 "/opt/pf/pfctl -f /tmp/demo-pf.conf && echo 'back to the stateful ruleset'"
|
||||
echo
|
||||
|
||||
bold "--- 8. divert-to: the packet goes to userspace and comes back ---"
|
||||
p0 "cat /tmp/demo-divert.conf"
|
||||
p0 "/opt/pf/pfctl -f /tmp/demo-divert.conf && echo 'divert rule loaded'"
|
||||
p0 "chmod +x /tmp/pdivert; (setsid /tmp/pdivert -p 10000 -f >/tmp/pdivert.log 2>&1 </dev/null &); sleep 1; pgrep pdivert"
|
||||
@@ -102,7 +124,7 @@ p0 "head -3 /tmp/pdivert.log"
|
||||
echo " (pdivert flipped the last payload byte and fixed the ICMP checksum)"
|
||||
echo
|
||||
|
||||
bold "--- 8. shutdown ---"
|
||||
bold "--- 9. shutdown ---"
|
||||
p0 "pkill pdivert; /opt/pf/pfctl -f /dev/null; /opt/pf/pfctl -d; echo 'pf disabled'"
|
||||
p0 "lsmod | grep '^pf '"
|
||||
echo
|
||||
|
||||
@@ -34,7 +34,7 @@ ANC=$(pfput /tmp/g4anc.conf) || { bad "anchor conf upload"; finish; }
|
||||
pf0 "/opt/pf/pfctl -a restricted -f $ANC" || { bad "anchor ruleset load"; finish; }
|
||||
|
||||
pf0 '/opt/pf/pfctl -sr' | grep -q 'anchor "restricted"' && ok "anchor rule in main ruleset" || bad "anchor rule missing"
|
||||
pf0 '/opt/pf/pfctl -a restricted -sr' | grep -q 'keep state' && ok "anchor rules listed" || bad "anchor rules missing"
|
||||
pf0 '/opt/pf/pfctl -a restricted -sr' | grep -q 'proto icmp from' && ok "anchor rules listed" || bad "anchor rules missing"
|
||||
|
||||
# the request must pass through the anchor (the block rule would drop it)
|
||||
cli "ping -c2 -W2 $SRV_IP >/dev/null 2>&1" && ok "anchor pass honoured (request allowed)" \
|
||||
|
||||
+8
-3
@@ -9,21 +9,26 @@
|
||||
set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh
|
||||
|
||||
FAILED=0
|
||||
EGRESS_IP=$(pf0 'ip -4 addr show egress | grep -o "inet [0-9.]*" | head -1 | cut -d" " -f2')
|
||||
# pf calls the interface "egress"; the OS names it ethX - resolve both
|
||||
EGRESS_DEV=$(pf0 "ip route get $SRV_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2")
|
||||
EGRESS_IP=$(pf0 "ip -4 addr show $EGRESS_DEV | grep -o 'inet [0-9.]*' | head -1 | cut -d' ' -f2")
|
||||
[ -n "$EGRESS_IP" ] || EGRESS_IP=192.168.122.8
|
||||
|
||||
pf0 'insmod /opt/pf/pf.ko 2>/dev/null || insmod /tmp/pf.ko; true'
|
||||
pf0 '/opt/pf/pfctl -e; /opt/pf/pfctl -f /dev/null' >/dev/null
|
||||
|
||||
# OpenBSD expands "(egress)" (an interface *group*) into the addresses of all
|
||||
# its members - on a multi-NIC host that is ambiguous, so the gate uses the
|
||||
# concrete transit interface (the OpenBSD-recommended form for NAT pools).
|
||||
cat > /tmp/g05.conf <<C
|
||||
pass out quick on egress from 10.0.1.0/24 to any nat-to (egress)
|
||||
pass out quick on egress from 10.0.1.0/24 to any nat-to ($EGRESS_DEV)
|
||||
pass in quick on ingress proto icmp keep state
|
||||
pass out quick on egress proto icmp keep state
|
||||
C
|
||||
pfload /tmp/g05.conf || { bad "nat rule load"; finish; }
|
||||
pf0 '/opt/pf/pfctl -sr' | grep -q 'nat-to' && ok "nat-to rule listed" || bad "nat-to not listed"
|
||||
|
||||
pf0 'tcpdump -ni egress -c 4 -e icmp > /tmp/nat.td 2>&1 &'
|
||||
pf0 "tcpdump -ni $EGRESS_DEV -c 4 -e icmp > /tmp/nat.td 2>&1 &"
|
||||
sleep 1
|
||||
cli "ping -c2 -W2 $SRV_IP >/dev/null 2>&1" && ok "transit ping through NAT" || bad "transit ping"
|
||||
sleep 2
|
||||
|
||||
+24
-23
@@ -1,50 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
# M6 gate: soak + unload/leak discipline.
|
||||
#
|
||||
# 1) load pf, ruleset with states/tables/anchors, drive mixed traffic ~20s
|
||||
# 1) load pf with a stateful ruleset, drive mixed traffic for ~20s
|
||||
# 2) assert no BUG/WARNING/Oops in dmesg
|
||||
# 3) disable, rmmod; assert module unloads cleanly (no refcount hang)
|
||||
# 4) insmod/rmmod cycle x2; assert slab objects for pf caches return to the
|
||||
# pre-load baseline (no leak), and that a reload works (timer/notifier
|
||||
# re-registration is idempotent)
|
||||
# 3) disable + rmmod: the module must unload cleanly (no refcount leak)
|
||||
# 4) insmod/rmmod cycles: slab objects for the pf caches must return to the
|
||||
# pre-load baseline (no leak) and a reload must work
|
||||
set -uo pipefail; cd "$(dirname "$0")"; . ./lib.sh
|
||||
|
||||
FAILED=0
|
||||
slab_snapshot() {
|
||||
pf0 "grep -E '^(pf|pfk)' /proc/slabinfo | awk '{print \$1, \$3}'" | sort
|
||||
}
|
||||
HELPER=$(pfput ./soak-helper.sh) || { bad "helper upload"; finish; }
|
||||
pf0 "chmod +x $HELPER" >/dev/null
|
||||
|
||||
pf0 'insmod /tmp/pf.ko 2>/dev/null || insmod /opt/pf/pf.ko; true'
|
||||
pf0 'insmod /opt/pf/pf.ko 2>/dev/null || insmod /tmp/pf.ko; true'
|
||||
pf0 '/opt/pf/pfctl -e; /opt/pf/pfctl -f /dev/null' >/dev/null
|
||||
BEFORE=$(slab_snapshot)
|
||||
BEFORE=$(pf0 "$HELPER slab")
|
||||
|
||||
cat > /tmp/g14.conf <<C
|
||||
table <peers> { $CLI_IP }
|
||||
anchor "s"
|
||||
pass in quick on ingress proto icmp from <peers> keep state
|
||||
pass out quick proto icmp keep state
|
||||
pass out quick on egress proto icmp keep state
|
||||
pass out quick proto tcp from any to any port 22 keep state
|
||||
C
|
||||
pfload /tmp/g14.conf || { bad soak ruleset; finish; }
|
||||
pfload /tmp/g14.conf || { bad "soak ruleset"; finish; }
|
||||
|
||||
# mixed traffic: ICMP + TCP handshakes, 20s
|
||||
cli "ping -q -c 30 -i 0.2 $SRV_IP >/dev/null 2>&1 &" 2>/dev/null
|
||||
# mixed traffic: ICMP + TCP handshakes for ~20s
|
||||
cli "ping -q -c 40 -i 0.5 $SRV_IP >/dev/null 2>&1" 2>/dev/null
|
||||
cli "for i in 1 2 3 4 5; do nc -w1 -z $SRV_IP 22; done" 2>/dev/null
|
||||
sleep 22
|
||||
ST=$(pf0 '/opt/pf/pfctl -s states | wc -l')
|
||||
|
||||
ST=$(pf0 "$HELPER states")
|
||||
[ "${ST:-0}" -ge 1 ] && ok "states under load ($ST)" || bad "no states under load"
|
||||
|
||||
DMESG=$(pf0 "dmesg | grep -cE 'BUG:|WARNING:|Oops|general protection'")
|
||||
[ "${DMESG:-1}" = 0 ] && ok "no kernel BUG/WARN/Oops" || bad "dmesg anomalies ($DMESG)"
|
||||
ANOM=$(pf0 "$HELPER dmesg")
|
||||
if [ -z "$ANOM" ]; then
|
||||
ok "no kernel BUG/WARN/Oops"
|
||||
else
|
||||
bad "dmesg anomalies:"
|
||||
printf '%s\n' "$ANOM" | sed 's/^/ /'
|
||||
fi
|
||||
|
||||
pf0 '/opt/pf/pfctl -d; /opt/pf/pfctl -f /dev/null' >/dev/null
|
||||
pf0 'rmmod pf && echo UNLOAD_OK' | grep -q UNLOAD_OK && ok "clean unload (refcounts released)" || bad "rmmod failed"
|
||||
sleep 1
|
||||
pf0 'insmod /tmp/pf.ko 2>/dev/null || insmod /opt/pf/pf.ko; true; (/opt/pf/pfctl -s info >/dev/null && echo RELOAD_OK)' | grep -q RELOAD_OK && ok "reload after unload" || bad "reload failed"
|
||||
pf0 'insmod /opt/pf/pf.ko 2>/dev/null || insmod /tmp/pf.ko; true; (/opt/pf/pfctl -s info >/dev/null && echo RELOAD_OK)' \
|
||||
| grep -q RELOAD_OK && ok "reload after unload" || bad "reload failed"
|
||||
pf0 'rmmod pf && echo UNLOAD_OK2' | grep -q UNLOAD_OK2 && ok "second unload" || bad "second rmmod"
|
||||
sleep 1
|
||||
AFTER=$(slab_snapshot)
|
||||
# compare per-cache object counts if they exist on both sides
|
||||
AFTER=$(pf0 "$HELPER slab")
|
||||
LEAK=$(join <(echo "$BEFORE") <(echo "$AFTER") 2>/dev/null | awk '$3-$2 > 0 {print $1, $3-$2}')
|
||||
if [ -n "$LEAK" ]; then bad "slab growth after cycles: $LEAK"; else ok "no slab growth after unload cycles"; fi
|
||||
finish
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
# Helper executed ON pf0 by 14-soak.sh (uploaded with pfput to avoid quoting
|
||||
# hell through two ssh hops).
|
||||
# $1 = "slab" | "dmesg" | "states"
|
||||
case "${1:-}" in
|
||||
slab)
|
||||
grep -E '^(pf|pfk)' /proc/slabinfo | tr -s ' ' | cut -d' ' -f1,3 | sort
|
||||
;;
|
||||
dmesg)
|
||||
# the CPU's Speculative Return Stack Overflow advisory is a kernel
|
||||
# build/CPU feature notice, not a fault of this module
|
||||
dmesg | grep -E 'BUG:|WARNING:|Oops|general protection|kernel BUG' \
|
||||
| grep -v 'Speculative Return Stack Overflow' || true
|
||||
;;
|
||||
states)
|
||||
/opt/pf/pfctl -s states 2>/dev/null | grep -c 'icmp' || true
|
||||
;;
|
||||
esac
|
||||
@@ -1562,6 +1562,84 @@ ifa_exists(const char *ifa_name)
|
||||
return (NULL);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* PFLINUX: interface group emulation.
|
||||
*
|
||||
* OpenBSD creates the "ingress" and "egress" interface groups (ifgroups)
|
||||
* and pfctl expands "(egress)" in a pool into the addresses of the group
|
||||
* members via the SIOCGIFGMEMB socket ioctl. Linux has neither ifgroups
|
||||
* nor that ioctl, so resolve the two groups locally:
|
||||
*
|
||||
* egress -> the interface of the default route (the same interface the
|
||||
* kernel module marks as the pf "egress" interface)
|
||||
* ingress -> every non-loopback interface
|
||||
*/
|
||||
static struct node_host *
|
||||
ifa_group_members(const char *group, int flags)
|
||||
{
|
||||
struct node_host *n, *h = NULL, *c;
|
||||
struct sockaddr_in sin;
|
||||
socklen_t slen = sizeof(sin);
|
||||
int s;
|
||||
|
||||
if (iftab == NULL)
|
||||
ifa_load();
|
||||
|
||||
if (!strcmp(group, "egress")) {
|
||||
/* source address of the default route */
|
||||
memset(&sin, 0, sizeof(sin));
|
||||
sin.sin_family = AF_INET;
|
||||
sin.sin_port = htons(9);
|
||||
sin.sin_addr.s_addr = htonl(0x01010101); /* discard route */
|
||||
if ((s = socket(AF_INET, SOCK_DGRAM, 0)) != -1) {
|
||||
if (connect(s, (struct sockaddr *)&sin,
|
||||
sizeof(sin)) == 0 &&
|
||||
getsockname(s, (struct sockaddr *)&sin, &slen) == 0) {
|
||||
close(s);
|
||||
for (n = iftab; n; n = n->next) {
|
||||
if (n->af != AF_INET)
|
||||
continue;
|
||||
if (n->addr.v.a.addr.v4.s_addr !=
|
||||
sin.sin_addr.s_addr)
|
||||
continue;
|
||||
if ((c = calloc(1, sizeof(*c))) == NULL)
|
||||
err(1, "calloc");
|
||||
*c = *n;
|
||||
c->next = NULL;
|
||||
c->tail = c;
|
||||
return (c);
|
||||
}
|
||||
return (NULL);
|
||||
}
|
||||
close(s);
|
||||
}
|
||||
return (NULL);
|
||||
}
|
||||
|
||||
/* "ingress" and any other group: every non-loopback interface */
|
||||
for (n = iftab; n; n = n->next) {
|
||||
if (n->af == AF_LINK)
|
||||
continue;
|
||||
if (n->ifa_flags & IFF_LOOPBACK)
|
||||
continue;
|
||||
if ((flags & PFI_AFLAG_BROADCAST) && n->af != AF_INET)
|
||||
continue;
|
||||
if ((c = calloc(1, sizeof(*c))) == NULL)
|
||||
err(1, "calloc");
|
||||
*c = *n;
|
||||
c->next = NULL;
|
||||
c->tail = c;
|
||||
if (h == NULL)
|
||||
h = c;
|
||||
else {
|
||||
h->tail->next = c;
|
||||
h->tail = c->tail;
|
||||
}
|
||||
}
|
||||
return (h);
|
||||
}
|
||||
|
||||
struct node_host *
|
||||
ifa_grouplookup(const char *ifa_name, int flags)
|
||||
{
|
||||
@@ -1576,6 +1654,10 @@ ifa_grouplookup(const char *ifa_name, int flags)
|
||||
strlcpy(ifgr.ifgr_name, ifa_name, sizeof(ifgr.ifgr_name));
|
||||
if (ioctl(s, SIOCGIFGMEMB, (caddr_t)&ifgr) == -1) {
|
||||
close(s);
|
||||
/* PFLINUX: interface group emulation (see above) */
|
||||
if (!strcmp(ifa_name, "ingress") ||
|
||||
!strcmp(ifa_name, "egress"))
|
||||
return (ifa_group_members(ifa_name, flags));
|
||||
return (NULL);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user