Docs: bring the documentation in line with the code
- Demo docs replaced by the screencast docs (single source of truth): the legacy demo-session.sh/demo-record.py and the v0.9 renderer were removed instead of being kept as a second convention. - feature-matrix: stale 'M2-in-progress' vocabulary gone; the rows the 15 gates prove now say VERIFIED-in-VM and name the gate; af-to is now an explicit non-goal row (INET6 0 in this build). - guide: the queues/divert/pflog sections described a state that no longer exists (priq-only, divert dropping packets, pflog logging nothing); they now describe the shipped behaviour. - vm-lab: the 15 real gates with what each proves, 15/15 green, and the pf0 -> cli/srv/gw2 ssh setup the screencasts rely on. - install/build: pdivert and pflogd are built and ported (docs said otherwise); module object list now includes hfsc.c, fq_codel.c, pfk_pflog.c; the screencast pipeline is an optional build stage. - pf-guide: the 'M2 scope only' framing is gone; honest gaps listed. - caveats: three new sections about the screencast pipeline (MiniMax video generation blocked by the account tier, narration as a separate track, a video frame is not a verdict). - porting-notes: what the recording taught us. - README/README.pt-BR: status, milestones, layout and video section updated. All internal doc links verified.
This commit is contained in:
1 parent
cc67531602
commit
dff2185b20
37 files changed
+677
-1616
No files matched your search
@@ -1,78 +1,83 @@
|
||||
# Matriz de features
|
||||
|
||||
English: [docs/en/feature-matrix.md](../en/feature-matrix.md).
|
||||
Inglês: [docs/en/feature-matrix.md](../en/feature-matrix.md).
|
||||
|
||||
Vocabulário de status (idêntico nos dois idiomas):
|
||||
|
||||
- **VERIFIED-in-VM** — passou de ponta a ponta na bancada libvirt (ver [vm-lab.md](vm-lab.md)).
|
||||
- **M2-in-progress** — implementado; os gates VM 01–04 existem, mas a suíte **ainda não está toda verde**; sem alegação de VERIFIED até uma rodada limpa.
|
||||
- **COMPILES-untested** — compila em `pf.ko`/`pfctl`; nenhum gate exercitou.
|
||||
- **PENDING-Mx** — entra no milestone Mx; o comportamento observável hoje está declarado.
|
||||
- **OUT-OF-SCOPE** — não-objetivo por projeto (spec §3).
|
||||
- **VERIFIED-in-VM** — exercitado de ponta a ponta no lab libvirt (veja [vm-lab.md](vm-lab.md)); o gate que prova está nomeado.
|
||||
- **VERIFIED-offline** — provado por um teste de fixture/ABI, não por tráfego (ex.: `make -C tools test`).
|
||||
- **COMPILES-untested** — compilado dentro do `pf.ko`/`pfctl`; nenhum gate exercitou ainda.
|
||||
- **NOT-ENABLED** — o código existe mas a feature está desligada neste build (ex.: `af-to`, que depende dos caminhos `INET6`).
|
||||
- **OUT-OF-SCOPE** — fora de escopo por desenho (spec §3).
|
||||
|
||||
Estado em 2026-11-11; evidência por linha: `out/test-logs/`,
|
||||
`docs/porting-notes.md`, commits de milestone.
|
||||
Atualizado em 2026-11-12; fonte da evidência por linha: `out/test-logs/`,
|
||||
`docs/porting-notes.md`, commits dos milestones e os screencasts
|
||||
([screencast.md](screencast.md)) para a prova visível.
|
||||
|
||||
A suíte está **15/15 verde** (`./scripts/vm-run-tests.sh`).
|
||||
|
||||
## Plano de controle e ferramentas
|
||||
|
||||
| Feature | Sintaxe de upstream | Milestone | Status |
|
||||
| Feature | Sintaxe upstream | Milestone | Status |
|
||||
|---|---|---|---|
|
||||
| Carga do módulo, `/dev/pf` (0600) | `insmod pf.ko` | M1 | VERIFIED-in-VM |
|
||||
| Carga do módulo, `/dev/pf` (0600) | `insmod pf.ko` | M1 | VERIFIED-in-VM (gate `02-ativacao.sh`) |
|
||||
| Habilitar/desabilitar | `pfctl -e`, `pfctl -d` | M1 | VERIFIED-in-VM |
|
||||
| Carga de ruleset | `pfctl -f arquivo` | M2 | VERIFIED-in-VM (cargas bem-sucedidas em toda rodada capturada; o status geral da rodada é misto — ver [vm-lab.md](vm-lab.md) §6) |
|
||||
| Checagem só de parse | `pfctl -n -f arquivo` | M1 | VERIFIED (suíte de fixtures offline, `make -C tools test`) |
|
||||
| Carga de ruleset | `pfctl -f file` | M2 | VERIFIED-in-VM (todo gate carrega um ruleset real) |
|
||||
| Checagem só de parse | `pfctl -n -f file` | M1 | VERIFIED-offline (`make -C tools test`) |
|
||||
| Status/contadores | `pfctl -s info [-v]` | M1 | VERIFIED-in-VM |
|
||||
| Listagem de regras | `pfctl -s rules [-v]` | M2 | COMPILES-untested |
|
||||
| Listagem de estados | `pfctl -s states [-v]` | M2 | M2-in-progress — o gate 01 mostra anomalia de listagem (ping passa, nenhum estado icmp exibido) enquanto a máquina de estados está sendo corrigida |
|
||||
| Tabelas (listagem/ops) | `pfctl -s tables`, `-t t -T show/add/del/kill` | M2 | M2-in-progress (gate 03) |
|
||||
| Anchors | `pfctl -a anchor -f arquivo`, `-s Anchors` | M2 | M2-in-progress (gate 04) |
|
||||
| Listagem de filas | `pfctl -s queue -v`, `-s ifq -v` | M5 | PENDING-M5 (o parse funciona; contadores ainda não conectados aos back ends reais) |
|
||||
| Paridade de ABI ioctl | `make -C tools abi` (tabelas do abitest) | M1 | VERIFIED no lado do build (tabela userland); o diff do lado kernel é um passo aberto para o integrador (`tools/UPSTREAM.md`) |
|
||||
| Listagem de regras | `pfctl -s rules [-v]`, `pfctl -sr` | M2 | VERIFIED-in-VM (gates 02–06) |
|
||||
| Listagem de estados | `pfctl -s states [-v]`, `pfctl -ss` | M2 | VERIFIED-in-VM (gate `01-state.sh`) |
|
||||
| Listagem/operações de tabelas | `pfctl -s tables`, `-t t -T show/add/del/kill` | M2 | VERIFIED-in-VM (gate `03-tables.sh`, incluindo um `-T delete` ao vivo liberando o fluxo) |
|
||||
| Anchors | `pfctl -a anchor -f file`, `-s anchors` | M2 | VERIFIED-in-VM (gate `04-anchors.sh`) |
|
||||
| Listagem de filas | `pfctl -s queue -v` | M5 | VERIFIED-in-VM (gate `15-queue.sh`: as duas classes com estatísticas) |
|
||||
| Paridade da ABI ioctl | `make -C tools abi` (tabelas do abitest) | M1 | VERIFIED-offline (tabela do userland); o diff do lado do kernel é um passo de integração em aberto (`tools/UPSTREAM.md`) |
|
||||
|
||||
## Núcleo de filtragem
|
||||
|
||||
| Feature | Sintaxe de upstream | Milestone | Status |
|
||||
| Feature | Sintaxe upstream | Milestone | Status |
|
||||
|---|---|---|---|
|
||||
| `pass`/`match`, quick, match por interface/proto/endereço/porta | `pass in quick on wan proto tcp from 10/8 to port 80` | M2 | M2-in-progress (o caminho de dados roda o `pf_test` real; rodada parcial recente: ping de trânsito OK, icmp bloqueado OK) |
|
||||
| `block drop` | `block drop in quick …` | M2 | M2-in-progress (gate 02a) |
|
||||
| `block return` / `return-rst` / `return-rst after quarter` | `block return-rst in quick proto tcp …` | M2 | M2-in-progress (gate 02b) |
|
||||
| `keep state` (icmp/tcp/udp), tabela de estados, purger | `… keep state` | M2 | M2-in-progress |
|
||||
| `set state-policy`, `allow-opts` etc. | opções `set …` | M2 | COMPILES-untested |
|
||||
| Syncookies (caminho de estouro de estado) | `set syncookies always` | M2 | COMPILES-untested |
|
||||
| Tabelas pfr (radix/hash) | `table <blk> persist { 1.2.3.4 }`, `-T add …` | M2 | M2-in-progress (gate 03) |
|
||||
| Anchors + include + precedência de quick | `anchor "x"`, `load anchor …` | M2 | M2-in-progress (gate 04) |
|
||||
| IPv6 para tudo acima | mesma sintaxe, v6 | M2+ | COMPILES-untested (o engine é completo para v6 por construção; gates v6 smoke planejados) |
|
||||
| Tráfego não-IPv4/IPv6 (ARP…) | — | M2 | PFLINUX por construção: hooks netfilter só de IPv4/IPv6 — ARP nunca entra no caminho do PF (sem gate ainda) |
|
||||
| `pass`/`match`, quick, on-interface/proto/addr/port | `pass in quick on wan proto tcp from 10/8 to port 80` | M2 | VERIFIED-in-VM (gate `01-state.sh`: ping de trânsito atravessando o firewall) |
|
||||
| `block drop` | `block drop in quick …` | M2 | VERIFIED-in-VM (gate `02-block.sh`: o ping morre e os contadores provam) |
|
||||
| `block return` / `return-rst` / `return-rst after quarter` | `block return-rst in quick proto tcp …` | M2 | VERIFIED-in-VM (gate `02-block.sh`: o `nc` do cliente recebe RST) |
|
||||
| `keep state` (icmp/tcp/udp), tabela de estados, purger | `… keep state` | M2 | VERIFIED-in-VM (gate `01-state.sh`; o estado aparece no `pfctl -ss`) |
|
||||
| `set state-policy`, `allow-opts`, etc. | opções `set …` | M2 | COMPILES-untested |
|
||||
| Syncookies (caminho de overflow de estados) | `set syncookies always` | M2 | COMPILES-untested |
|
||||
| Tabelas pfr (radix/hash) | `table <blk> persist { 1.2.3.4 }`, `-T add …` | M2 | VERIFIED-in-VM (gate `03-tables.sh`) |
|
||||
| Anchors + include + precedência de quick | `anchor "x"`, `load anchor …` | M2 | VERIFIED-in-VM (gate `04-anchors.sh`) |
|
||||
| IPv6 para tudo acima | mesma sintaxe, v6 | M2+ | COMPILES-untested — o engine é completo para v6 por construção, mas o caminho v6 não é exercitado por gate |
|
||||
| Tráfego não-IPv4/IPv6 (ARP…) | — | M2 | PFLINUX por construção: só os hooks netfilter IPv4/IPv6 — ARP nunca entra no caminho do PF (sem gate) |
|
||||
|
||||
## NAT / normalização (M3)
|
||||
|
||||
| Feature | Sintaxe de upstream | Milestone | Status |
|
||||
| Feature | Sintaxe upstream | Milestone | Status |
|
||||
|---|---|---|---|
|
||||
| `nat-to` (pools de portas, sticky-port, static-port) | `pass out … nat-to (eth1:0)` | M3 | **VERIFICADO em lab** — NAT de origem funciona ponta a ponta: o pacote sai com o endereço do pool (prova via tcpdump), a resposta é des-NATeada e roteada de novo para o cliente, e o estado mostra as duas tuplas (`10.0.2.1:59335 (10.0.1.10:4380) -> 10.0.2.20:8`). Gate: `tests/vm/05-nat.sh`. |
|
||||
| `rdr-to` | `pass in … to <fw> port 8080 rdr-to <srv> port 80 keep state` | M3 | **VERIFICADO em lab** — o redirect de portas funciona ponta a ponta (gate `06-rdr.sh`): o cliente fala com o endereço do firewall, o pf reescreve o destino, re-rotea o pacote e a resposta é des-redirectuada. O gate também baixa 2 MB pelo redirect e confere a integridade byte a byte (guarda de regressão GSO/TSO). |
|
||||
| `binat-to` | `pass quick proto icmp from 10.0.2.20 to 10.0.1.10 binat-to 10.0.2.50 keep state` | M3 | **VERIFICADO em lab** — gate `07-binat.sh`. O pfctl expande uma regra `binat-to` no par documentado (`pass out … nat-to <mapeado> static-port` + `pass in … rdr-to <real>`), e as duas metades funcionam: o peer vê o endereço mapeado, o original não vaza, e a resposta é traduzida de volta. |
|
||||
| Scrub/normalização: remonteagem, `fragment drop-overlap`, `min-ttl`, `max-mss`, `df`/`no-df`, `random-id`, `set reassemble yes` | `match in all scrub (min-ttl 100 max-mss 500)` | M3 | **VERIFICADO em lab** — gate `09-scrub.sh`: `min-ttl` eleva o TTL (o egress mostra 99 após o decremento do encaminhamento) e `max-mss` limita o MSS de um SYN, ambos medidos no egress do próprio firewall. Os caminhos de remonteagem/overlap estão compilados e alcançados, mas ainda sem gate. |
|
||||
| Fingerprint de SO em estados | `pass … set os A` + `/etc/pf.os` | bônus | COMPILES-untested (`pf_osfp.c` compilado; sem empacotamento de fingerprints ainda — ausência de `/etc/pf.os` é tolerada) |
|
||||
| `nat-to` (pools de portas, sticky-port, static-port) | `pass out … nat-to (eth1:0)` | M3 | **VERIFIED-in-VM** — gate `05-nat.sh`: o NAT de origem funciona de ponta a ponta (prova por tcpdump), a resposta é des-NATeada e re-encaminhada para o cliente, e o estado mostra as duas tuplas (`10.0.2.1:59335 (10.0.1.10:4380) -> 10.0.2.20:8`). |
|
||||
| `rdr-to` | `pass in … to <fw> port 8080 rdr-to <srv> port 80 keep state` | M3 | **VERIFIED-in-VM** — gate `06-rdr.sh`: o cliente fala com o endereço do firewall, o pf reescreve o destino, re-encaminha o pacote, e a resposta é des-redirecionada. O gate também puxa 2 MB pelo redirect e confere a integridade byte a byte (guarda de regressão GSO/TSO). |
|
||||
| `binat-to` | `pass quick proto icmp from 10.0.2.20 to 10.0.1.10 binat-to 10.0.2.50 keep state` | M3 | **VERIFIED-in-VM** — gate `07-binat.sh`. O pfctl expande uma regra `binat-to` no par documentado (`pass out … nat-to <mapped> static-port` + `pass in … rdr-to <real>`), e as duas metades funcionam: o par vê o endereço mapeado, o original nunca vaza, e a resposta é traduzida de volta. |
|
||||
| Pools de NAT (`round-robin`, `source-hash`) | `nat-to { 10.0.2.1, 10.0.3.1 } round-robin` | M4 | **VERIFIED-in-VM** — gate `10-pools.sh`, e visível no screencast: o round-robin alterna o endereço de origem entre os membros (3 pacotes cada), o source-hash mantém uma origem em um membro só. |
|
||||
| Scrub/normalize: remontagem, `fragment drop-overlap`, `min-ttl`, `max-mss`, `df`/`no-df`, `random-id`, `set reassemble yes` | `match in all scrub (min-ttl 100 max-mss 500)` | M3 | **VERIFIED-in-VM** — gate `09-scrub.sh`: o `min-ttl` aumenta o TTL (egresso mostra 99 depois do decremento do forward) e o `max-mss` limita a opção MSS do SYN, ambos medidos no egresso do próprio firewall. Os caminhos de remontagem/overlap compilam e são alcançados, mas não têm gate. |
|
||||
| Fingerprint de OS nos estados | `pass … set os A` + `/etc/pf.os` | bonus | COMPILES-untested (`pf_osfp.c` compilado; ainda sem o pacote de fingerprints — a ausência de `/etc/pf.os` é tolerada) |
|
||||
|
||||
## Roteamento / divert / filas / logging
|
||||
|
||||
| Feature | Sintaxe de upstream | Milestone | Status |
|
||||
| Feature | Sintaxe upstream | Milestone | Status |
|
||||
|---|---|---|---|
|
||||
| Pools de `route-to` (`rr`, `round-robin`, `source-hash`, `hash`), `reply-to`, `dup-to` | `pass in … route-to { 10.0.3.2, 10.0.2.2 } round-robin keep state` | M4 | **`round-robin` e `source-hash` VERIFICADOS em lab** (gate `10-pools.sh`): com dois gateways em dois enlaces, o round-robin usa ambos e o source-hash fixa todo fluxo de uma origem em um único enlace. `dup-to` verificado no gate 08; `reply-to` ainda sem gate. `fail-to` **não existe no OpenBSD 8.0** (removido upstream), então está fora de escopo. |
|
||||
| `divert-to` / tag `divert` | `pass in … divert-to <host> port <n>` | VERIFICADO em lab | **Funciona ponta a ponta.** O módulo registra em runtime a família de sockets `AF_DIVERT` (`PF_DIVERT=32`) — sem patch no kernel. A regra `divert-to` entrega o pacote a essa família; sem listener o pacote é descartado (`divs_noport`, paridade BSD); com `tools/pdivert` rodando o pacote vai ao userspace, é reescrito e reinjetado (verificado com mutação de payload + correção de checksum, ping 0% loss). |
|
||||
| Filas (`queue`, `queue on`, `pfctl -s queue -v`) | `queue rootq on eth2 bandwidth 10Mbit` + `queue leafq on eth2 bandwidth 2Mbit parent rootq default` | M5 | **VERIFICADO em lab** — gate `15-queue.sh`: a configuração de filas carrega, a disciplina hfsc é anexada à interface, `pfctl -s queue -v` lista as duas classes com estatísticas, um fluxo de 2 MB pela fila completa, e o contador de pacotes da classe avança (o escalonador está no caminho de dados). Note que `bandwidth` é *linkshare* do HFSC - garantia sob contenção, não teto: o pf.conf do OpenBSD 8.0 não tem `minimum`/`maximum`, então um fluxo isolado não é limitado, nem no Linux nem no OpenBSD. |
|
||||
| HFSC | `queue on wan { hfsc … }` | M5 | COMPILES-untested — **fallback priq até o M5**; `pfsrc/hfsc.c` staged (código real do OpenBSD 8.0) mas **fora da lista de objetos** do módulo |
|
||||
| FQ-CoDel | `queue on wan { fq_codel … }` | M5 | COMPILES-untested — igual ao HFSC: staged, não compilado |
|
||||
| Qdisc `pfq` (`tc qdisc … root pfq`) | n/a (glue Linux) | M5 | PENDING-M5 (ainda não existe na árvore) |
|
||||
| pflog (`log`, `log all`, `block log`) + `pflogd` pcap (DLT_PFLOG) | `pass out quick log` + `pflogd /tmp/x.pcap` | M4 | **VERIFICADO em lab** — gate `12-pflog.sh`: os pacotes logados são enfileirados pelo engine, lidos via `/dev/pflog` e gravados como pcap com linktype DLT_PFLOG (117); o dump contém os bytes reais do pacote. Desvio: um chardev em vez de interface if_pflog/bpf (Linux não tem bpf sobre essa interface virtual aqui); o layout do registro é byte-compatível com `pflog(4)` do OpenBSD. `if_pflog.c` permanece fora do build. |
|
||||
| `route-to` / `dup-to` / `reply-to` | `pass in … route-to { 10.0.3.2 } keep state`, `reply-to { gw }` | M4 | **VERIFIED-in-VM** — gates `08-routeto.sh` (o route-to sai pelo link do gateway, o dup-to copia mantendo o fluxo) e `13-replyto.sh` (a resposta de um fluxo é forçada para o link do gateway em vez do cliente). Divergência: o próximo salto forçado é instalado definindo `rt_gw4`/`rt_gw_family` na rota consultada (o Linux só faz ARP do gateway quando a rota declara um). `fail-to` **não é palavra-chave no OpenBSD 8.0** (removido upstream), logo está fora de escopo. |
|
||||
| `divert-to` / tag `divert` | `pass in … divert-to <host> port <n>` | M4 | **VERIFIED-in-VM** — gate `11-divert.sh`. O módulo registra uma família de sockets `AF_DIVERT` (`PF_DIVERT=32`) em runtime — sem patch no kernel. Uma regra `divert-to` entrega o pacote a essa família; sem listener o pacote é descartado (`divs_noport`, paridade com o BSD); com o `tools/pdivert` rodando o pacote vai para o userspace, é reescrito e reinjetado (verificado com mutação do payload + correção de checksum, ping com 0% de perda). |
|
||||
| Filas (`queue`, `queue on`, `pfctl -s queue -v`) | `queue rootq on eth2 bandwidth 10Mbit` + `queue leafq on eth2 bandwidth 2Mbit parent rootq default` | M5 | **VERIFIED-in-VM** — gate `15-queue.sh`: a configuração de filas carrega, a disciplina hfsc é anexada à interface, `pfctl -s queue -v` lista as duas classes com estatísticas, um fluxo de 2 MB pela fila completa, e o contador de pacotes da classe avança (o escalonador está no caminho de dados). Note que `bandwidth` é *linkshare* do HFSC — garantia sob contenção, não teto: o pf.conf do OpenBSD 8.0 não tem `minimum`/`maximum`, então um fluxo isolado não é limitado, nem no Linux nem no OpenBSD. |
|
||||
| HFSC | `queue on wan { hfsc … }` | M5 | **VERIFIED-in-VM** — o `pfsrc/hfsc.c` (código real do OpenBSD 8.0) está na lista de objetos do módulo e é a disciplina que o gate exercita. As classes usam o `net/hfsc.h` vendorizado (classe raiz `0x10000`, classe default `0x1000`) compartilhado com o `pfctl`, então a ABI de estatísticas bate nos dois lados. |
|
||||
| FQ-CoDel | `queue on wan { fq_codel … }` | M5 | COMPILES-untested — o `pfsrc/fq_codel.c` é compilado dentro do `pf.ko` e o `net/fq_codel.h` vendorizado é compartilhado com o `pfctl`, mas nenhum gate o exercita. |
|
||||
| Interface `pfq` | n/a (interfaces BSD `ifq`/`pfq`, não um qdisc `tc` do Linux) | M5 | **VERIFIED-in-VM** — o port mantém a forma do BSD: `compat/pfk_ifq.c` implementa as interfaces `ifq_ops`/`pfq_ops` e os back ends do OpenBSD se plugam nelas (`pfq` nunca foi um qdisc `tc` do Linux, nem nunca pretendeu ser). priq é a disciplina default. |
|
||||
| pflog (`log`, `log all`, `block log`) + pcap do `pflogd` (DLT_PFLOG) | `pass out quick log` + `pflogd /tmp/x.pcap` | M4 | **VERIFIED-in-VM** — gate `12-pflog.sh`: os pacotes logados são enfileirados pelo engine, lidos por `/dev/pflog` e escritos como pcap com linktype DLT_PFLOG (117); o dump contém os bytes reais do pacote. Divergência: um chardev em vez de uma interface if_pflog/bpf (o Linux não tem bpf sobre uma interface virtual dessas aqui); o layout do registro é byte a byte compatível com o `pflog(4)` do OpenBSD. O `if_pflog.c` continua fora da build. |
|
||||
|
||||
## Não-objetivos (explícitos)
|
||||
## Fora de escopo (explícito)
|
||||
|
||||
| Item | Motivo |
|
||||
|---|---|
|
||||
| Failover `pfsync` / CARP | não-objetivo (spec §3) |
|
||||
| IPsec / pfkey | não-objetivo |
|
||||
| `ftp-proxy` e daemons de userland | não-objetivo |
|
||||
| Instâncias PF por netns | port fiel = instância global única em `init_net` |
|
||||
| Agendadores exclusivos do ALTQ clássico (cbq/wfq/red…) | removidos do upstream antes do OpenBSD 8.0 — impossível portar da base |
|
||||
| Labels de rota (`route-label`) | não-objetivo; a camada de ioctl rejeita regras com label |
|
||||
| Failover `pfsync` / CARP | fora de escopo (spec §3) |
|
||||
| IPsec / pfkey | fora de escopo |
|
||||
| `ftp-proxy` e daemons de userland | fora de escopo |
|
||||
| Instâncias PF por netns | port fiel = uma instância global no `init_net` |
|
||||
| Agendadores exclusivos do ALTQ clássico (cbq/wfq/red…) | removidos upstream antes do OpenBSD 8.0 — impossíveis de portar a partir da base |
|
||||
| Routing labels (`route-label`) | fora de escopo; a camada de ioctl rejeita regras com label |
|
||||
| `af-to` (tradução IPv4↔IPv6) | o código está sob `#ifdef INET6` e este build tem `INET6 0`; habilitá-lo é um milestone à parte, não uma feature faltando na base |
|
||||
Reference in new issue
Block a user