Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe324259c0 |
No files matched your search
+3
-2
@@ -1,6 +1,8 @@
|
|||||||
_vendor/
|
_vendor/
|
||||||
out/
|
out/*
|
||||||
!out/demo-pf-linux.cast
|
!out/demo-pf-linux.cast
|
||||||
|
!out/screencast/
|
||||||
|
!out/screencast/*.mp4
|
||||||
*.o
|
*.o
|
||||||
*.ko
|
*.ko
|
||||||
*.mod
|
*.mod
|
||||||
@@ -11,7 +13,6 @@ out/
|
|||||||
modules.order
|
modules.order
|
||||||
Module.symvers
|
Module.symvers
|
||||||
tools/out/
|
tools/out/
|
||||||
!out/demo-pf-linux.cast
|
|
||||||
lab/seeds/
|
lab/seeds/
|
||||||
*.pcap
|
*.pcap
|
||||||
*.log
|
*.log
|
||||||
@@ -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
|
instances, classic-ALTQ-only schedulers (cbq/wfq/red — removed upstream before
|
||||||
OpenBSD 8.0).
|
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
|
## Quick start
|
||||||
|
|
||||||
Full recipe: [`docs/en/install.md`](docs/en/install.md). Short version
|
Full recipe: [`docs/en/install.md`](docs/en/install.md). Short version
|
||||||
|
|||||||
@@ -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
|
||||||
|
```
|
||||||
@@ -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
@@ -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"
|
||||||
Executable
+30
@@ -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
@@ -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"
|
||||||
Executable
+54
@@ -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
@@ -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"
|
||||||
Executable
+45
@@ -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"
|
||||||
Executable
+14
@@ -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
|
||||||
Executable
+72
@@ -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
|
||||||
|
}
|
||||||
Executable
+65
@@ -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
|
||||||
Executable
+336
@@ -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()
|
||||||
Reference in new issue
Block a user