1 Commits
Author SHA1 Message Date
lgcosta fe324259c0 Screencasts: seis videos silenciosos 1080p30 da demonstracao
Novo pipeline de video: scripts/screencast/render.py roda uma fase em um pty,
emula o terminal com pyte, gera frames PNG e mux com ffmpeg (H.264,
yuv420p, +faststart, sem trilha de audio). Fallback de encoder: libx264 ->
libopenh264 -> VP9.

Fases (scripts/screencast/phases/, uma sessao real contra o lab por video):
  1. implementacao no kernel - fontes OpenBSD + compat + build limpo do pf.ko
  2. ativacao - insmod, /dev/pf, pfctl -e, pfctl -s info
  3. regras basicas - tabela, anchor, keep state, block drop
  4. roteamento - route-to e reply-to (provado com tcpdump nas 2 interfaces)
  5. balanceamento - pool round-robin, pool source-hash, dup-to
  6. QOS - filas hfsc, pfctl -s queue -v, transferencia de 2 MB pela fila

Os videos ficam versionados em out/screencast/ (.gitignore ajustado para
nao ignorar *.mp4) e o indice/texto de descricao do YouTube em
docs/{en,pt-BR}/screencast.md.

Correcoes na propria demo (encontradas gravando): a acao de uma regra no pf
e pass OU block (nunca os dois); as regras de um anchor vivem em um ruleset
proprio (pfctl -a <anchor> -f); o nc do QOS precisa rodar na VM do servidor,
nao no firewall; o rotulo 'FASE x/6' vem do numero da fase, nao da posicao
na lista de gravacao.
2026-11-12 08:41:41 -03:00
20 changed files with 958 additions and 2 deletions

No files matched your search

+3 -2
View File
@@ -1,6 +1,8 @@
_vendor/
out/
out/*
!out/demo-pf-linux.cast
!out/screencast/
!out/screencast/*.mp4
*.o
*.ko
*.mod
@@ -11,7 +13,6 @@ out/
modules.order
Module.symvers
tools/out/
!out/demo-pf-linux.cast
lab/seeds/
*.pcap
*.log
+14
View File
@@ -45,6 +45,20 @@ Not in scope: `pfsync`/CARP failover, IPsec/pfkey, `ftp-proxy`, per-netns PF
instances, classic-ALTQ-only schedulers (cbq/wfq/red — removed upstream before
OpenBSD 8.0).
## Screencasts
Six silent 1080p30 MP4 screencasts of the lab — implementation in the kernel,
activation, basic rules, routing, balancing and QOS — each one a real session
against the Alpine VMs:
```sh
./scripts/screencast/record-all.sh # -> out/screencast/*.mp4
```
Details, per-phase breakdown and the YouTube description text:
[`docs/en/screencast.md`](docs/en/screencast.md) ·
[`docs/pt-BR/screencast.md`](docs/pt-BR/screencast.md)
## Quick start
Full recipe: [`docs/en/install.md`](docs/en/install.md). Short version
+70
View File
@@ -0,0 +1,70 @@
# Screencasts / Videos da demonstracao
> PT-BR: [docs/pt-BR/screencast.md](pt-BR/screencast.md) · EN: this page
Seis videos silenciosos, 1920x1080 @ 30fps, H.264/MP4 — prontos para subir no
YouTube. Cada video e a gravacao de uma sessao **real** contra o lab: VMs
Alpine sob libvirt, rodando o kernel 7.3.4 compilado do fonte com o `pf.ko`
deste repositorio. Nada e simulado; a unica coisa roteirizada e o ritmo.
| # | Video | Duracao | O que mostra |
|---|-------|---------|--------------|
| 1 | `01-implementacao-kernel.mp4` | 22 s | A fonte do OpenBSD 8.0 organizada em `module/pfsrc`, a camada de compatibilidade em `module/compat`, e a compilacao do modulo `pf.ko` contra o kernel do lab |
| 2 | `02-ativacao.mp4` | 29 s | `insmod pf.ko`, `/dev/pf` aparecendo, `pfctl -e`, `pfctl -s info` com a tabela de estados |
| 3 | `03-regras-basicas.mp4` | 54 s | Um `pf.conf` real: tabela, anchor, `quick`, `keep state`, `block drop` (o ping morre), contadores por regra e `pfctl -ss` |
| 4 | `04-roteamento.mp4` | 47 s | `route-to` (o pacote sai pelo caminho forcado, provado com `tcpdump` nas duas interfaces) e `reply-to` (resposta por gateway diferente) |
| 5 | `05-balanceamento.mp4` | 69 s | Pool `nat-to { ... } round-robin` alternando a origem no fio, pool `source-hash` fixando por origem, e `dup-to` replicando o pacote |
| 6 | `06-qos.mp4` | 34 s | Filas do OpenBSD 8.0 (`queue ... bandwidth`, classes `hfsc`), `pfctl -s queue -v`, transferencia de 2 MB pela fila e os contadores da classe subindo |
## Como regravar
```sh
# renderiza todas as fases
./scripts/screencast/record-all.sh
# ou uma fase especifica
./scripts/screencast/record-all.sh 05-balanceamento
```
Os arquivos sao gravados em `out/screencast/*.mp4`. O renderizador
(`scripts/screencast/render.py`) roda a fase em um pty, emula o terminal com
`pyte`, converte cada evento em um frame PNG e mux com `ffmpeg` (H.264,
`yuv420p`, `+faststart`, **sem trilha de audio**). Se o `ffmpeg` da maquina nao
tiver `libx264`, ele cai para `libopenh264` e depois para VP9 automaticamente.
Requisitos: `ffmpeg`, `python3` com `PIL` e `pyte`, a fonte DejaVu Sans Mono, e
o lab de VMs no ar (as fases 2 a 6 falam com o lab; a fase 1 roda no host de
build).
## Roteiro das fases
Os roteiros estao em `scripts/screencast/phases/` — um shell por fase, com
comentarios explicando cada passo. Eles usam os mesmos comandos que os gates
da suite (`tests/vm/*.sh`), entao o que o video mostra e exatamente o que a
suite verifica.
## Texto de descricao (YouTube)
```
PF on Linux - o motor do OpenBSD PF 8.0 dentro do kernel Linux
Port completo do Packet Filter do OpenBSD 8.0 para o kernel Linux 7.3.4,
empacotado como modulo (pf.ko) e controlada pelo pfctl original do OpenBSD
sobre a ABI de ioctls do /dev/pf.
O lab e real: 4 VMs Alpine sob libvirt, kernel compilado do fonte com o
modulo, trafego real entre cliente, firewall e servidor.
Fases:
1. Implementacao no kernel - das fontes OpenBSD ao pf.ko
2. Ativacao - insmod, /dev/pf, pfctl -e, pfctl -s info
3. Regras basicas - tabelas, anchors, keep state, block drop
4. Roteamento - route-to e reply-to
5. Balanceamento - pools round-robin, source-hash e dup-to
6. QOS - filas hfsc do OpenBSD 8.0 no caminho de dados
Suite automatizada: 15 gates, todos verdes.
Codigo e documentacao: https://git.labexposed.com/lgcosta/pf-linux-port
#linux #kernel #openbsd #pf #firewall #netfilter #screencast
```
+70
View File
@@ -0,0 +1,70 @@
# Screencasts / Videos da demonstracao
> PT-BR: esta pagina · EN: [docs/en/screencast.md](en/screencast.md)
Seis videos silenciosos, 1920x1080 @ 30fps, H.264/MP4 — prontos para subir no
YouTube. Cada video e a gravacao de uma sessao **real** contra o lab: VMs
Alpine sob libvirt, rodando o kernel 7.3.4 compilado do fonte com o `pf.ko`
deste repositorio. Nada e simulado; a unica coisa roteirizada e o ritmo.
| # | Video | Duracao | O que mostra |
|---|-------|---------|--------------|
| 1 | `01-implementacao-kernel.mp4` | 22 s | A fonte do OpenBSD 8.0 organizada em `module/pfsrc`, a camada de compatibilidade em `module/compat`, e a compilacao do modulo `pf.ko` contra o kernel do lab |
| 2 | `02-ativacao.mp4` | 29 s | `insmod pf.ko`, `/dev/pf` aparecendo, `pfctl -e`, `pfctl -s info` com a tabela de estados |
| 3 | `03-regras-basicas.mp4` | 54 s | Um `pf.conf` real: tabela, anchor, `quick`, `keep state`, `block drop` (o ping morre), contadores por regra e `pfctl -ss` |
| 4 | `04-roteamento.mp4` | 47 s | `route-to` (o pacote sai pelo caminho forcado, provado com `tcpdump` nas duas interfaces) e `reply-to` (resposta por gateway diferente) |
| 5 | `05-balanceamento.mp4` | 69 s | Pool `nat-to { ... } round-robin` alternando a origem no fio, pool `source-hash` fixando por origem, e `dup-to` replicando o pacote |
| 6 | `06-qos.mp4` | 34 s | Filas do OpenBSD 8.0 (`queue ... bandwidth`, classes `hfsc`), `pfctl -s queue -v`, transferencia de 2 MB pela fila e os contadores da classe subindo |
## Como regravar
```sh
# renderiza todas as fases
./scripts/screencast/record-all.sh
# ou uma fase especifica
./scripts/screencast/record-all.sh 05-balanceamento
```
Os arquivos sao gravados em `out/screencast/*.mp4`. O renderizador
(`scripts/screencast/render.py`) roda a fase em um pty, emula o terminal com
`pyte`, converte cada evento em um frame PNG e mux com `ffmpeg` (H.264,
`yuv420p`, `+faststart`, **sem trilha de audio**). Se o `ffmpeg` da maquina nao
tiver `libx264`, ele cai para `libopenh264` e depois para VP9 automaticamente.
Requisitos: `ffmpeg`, `python3` com `PIL` e `pyte`, a fonte DejaVu Sans Mono, e
o lab de VMs no ar (as fases 2 a 6 falam com o lab; a fase 1 roda no host de
build).
## Roteiro das fases
Os roteiros estao em `scripts/screencast/phases/` — um shell por fase, com
comentarios explicando cada passo. Eles usam os mesmos comandos que os gates
da suite (`tests/vm/*.sh`), entao o que o video mostra e exatamente o que a
suite verifica.
## Texto de descricao (YouTube)
```
PF on Linux - o motor do OpenBSD PF 8.0 dentro do kernel Linux
Port completo do Packet Filter do OpenBSD 8.0 para o kernel Linux 7.3.4,
empacotado como modulo (pf.ko) e controlada pelo pfctl original do OpenBSD
sobre a ABI de ioctls do /dev/pf.
O lab e real: 4 VMs Alpine sob libvirt, kernel compilado do fonte com o
modulo, trafego real entre cliente, firewall e servidor.
Fases:
1. Implementacao no kernel - das fontes OpenBSD ao pf.ko
2. Ativacao - insmod, /dev/pf, pfctl -e, pfctl -s info
3. Regras basicas - tabelas, anchors, keep state, block drop
4. Roteamento - route-to e reply-to
5. Balanceamento - pools round-robin, source-hash e dup-to
6. QOS - filas hfsc do OpenBSD 8.0 no caminho de dados
Suite automatizada: 15 gates, todos verdes.
Codigo e documentacao: https://git.labexposed.com/lgcosta/pf-linux-port
#linux #kernel #openbsd #pf #firewall #netfilter #screencast
```
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# Fase 1 - a implementacao no kernel: da fonte OpenBSD ao modulo pf.ko.
# Roda no host de build (o port esta no repositorio; a VM so recebe o .ko).
set -u
cd "$(dirname "$0")"
. ./lib.sh
banner "FASE 1 - A IMPLEMENTACAO NO KERNEL"
bold "# o port: fontes BSD trazidos do OpenBSD 8.0 + camada de compatibilidade"
dim " module/pfsrc/ - fontes do OpenBSD (pf.c, pf_lb.c, pf_norm.c, hfsc.c, ...)"
dim " module/compat/ - shims que traduzem as APIs BSD para o Linux"
dim " module/include/ - cabecalhos (pfk.h) e os headers vendorizados do OpenBSD"
echo
cyan "host# ls module/pfsrc | head -20"
ls "$REPO/module/pfsrc" | head -20
echo
cyan "host# wc -l module/pfsrc/*.c | tail -3"
(cd "$REPO" && wc -l module/pfsrc/*.c | tail -3)
cyan "host# wc -l module/compat/*.c | tail -1"
(cd "$REPO" && wc -l module/compat/*.c | tail -1)
echo
dim " (as fontes OpenBSD entram praticamente intactas; a compat faz a ponte"
dim " mbuf->sk_buff, spl->spinlock, epoch->RCU, ifnet->net_device, ...)"
bold
bold "# compilando o modulo contra o kernel do lab (build limpo)"
echo
cyan "host# ./scripts/build-module.sh clean"
(cd "$REPO" && ./scripts/build-module.sh clean 2>&1 | tail -3)
cyan "host# ./scripts/build-module.sh"
(cd "$REPO" && ./scripts/build-module.sh 2>&1 | tail -18)
echo
cyan "host# ls -la module/pf.ko"
ls -la "$REPO/module/pf.ko"
cyan "host# modinfo module/pf.ko | head -5"
modinfo "$REPO/module/pf.ko" 2>/dev/null | head -5 || \
(cd "$REPO" && strings module/pf.ko | grep -m2 vermagic)
echo
green " modulo pronto: o motor PF 8.0 do OpenBSD pronto para carregar no Linux"
+30
View File
@@ -0,0 +1,30 @@
#!/usr/bin/env bash
# Fase 2 - ativacao: carregar pf.ko, habilitar o firewall, ver o estado.
set -u
cd "$(dirname "$0")"
. ./lib.sh
banner "FASE 2 - ATIVACAO"
bold "# o modulo ainda nao esta carregado"
p0 "rmmod pf 2>/dev/null; lsmod | grep '^pf ' || echo 'pf: not loaded'"
p0 "ls -l /dev/pf 2>/dev/null || echo '/dev/pf: ausente enquanto pf nao carrega'"
echo
bold "# carregando o motor PF no kernel"
p0 "insmod /opt/pf/pf.ko && echo 'pf.ko carregado no kernel'"
p0 "dmesg | tail -4"
p0 "lsmod | grep '^pf '"
p0 "ls -l /dev/pf"
echo
bold "# habilitando o firewall"
p0 "/opt/pf/pfctl -e && echo 'pf habilitado'"
p0 "/opt/pf/pfctl -si | head -6"
echo
bold "# o ruleset comeca vazio (a politica padrao do pf e bloquear, com return)"
p0 "/opt/pf/pfctl -sr"
p0 "/opt/pf/pfctl -sf | head -5"
echo
green " motor ativo: /dev/pf respondendo, pfctl sobre a ABI de ioctl do OpenBSD"
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env bash
# Fase 3 - regras basicas: tabelas, anchors, keep state, block/pass.
set -u
cd "$(dirname "$0")"
. ./lib.sh
banner "FASE 3 - REGRAS BASICAS"
p0 "/opt/pf/pfctl -e"
p0 "/opt/pf/pfctl -f /dev/null"
bold "# um pf.conf de verdade, sintaxe do OpenBSD"
cat > /tmp/sc-regras.conf <<'CONF'
# --- regras basicas do PF ---
set block-policy return
table <trusted> { 10.0.1.0/24 }
anchor "demo"
pass in quick on ingress proto icmp from <trusted> keep state
pass out quick on egress proto icmp keep state
pass in quick
pass out quick
CONF
put /tmp/sc-regras.conf >/dev/null
p0 "cat /tmp/sc-regras.conf"
p0 "/opt/pf/pfctl -f /tmp/sc-regras.conf && echo 'ruleset carregado'"
echo
bold "# o que o pf entendeu"
p0 "/opt/pf/pfctl -sr"
p0 "/opt/pf/pfctl -s Tables"
p0 "/opt/pf/pfctl -t trusted -T show"
echo
bold "# trafego permitido: passa e cria estado"
vmc "ping -c2 -W2 $SRV_IP | tail -3"
p0 "/opt/pf/pfctl -ss | head -5"
echo
bold "# a mesma regra com 'block drop' bloqueia de verdade"
cat > /tmp/sc-block.conf <<CONF
set block-policy return
table <trusted> { 10.0.1.0/24 }
block drop in quick on ingress proto icmp from <trusted>
pass in quick
pass out quick
CONF
put /tmp/sc-block.conf >/dev/null
p0 "/opt/pf/pfctl -f /tmp/sc-block.conf && echo 'ruleset de bloqueio carregado'"
vmc "ping -c2 -W2 $SRV_IP | tail -3"
p0 "/opt/pf/pfctl -sr -vv | head -4"
echo
bold "# anchors: regras aninhadas, como no OpenBSD"
p0 "cat > /tmp/sc-anchor.conf <<'EOF'
pass in quick on ingress proto icmp from 10.0.1.10 to 10.0.2.20 keep state
pass in quick
pass out quick
EOF"
p0 "/opt/pf/pfctl -a demo -f /tmp/sc-anchor.conf && echo 'ruleset do anchor carregado'"
p0 "/opt/pf/pfctl -a demo -sr"
p0 "cat > /tmp/sc-main.conf <<'EOF'
anchor \"demo\"
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/sc-main.conf && echo 'ruleset principal com anchor carregado'"
p0 "/opt/pf/pfctl -sr"
echo
bold "# voltando ao ruleset com estado"
p0 "/opt/pf/pfctl -f /tmp/sc-regras.conf && echo 'ruleset stateful restaurado'"
vmc "ping -c2 -W2 $SRV_IP | tail -2"
green " tabelas, anchors, quick, keep state, block drop: todos no lugar"
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# Fase 4 - roteamento: route-to (caminho forcado) e reply-to (assimetria).
set -u
cd "$(dirname "$0")"
. ./lib.sh
banner "FASE 4 - ROTEAMENTO"
p0 "/opt/pf/pfctl -e"
p0 "/opt/pf/pfctl -f /dev/null"
put ./an.sh >/dev/null
IF_SRV=$(onvm "$PF0_MGMT" "ip route get $SRV_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2" | tr -d '\r')
IF_GW=$(onvm "$PF0_MGMT" "ip route get $GW2_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2" | tr -d '\r')
[ -n "$IF_SRV" ] || IF_SRV=eth2
[ -n "$IF_GW" ] || IF_GW=eth3
dim " (o servidor fica atras de $IF_SRV; o segundo gateway, em $IF_GW)"
bold "# route-to: o pacote sai pelo caminho forcado, nao pelo normal"
cat > /tmp/sc-route.conf <<CONF
pass in quick proto icmp from $CLI_IP to $SRV_IP route-to { $GW2_IP } keep state
pass out quick
CONF
put /tmp/sc-route.conf >/dev/null
p0 "/opt/pf/pfctl -f /tmp/sc-route.conf && echo 'regra route-to carregada'"
p0 "/opt/pf/pfctl -sr"
p0 "pkill tcpdump 2>/dev/null; (tcpdump -ni $IF_GW -c 4 -e icmp > /tmp/sc_gw.td 2>&1 &); (tcpdump -ni $IF_SRV -c 4 -e icmp > /tmp/sc_srv.td 2>&1 &); sleep 1"
vmc "ping -c2 -W2 $SRV_IP | tail -2"
dim " (o ping falha de proposito: o pacote foi entregue ao gateway $GW2_IP)"
sleep 3
p0 "echo \"egresso forcado ($IF_GW):\"; sh /tmp/an.sh /tmp/sc_gw.td req"
p0 "echo \"egresso normal ($IF_SRV):\"; sh /tmp/an.sh /tmp/sc_srv.td req"
echo
bold "# reply-to: a resposta volta por um caminho diferente do pedido"
cat > /tmp/sc-reply.conf <<CONF
pass in quick proto icmp from $CLI_IP to $SRV_IP reply-to { $GW2_IP } keep state
pass out quick
CONF
put /tmp/sc-reply.conf >/dev/null
p0 "/opt/pf/pfctl -f /tmp/sc-reply.conf && echo 'regra reply-to carregada'"
p0 "/opt/pf/pfctl -sr"
p0 "pkill tcpdump 2>/dev/null; (tcpdump -ni $IF_GW -c 6 -e icmp > /tmp/sc_rgw.td 2>&1 &); (tcpdump -ni eth1 -c 6 -e icmp > /tmp/sc_rcli.td 2>&1 &); sleep 1"
vmc "ping -c2 -W2 $SRV_IP | tail -2"
dim " (de novo sem resposta direta: a resposta foi forcada para o gateway)"
sleep 3
p0 "echo \"resposta forcada ($IF_GW):\"; sh /tmp/an.sh /tmp/sc_rgw.td rep"
p0 "echo \"resposta pelo caminho normal do cliente:\"; sh /tmp/an.sh /tmp/sc_rcli.td rep"
echo
bold "# rota normal restaurada"
p0 "/opt/pf/pfctl -f /dev/null; /opt/pf/pfctl -sr"
p0 "pkill tcpdump 2>/dev/null; echo 'capturas encerradas'"
green " route-to e reply-to: o proximo salto e decidido pelo pf, nao pela tabela de rotas"
+68
View File
@@ -0,0 +1,68 @@
#!/usr/bin/env bash
# Fase 5 - balanceamento: pools (round-robin, source-hash) e dup-to.
set -u
cd "$(dirname "$0")"
. ./lib.sh
banner "FASE 5 - BALANCEAMENTO"
p0 "/opt/pf/pfctl -e"
p0 "/opt/pf/pfctl -f /dev/null"
put ./an.sh >/dev/null
IF_SRV=$(onvm "$PF0_MGMT" "ip route get $SRV_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2" | tr -d '\r')
IF_GW=$(onvm "$PF0_MGMT" "ip route get $GW2_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2" | tr -d '\r')
[ -n "$IF_SRV" ] || IF_SRV=eth2
[ -n "$IF_GW" ] || IF_GW=eth3
bold "# pool round-robin: a traducao de origem alterna entre os membros do pool"
cat > /tmp/sc-pool.conf <<CONF
pass out quick on egress from 10.0.1.0/24 to any nat-to { 10.0.2.1, 10.0.3.1 } round-robin
pass in quick on ingress proto icmp keep state
pass out quick on egress proto icmp keep state
CONF
put /tmp/sc-pool.conf >/dev/null
p0 "cat /tmp/sc-pool.conf"
p0 "/opt/pf/pfctl -f /tmp/sc-pool.conf && echo 'pool round-robin carregado'"
p0 "/opt/pf/pfctl -sr"
p0 "pkill tcpdump 2>/dev/null; (tcpdump -ni $IF_SRV -c 12 -e icmp > /tmp/sc_pool.td 2>&1 &); sleep 1"
for i in 1 2 3 4 5 6; do
vmc "ping -c1 -W2 $SRV_IP >/dev/null 2>&1; echo ' pacote $i enviado'"
done
sleep 4
p0 "echo 'origem vista pelo servidor (o pool alterna entre os membros):'; sh /tmp/an.sh /tmp/sc_pool.td src"
echo
bold "# pool source-hash: a mesma origem cai sempre no mesmo membro"
cat > /tmp/sc-hash.conf <<CONF
pass out quick on egress from 10.0.1.0/24 to any nat-to { 10.0.2.1, 10.0.3.1 } source-hash
pass in quick on ingress proto icmp keep state
pass out quick on egress proto icmp keep state
CONF
put /tmp/sc-hash.conf >/dev/null
p0 "/opt/pf/pfctl -f /tmp/sc-hash.conf && echo 'pool source-hash carregado'"
p0 "pkill tcpdump 2>/dev/null; (tcpdump -ni $IF_SRV -c 8 -e icmp > /tmp/sc_hash.td 2>&1 &); sleep 1"
for i in 1 2 3 4; do
vmc "ping -c1 -W2 $SRV_IP >/dev/null 2>&1; echo ' pacote $i enviado'"
done
sleep 4
p0 "echo 'origem com source-hash (estavel por origem):'; sh /tmp/an.sh /tmp/sc_hash.td src"
echo
bold "# dup-to: o pacote segue o caminho normal e uma copia vai para o outro gateway"
cat > /tmp/sc-dup.conf <<CONF
pass in quick proto icmp from $CLI_IP to $SRV_IP dup-to { $GW2_IP } keep state
pass out quick
CONF
put /tmp/sc-dup.conf >/dev/null
p0 "/opt/pf/pfctl -f /tmp/sc-dup.conf && echo 'regra dup-to carregada'"
p0 "/opt/pf/pfctl -sr"
p0 "pkill tcpdump 2>/dev/null; (tcpdump -ni $IF_SRV -c 4 -e icmp > /tmp/sc_dup1.td 2>&1 &); (tcpdump -ni $IF_GW -c 4 -e icmp > /tmp/sc_dup2.td 2>&1 &); sleep 1"
vmc "ping -c2 -W2 $SRV_IP | tail -2"
sleep 4
p0 "echo \"caminho normal ($IF_SRV):\"; sh /tmp/an.sh /tmp/sc_dup1.td req"
p0 "echo \"copia espelhada ($IF_GW):\"; sh /tmp/an.sh /tmp/sc_dup2.td req"
echo
p0 "/opt/pf/pfctl -f /dev/null; pkill tcpdump 2>/dev/null"
green " round-robin reparte a carga; source-hash fixa por origem; dup-to replica"
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# Fase 6 - QOS: filas do OpenBSD 8.0 (hfsc) no caminho de dados do Linux.
set -u
cd "$(dirname "$0")"
. ./lib.sh
banner "FASE 6 - QOS (FILAS / ALTQ)"
p0 "/opt/pf/pfctl -e"
p0 "/opt/pf/pfctl -f /dev/null"
IF_SRV=$(onvm "$PF0_MGMT" "ip route get $SRV_IP | grep -o 'dev [a-z0-9]*' | cut -d' ' -f2" | tr -d '\r')
[ -n "$IF_SRV" ] || IF_SRV=eth2
bold "# a sintaxe de filas do OpenBSD 8.0: queue com classes hfsc"
cat > /tmp/sc-q.conf <<CONF
queue rootq on $IF_SRV bandwidth 10Mbit
queue leafq on $IF_SRV bandwidth 2Mbit parent rootq default
pass in quick proto tcp from $CLI_IP to $SRV_IP port 9095 queue leafq keep state
pass out quick keep state
CONF
put /tmp/sc-q.conf >/dev/null
p0 "cat /tmp/sc-q.conf"
p0 "/opt/pf/pfctl -f /tmp/sc-q.conf && echo 'filas carregadas'"
p0 "/opt/pf/pfctl -sr"
echo
bold "# o pfctl mostra as classes e as estatisticas do escalonador"
p0 "/opt/pf/pfctl -s queue -v"
echo
bold "# transferencia de 2 MB pela fila: o trafego entra na classe"
vms "head -c 2000000 /dev/zero | tr '\\0' A > /tmp/sc_data.bin; ls -l /tmp/sc_data.bin; pkill nc 2>/dev/null; (setsid nc -l -p 9095 < /tmp/sc_data.bin >/dev/null 2>&1 &); sleep 1; echo 'servidor escutando na 9095'"
vmc "nc -w60 $SRV_IP 9095 > /tmp/sc_out.bin; wc -c < /tmp/sc_out.bin"
sleep 1
p0 "/opt/pf/pfctl -s queue -v"
echo
bold "# a disciplina hfsc contou o trafego da classe"
p0 "/opt/pf/pfctl -sr -vv | head -6"
p0 "ip -s link show $IF_SRV | head -4"
echo
p0 "/opt/pf/pfctl -f /dev/null; pkill nc 2>/dev/null; /opt/pf/pfctl -s queue -v"
green " filas do OpenBSD 8.0 (hfsc) anexadas a interface e contando pacotes"
+14
View File
@@ -0,0 +1,14 @@
#!/bin/sh
# Analysis helper executed ON the firewall VM (avoids quoting hell through the
# ssh layers). usage: an.sh <capture-file> <mode>
f=$1
mode=$2
case "$mode" in
req) echo "requerentes no fio: $(grep -c 'echo request' "$f")" ;;
rep) echo "respostas no fio: $(grep -c 'echo reply' "$f")" ;;
src) echo "origem no fio (contagem por endereco de origem):"
grep 'echo request' "$f" | sed 's/.*: \([0-9.]*\) > .*/\1/' | sort | uniq -c ;;
uniq) echo "origens distintas: $(grep 'echo request' "$f" | sed 's/.*: \([0-9.]*\) > .*/\1/' | sort -u | tr '\n' ' ')" ;;
lines) grep 'echo request' "$f" | head -2 ;;
*) echo "modo desconhecido: $mode" ;;
esac
+72
View File
@@ -0,0 +1,72 @@
#!/usr/bin/env bash
# Shared helpers for the screencast phase scripts.
#
# Everything printed by a phase is the REAL output of the lab: Alpine VMs
# under libvirt running the kernel built from source with pf.ko. Only the
# framing (prompts, banners) is scripted.
set -u
DEB=${DEB:-root@192.168.1.242}
KEY=${KEY:-$HOME/.ssh/pflab_ed}
CLI_IP=${CLI_IP:-10.0.1.10}
SRV_IP=${SRV_IP:-10.0.2.20}
GW2_IP=${GW2_IP:-10.0.3.2}
REPO=${REPO:-$(cd "$(dirname "$0")/../../.." && pwd)}
cyan() { printf '\033[1;36m%s\033[0m\n' "$*"; }
green() { printf '\033[1;32m%s\033[0m\n' "$*"; }
yellow(){ printf '\033[1;33m%s\033[0m\n' "$*"; }
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
dim() { printf '\033[2m%s\033[0m\n' "$*"; }
banner() {
echo
bold "==============================================================="
bold " $*"
bold "==============================================================="
echo
}
mgmt() {
ssh -o ConnectTimeout=8 "$DEB" "virsh domifaddr $1 | awk '/vnet/{print \$4}' | cut -d/ -f1" \
2>/dev/null | tr -d '\r' | head -1
}
PF0_MGMT=${PF0_MGMT:-$(mgmt pftest-pf0)}
CLI_MGMT=${CLI_MGMT:-$(mgmt pftest-cli)}
SRV_MGMT=${SRV_MGMT:-$(mgmt pftest-srv)}
[ -n "$PF0_MGMT" ] || PF0_MGMT=192.168.122.8
[ -n "$CLI_MGMT" ] || CLI_MGMT=192.168.122.230
[ -n "$SRV_MGMT" ] || SRV_MGMT=192.168.122.202
onvm() {
local ip=$1; shift
ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new -o LogLevel=ERROR \
-o ProxyJump=root@192.168.1.242 -i "$KEY" root@"$ip" "$@" 2>&1
}
# run on the firewall VM, showing the command as a prompt
p0() {
cyan "pf0# $*"
onvm "$PF0_MGMT" "$@"
sleep 0.7
}
# run on the client VM
vmc() {
cyan "cli# $*"
onvm "$CLI_MGMT" "$@"
sleep 0.7
}
# run on the server VM
vms() {
cyan "srv# $*"
onvm "$SRV_MGMT" "$@"
sleep 0.7
}
# upload a local file to the firewall VM
put() {
scp -q -o ProxyJump=root@192.168.1.242 -i "$KEY" "$1" "root@$PF0_MGMT:/tmp/" 2>/dev/null
}
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bash
# Render every screencast phase into out/screencast/*.mp4 (silent, 1080p30).
#
# ./scripts/screencast/record-all.sh [phase ...]
#
# Each phase is a real session against the lab; the videos carry no audio.
set -u
cd "$(dirname "$0")/../.."
REPO=$(pwd)
OUT="$REPO/out/screencast"
mkdir -p "$OUT"
render() { # <file> <title> <subtitle> <phase-no> [panel]
local f=$1 title=$2 sub=$3 no=$4
local panel=${5:-"alpine (pf0) - lab libvirt - kernel 7.3.4 + pf.ko"}
echo "=== gravando $f ==="
python3 "$REPO/scripts/screencast/render.py" \
--script "$REPO/scripts/screencast/phases/$f" \
--out "$OUT/${f%.sh}.mp4" \
--title "$title" --subtitle "$sub" \
--phase-no "$no" --phase-total 6 --panel "$panel" --min-hold 1.1 \
--footer "PF on Linux - OpenBSD PF 8.0 no kernel Linux - lab libvirt" || return 1
}
declare -A TITLES=(
[01-implementacao-kernel]="Fase 1 - Implementacao no kernel"
[02-ativacao]="Fase 2 - Ativacao"
[03-regras-basicas]="Fase 3 - Regras basicas"
[04-roteamento]="Fase 4 - Roteamento"
[05-balanceamento]="Fase 5 - Balanceamento"
[06-qos]="Fase 6 - QOS"
)
declare -A SUBS=(
[01-implementacao-kernel]="Das fontes OpenBSD 8.0 ao modulo pf.ko"
[02-ativacao]="insmod pf.ko / pfctl -e / pfctl -s info"
[03-regras-basicas]="tabelas, anchors, keep state, block drop"
[04-roteamento]="route-to e reply-to: o proximo salto decidido pelo pf"
[05-balanceamento]="pools round-robin, source-hash e dup-to"
[06-qos]="filas hfsc do OpenBSD 8.0 no caminho de dados"
)
PHASES=("$@")
[ ${#PHASES[@]} -eq 0 ] && PHASES=(01-implementacao-kernel 02-ativacao 03-regras-basicas \
04-roteamento 05-balanceamento 06-qos)
for p in "${PHASES[@]}"; do
f="$p.sh"
[ -f "$REPO/scripts/screencast/phases/$f" ] || { echo "fase inexistente: $f"; exit 1; }
# the phase number comes from the phase's own name, not from its position
# in the list, so a partial re-render keeps the right "FASE x/6" label
n=$((10#${p%%-*}))
case "$p" in
01-*) panel="host de build - repositorio pf_port" ;;
*) panel="alpine (pf0) - lab libvirt - kernel 7.3.4 + pf.ko" ;;
esac
render "$f" "${TITLES[$p]}" "${SUBS[$p]}" "$n" "$panel" || echo "FALHOU: $f"
done
echo
echo "=== videos gerados ==="
for m in "$OUT"/*.mp4; do
[ -e "$m" ] || continue
d=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$m")
printf '%-46s %6.1fs %5.1f MB\n' "$(basename "$m")" "$d" "$(echo "scale=1; $(stat -c%s "$m")/1000000" | bc)"
done
+336
View File
@@ -0,0 +1,336 @@
#!/usr/bin/env python3
"""
Render a shell session into a silent, YouTube-ready MP4 screencast.
Pipeline: pty -> raw bytes -> pyte (terminal emulation) -> PNG frames -> MP4
Every byte on screen is the real output of the command; only the pacing and
the framing are produced here. No audio track is written (the videos are
silent by design).
Usage:
./scripts/screencast/render.py --script scripts/screencast/phases/02-ativacao.sh \
--out out/screencast/02-ativacao.mp4 --title "Fase 2 - Ativacao" \
--subtitle "insmod pf.ko / pfctl -e / pfctl -s info"
"""
import argparse
import errno
import fcntl
import os
import pty
import select
import shutil
import signal
import struct
import subprocess
import sys
import termios
import time
import pyte
from PIL import Image, ImageDraw, ImageFont
# ---------------------------------------------------------------- palette ---
BG = (14, 17, 23)
PANEL = (20, 24, 31)
BORDER = (48, 54, 61)
FG = (201, 209, 217)
HEADER_BG = (24, 29, 38)
HEADER_FG = (240, 246, 252)
ACCENT = (88, 166, 255)
MUTED = (110, 118, 129)
TITLE_BG = (17, 21, 28)
# pyte colour names -> RGB (a calm, high-contrast terminal palette)
NAMED = {
"black": (48, 54, 61),
"red": (255, 123, 114),
"green": (63, 185, 80),
"yellow": (210, 153, 34),
"blue": (88, 166, 255),
"magenta": (188, 140, 255),
"cyan": (57, 197, 207),
"white": (201, 209, 217),
"brown": (168, 115, 66),
"default": None,
}
def color_of(spec, default):
if spec == "default":
return default
if spec in NAMED:
return NAMED[spec] or default
try:
if len(spec) == 6:
return tuple(int(spec[i:i + 2], 16) for i in (0, 2, 4))
except ValueError:
pass
return default
class Recorder:
"""Runs a command on a pty and keeps a timestamped snapshot per output event."""
def __init__(self, cols, rows, env=None):
self.cols, self.rows = cols, rows
self.screen = pyte.Screen(cols, rows)
self.screen.set_mode(pyte.modes.LNM)
self.stream = pyte.ByteStream(self.screen)
self.events = [] # (timestamp, rows_of_cells)
self.t0 = time.time()
def snapshot(self):
rows = []
for y in range(self.rows):
line = self.screen.buffer[y]
cells = []
for x in range(self.cols):
ch = line[x]
fg = color_of(ch.fg, FG)
bold = ch.bold
cells.append((ch.data or " ", fg, bold))
rows.append(cells)
return rows, (self.screen.cursor.y, self.screen.cursor.x)
def run(self, argv, env=None):
master, slave = pty.openpty()
fcntl.ioctl(slave, termios.TIOCSWINSZ,
struct.pack("HHHH", self.rows, self.cols, 0, 0))
e = dict(os.environ)
e.update({"TERM": "xterm-256color", "COLUMNS": str(self.cols),
"LINES": str(self.rows), "LC_ALL": "C.UTF-8"})
if env:
e.update(env)
proc = subprocess.Popen(argv, stdin=slave, stdout=slave, stderr=slave,
env=e, start_new_session=True, close_fds=True)
os.close(slave)
while True:
try:
r, _, _ = select.select([master], [], [], 0.25)
except InterruptedError:
continue
if master in r:
try:
data = os.read(master, 65536)
except OSError as exc:
if exc.errno == errno.EIO:
break
raise
if not data:
break
self.stream.feed(data)
rows, cur = self.snapshot()
self.events.append((time.time() - self.t0, rows, cur))
elif proc.poll() is not None:
# drain whatever is left
try:
while True:
data = os.read(master, 65536)
if not data:
break
self.stream.feed(data)
rows, cur = self.snapshot()
self.events.append((time.time() - self.t0, rows, cur))
except OSError:
pass
break
proc.wait()
os.close(master)
if not self.events:
rows, cur = self.snapshot()
self.events.append((time.time() - self.t0, rows, cur))
return proc.returncode
# ------------------------------------------------------------- rendering ---
def load_font(path, size):
return ImageFont.truetype(path, size)
def render_frame(rows, title, subtitle, phase_no, phase_total, cols, font, bold_font,
small_font, footer, cursor=None, panel="alpine (pf0)"):
W, H = 1920, 1080
img = Image.new("RGB", (W, H), BG)
d = ImageDraw.Draw(img)
# header
d.rectangle([0, 0, W, 74], fill=HEADER_BG)
d.text((44, 26), "PF on Linux", font=bold_font, fill=HEADER_FG)
tw = d.textlength("PF on Linux", font=bold_font)
d.text((44 + tw + 26, 30), "-", font=font, fill=MUTED)
d.text((44 + tw + 46, 30), "OpenBSD PF 8.0 engine inside the Linux kernel",
font=font, fill=MUTED)
if phase_total:
label = f"FASE {phase_no}/{phase_total}"
lw = d.textlength(label, font=small_font)
d.text((W - 44 - lw, 32), label, font=small_font, fill=ACCENT)
# title block
d.text((44, 112), title, font=bold_font, fill=HEADER_FG)
if subtitle:
d.text((44, 162), subtitle, font=font, fill=MUTED)
# terminal panel, sized to the content
top = 212
left = 44
right = W - 44
lh = 30
used = 0
for i, line in enumerate(rows):
if any(c[0].strip() for c in line):
used = i + 1
bottom = min(H - 84, top + 44 + 28 + (max(used, 4) + 1) * lh)
d.rectangle([left, top, right, bottom], fill=PANEL, outline=BORDER, width=2)
d.rectangle([left, top, right, top + 44], fill=HEADER_BG)
for i, c in enumerate([(255, 95, 86), (255, 189, 46), (39, 201, 63)]):
d.ellipse([left + 24 + i * 26, top + 15, left + 38 + i * 26, top + 29], fill=c)
d.text((left + 110, top + 13), panel, font=small_font, fill=MUTED)
# text
x0 = left + 28
y0 = top + 64
cw = d.textlength("M", font=font)
for i, line in enumerate(rows):
y = y0 + i * lh
if y > bottom - 10:
break
for x, (ch, fg, bold) in enumerate(line):
if ch == " ":
continue
d.text((x0 + x * cw, y), ch, font=bold_font if bold else font, fill=fg)
# cursor block
if cursor:
cy, cx = cursor
if 0 <= cy < len(rows) and 0 <= cx < cols:
d.rectangle([x0 + cx * cw, y0 + cy * lh, x0 + (cx + 1) * cw - 1,
y0 + (cy + 1) * lh - 1], fill=ACCENT)
# footer
d.text((44, H - 58), footer, font=small_font, fill=MUTED)
url = "git.labexposed.com/lgcosta/pf-linux-port"
uw = d.textlength(url, font=small_font)
d.text((W - 44 - uw, H - 58), url, font=small_font, fill=MUTED)
return img
def render_title(title, subtitle, phase_no, phase_total, bold_font, font, small_font):
W, H = 1920, 1080
img = Image.new("RGB", (W, H), TITLE_BG)
d = ImageDraw.Draw(img)
d.rectangle([0, 0, W, 8], fill=ACCENT)
d.text((120, 400), title, font=bold_font, fill=HEADER_FG)
if subtitle:
d.text((120, 470), subtitle, font=font, fill=MUTED)
if phase_total:
d.text((120, 560), f"FASE {phase_no} DE {phase_total}", font=small_font, fill=ACCENT)
d.text((120, 620), "PF on Linux - OpenBSD PF 8.0 no kernel Linux",
font=small_font, fill=MUTED)
return img
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--script", required=True)
ap.add_argument("--out", required=True)
ap.add_argument("--title", required=True)
ap.add_argument("--subtitle", default="")
ap.add_argument("--footer", default="")
ap.add_argument("--phase-no", type=int, default=0)
ap.add_argument("--phase-total", type=int, default=0)
ap.add_argument("--cols", type=int, default=120)
ap.add_argument("--rows", type=int, default=32)
ap.add_argument("--fps", type=int, default=30)
ap.add_argument("--max-hold", type=float, default=2.0)
ap.add_argument("--min-hold", type=float, default=0.25)
ap.add_argument("--title-card", type=float, default=3.0)
ap.add_argument("--keep-frames", action="store_true")
ap.add_argument("--panel", default="alpine (pf0) - lab libvirt - kernel 7.3.4 + pf.ko")
args = ap.parse_args()
repo = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
mono = "/usr/share/fonts/dejavu-sans-mono-fonts/DejaVuSansMono.ttf"
mono_b = "/usr/share/fonts/dejavu-sans-mono-fonts/DejaVuSansMono-Bold.ttf"
font = load_font(mono, 21)
bold_font = load_font(mono_b, 21)
small_font = load_font(mono, 19)
rec = Recorder(args.cols, args.rows)
rc = rec.run(["bash", args.script])
sys.stderr.write(f"[screencast] script rc={rc}, events={len(rec.events)}\n")
os.makedirs(os.path.dirname(os.path.abspath(args.out)) or ".", exist_ok=True)
frames_dir = os.path.join(os.path.dirname(os.path.abspath(args.out)), "_frames")
shutil.rmtree(frames_dir, ignore_errors=True)
os.makedirs(frames_dir, exist_ok=True)
def dump(img, idx):
img.save(os.path.join(frames_dir, f"{idx:05d}.png"))
n = 0
# title card
title_img = render_title(args.title, args.subtitle, args.phase_no,
args.phase_total, bold_font, font, small_font)
title_frames = int(args.title_card * args.fps)
for _ in range(title_frames):
dump(title_img, n)
n += 1
# session frames, paced by the real inter-event delays
prev_t = 0.0
for i, (ts, rows, cur) in enumerate(rec.events):
hold = ts - prev_t
prev_t = ts
if i + 1 < len(rec.events):
hold = rec.events[i + 1][0] - ts
hold = max(args.min_hold, min(args.max_hold, hold))
img = render_frame(rows, args.title, args.subtitle, args.phase_no,
args.phase_total, args.cols, font, bold_font,
small_font, args.footer, cursor=cur, panel=args.panel)
frames = max(1, int(round(hold * args.fps)))
for _ in range(frames):
dump(img, n)
n += 1
# closing card
end_img = render_frame(rec.events[-1][1], args.title, args.subtitle,
args.phase_no, args.phase_total, args.cols, font,
bold_font, small_font,
args.footer or "fim da fase",
cursor=rec.events[-1][2], panel=args.panel)
for _ in range(int(1.5 * args.fps)):
dump(end_img, n)
n += 1
# encoder: prefer x264, fall back to openh264 (both H.264 in MP4, which is
# what YouTube wants), then VP9/WebM.
have = subprocess.run(["ffmpeg", "-hide_banner", "-encoders"],
capture_output=True, text=True).stdout
base = ["ffmpeg", "-y", "-loglevel", "error", "-framerate", str(args.fps),
"-i", os.path.join(frames_dir, "%05d.png"),
"-pix_fmt", "yuv420p", "-movflags", "+faststart", "-r", str(args.fps)]
if "libx264" in have:
cmd = base + ["-c:v", "libx264", "-preset", "medium", "-crf", "20",
"-profile:v", "high", "-level", "4.0", args.out]
elif "libopenh264" in have:
cmd = base + ["-c:v", "libopenh264", "-b:v", "6M",
"-profile:v", "high", args.out]
elif "libvpx-vp9" in have:
cmd = base + ["-c:v", "libvpx-vp9", "-b:v", "0", "-crf", "30",
"-row-mt", "1", "-cpu-used", "4", args.out]
else:
raise SystemExit("[screencast] no usable H.264/VP9 encoder in ffmpeg")
subprocess.run(cmd, check=True)
if not args.keep_frames:
shutil.rmtree(frames_dir, ignore_errors=True)
dur = subprocess.run(["ffprobe", "-v", "error", "-show_entries",
"format=duration", "-of", "csv=p=0", args.out],
capture_output=True, text=True).stdout.strip()
size = os.path.getsize(args.out)
sys.stderr.write(f"[screencast] wrote {args.out} frames={n} "
f"duration={dur}s size={size/1e6:.1f}MB\n")
if __name__ == "__main__":
main()