fixed4339

This commit is contained in:
Rodrigo Bertoldo de Sousa Martins committed 2026-09-14 11:39:24 -03:00
1 parent 26d0220ecb
commit 13d0584df9
2 files changed
+109 -38

No files matched your search

+100 -29
View File
@@ -60,6 +60,39 @@ static __always_inline void stats_inc(__u32 idx)
__sync_fetch_and_add(val, 1); __sync_fetch_and_add(val, 1);
} }
#define SOCKMAP_AF_INET 2
#define SOCKMAP_AF_INET6 10
// ─────────────────────────────────────────────────────────
// BUG ENCONTRADO E CORRIGIDO: o filtro "só IPv4" (family != AF_INET)
// descartava toda conexão aceita por um servidor que escuta em dual-stack
// (bind em "::", padrão do Apache/muitos daemons quando não força IPv4).
// O socket ACEITO aparece como AF_INET6 pro kernel/eBPF mesmo a conexão
// sendo, na prática, IPv4 — o endereço vem mapeado como ::ffff:a.b.c.d.
//
// Evidência que confirmou isso: em produção, o Nextcloud (Apache
// dual-stack) tinha 0 de 184 conexões aceitas registradas no sockhash —
// o lado ATIVO dele (quando ELE disca pra MariaDB/Redis com IP literal,
// então family=AF_INET) funcionava 375/375, mas o lado PASSIVO (aceitando
// conexão do nginx) nunca registrava, porque a mesma conexão TCP aparece
// como AF_INET6 do lado de quem aceitou. Isso deixava o socket do nginx
// "meio-registrado" (só o lado dele no mapa), redirect sempre em
// fallback, e é o cenário suspeito por trás dos travamentos em
// transferências grandes.
//
// Verifica se um endereço IPv6 é, na verdade, um IPv4 mapeado
// (::ffff:a.b.c.d, RFC 4291 §2.5.5.2: 80 bits zero + 16 bits 0xffff +
// 32 bits do IPv4) e extrai o IPv4 equivalente.
static __always_inline bool ipv4_mapped(const __u32 ip6[4], __u32 *out_ip4)
{
if (ip6[0] != 0 || ip6[1] != 0)
return false;
if (bpf_ntohl(ip6[2]) != 0x0000ffff)
return false;
*out_ip4 = ip6[3];
return true;
}
// ───────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────
// Verifica se um IP (em network byte order / big-endian) é // Verifica se um IP (em network byte order / big-endian) é
// endereço privado RFC-1918 ou loopback. // endereço privado RFC-1918 ou loopback.
@@ -114,14 +147,39 @@ static __always_inline bool is_local_ip(__u32 ip_be)
// calculam exatamente o mesmo valor pro mesmo par de portas, não importa // calculam exatamente o mesmo valor pro mesmo par de portas, não importa
// qual seja a codificação exata do kernel — o que importa é que os dois // qual seja a codificação exata do kernel — o que importa é que os dois
// membros do par usem a MESMA transformação simétrica. // membros do par usem a MESMA transformação simétrica.
static __always_inline void // Retorna false se a conexão não puder ser reduzida a um par IPv4 (IPv6
// nativo de verdade, fora do escopo deste programa) — quem chama deve
// tratar isso como "não é pra registrar", igual ao antigo filtro de
// family, só que agora sem descartar dual-stack por engano.
static __always_inline bool
extract_key_from_ops(struct bpf_sock_ops *ops, struct sock_key *key) extract_key_from_ops(struct bpf_sock_ops *ops, struct sock_key *key)
{ {
key->sip4 = ops->local_ip4; __u32 local_ip4, remote_ip4;
key->dip4 = ops->remote_ip4;
if (ops->family == SOCKMAP_AF_INET) {
local_ip4 = ops->local_ip4;
remote_ip4 = ops->remote_ip4;
} else if (ops->family == SOCKMAP_AF_INET6) {
if (!ipv4_mapped(ops->local_ip6, &local_ip4) ||
!ipv4_mapped(ops->remote_ip6, &remote_ip4))
return false;
} else {
return false;
}
key->sip4 = local_ip4;
key->dip4 = remote_ip4;
key->sport = bpf_htonl(ops->local_port) >> 16; key->sport = bpf_htonl(ops->local_port) >> 16;
key->dport = ops->remote_port >> 16; key->dport = ops->remote_port >> 16;
key->family = ops->family; // family SEMPRE normalizado pra AF_INET: os dois lados da MESMA
// conexão TCP podem reportar families diferentes entre si (um vê
// AF_INET literal, o outro vê AF_INET6 mapeado por causa de bind
// dual-stack) — sem normalizar, as chaves dos dois lados nunca
// bateriam e o par nunca seria encontrado no sockhash. Foi
// exatamente isso que deixava o Nextcloud (Apache em dual-stack)
// com 0 registros passivos em 184 conexões aceitas.
key->family = SOCKMAP_AF_INET;
return true;
} }
// ───────────────────────────────────────────────────────── // ─────────────────────────────────────────────────────────
@@ -136,8 +194,12 @@ int bpf_sockops_handler(struct bpf_sock_ops *skops)
{ {
__u32 op = skops->op; __u32 op = skops->op;
// Só processamos conexões IPv4 // Aceita AF_INET e AF_INET6 aqui (o filtro real de família acontece
if (skops->family != 2) // AF_INET // dentro de extract_key_from_ops, que resolve IPv6 mapeado pra IPv4
// ou retorna false se for IPv6 de verdade). Cortar por family==AF_INET
// logo no topo, como a versão antiga fazia, foi o que descartava
// silenciosamente toda conexão aceita por um listener dual-stack.
if (skops->family != SOCKMAP_AF_INET && skops->family != SOCKMAP_AF_INET6)
return 0; return 0;
switch (op) { switch (op) {
@@ -145,25 +207,24 @@ int bpf_sockops_handler(struct bpf_sock_ops *skops)
case BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB: case BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB:
// Conexão ativa: cliente conectou a um servidor // Conexão ativa: cliente conectou a um servidor
case BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB: { case BPF_SOCK_OPS_ACTIVE_ESTABLISHED_CB: {
struct sock_key key = {};
if (!extract_key_from_ops(skops, &key)) {
stats_inc(3); // IPv6 de verdade (não mapeado) — fora do escopo
return 0;
}
// ── Filtro: só conexões entre IPs privados RFC-1918 ─────── // ── Filtro: só conexões entre IPs privados RFC-1918 ───────
// Ignora tráfego externo (HTTPS público, metadata AWS 169.254.x.x). // Ignora tráfego externo (HTTPS público, metadata AWS 169.254.x.x).
// Bug histórico: NÃO usar ip_be >> 24 sem bpf_ntohl — em x86 if (!is_local_ip(key.sip4) || !is_local_ip(key.dip4)) {
// little-endian o primeiro octeto fica no byte menos significativo.
if (!is_local_ip(skops->local_ip4) || !is_local_ip(skops->remote_ip4)) {
stats_inc(3); // conta conexões externas ignoradas stats_inc(3); // conta conexões externas ignoradas
return 0; return 0;
} }
struct sock_key key = {}; if (skops->family == SOCKMAP_AF_INET6)
extract_key_from_ops(skops, &key); bpf_printk("REG via IPv6 mapeado: op=%u sip=%x dip=%x", op, key.sip4, key.dip4);
// DEBUG TEMPORÁRIO — remover depois de confirmar o comportamento.
bpf_printk("REG op=%u sip=%x dip=%x", op, key.sip4, key.dip4);
bpf_printk("REG sport=%u dport=%u", key.sport, key.dport);
// BPF_ANY: atualiza mesmo se já existir (cobre reconexões keepalive) // BPF_ANY: atualiza mesmo se já existir (cobre reconexões keepalive)
int ret = bpf_sock_hash_update(skops, &sock_hash, &key, BPF_ANY); int ret = bpf_sock_hash_update(skops, &sock_hash, &key, BPF_ANY);
bpf_printk("REG update ret=%d", ret);
if (ret == 0) { if (ret == 0) {
stats_inc(0); stats_inc(0);
@@ -190,10 +251,8 @@ int bpf_sockops_handler(struct bpf_sock_ops *skops)
new_state == BPF_TCP_FIN_WAIT2 || new_state == BPF_TCP_FIN_WAIT2 ||
new_state == BPF_TCP_TIME_WAIT) { new_state == BPF_TCP_TIME_WAIT) {
struct sock_key key = {}; struct sock_key key = {};
extract_key_from_ops(skops, &key); if (extract_key_from_ops(skops, &key))
// DEBUG TEMPORÁRIO bpf_map_delete_elem(&sock_hash, &key);
bpf_printk("DEL state=%u sport=%u dport=%u", new_state, key.sport, key.dport);
bpf_map_delete_elem(&sock_hash, &key);
} }
break; break;
} }
@@ -228,21 +287,33 @@ int bpf_msg_redirect(struct sk_msg_md *msg)
// //
// Mesma normalização de porta usada em extract_key_from_ops (mesmo // Mesma normalização de porta usada em extract_key_from_ops (mesmo
// domínio dos dois lados): remote_port só com ">> 16", local_port // domínio dos dois lados): remote_port só com ">> 16", local_port
// convertido pra esse domínio via "bpf_htonl(...) >> 16". // convertido pra esse domínio via "bpf_htonl(...) >> 16". Mesma
// resolução de IPv6-mapeado-pra-IPv4 também — precisa bater com o
// que extract_key_from_ops grava no REGISTRO, senão o par nunca é
// encontrado (era exatamente o bug do Nextcloud dual-stack).
__u32 sip4, dip4;
if (msg->family == SOCKMAP_AF_INET) {
sip4 = msg->remote_ip4;
dip4 = msg->local_ip4;
} else if (msg->family == SOCKMAP_AF_INET6) {
if (!ipv4_mapped(msg->remote_ip6, &sip4) || !ipv4_mapped(msg->local_ip6, &dip4)) {
stats_inc(2); // IPv6 de verdade — nunca deveria estar no mapa, mas por segurança cai no fallback
return SK_PASS;
}
} else {
stats_inc(2);
return SK_PASS;
}
struct sock_key key = {}; struct sock_key key = {};
key.sip4 = msg->remote_ip4; // IP remoto = IP do par key.sip4 = sip4; // IP remoto = IP do par
key.dip4 = msg->local_ip4; // IP local = nosso IP key.dip4 = dip4; // IP local = nosso IP
key.sport = msg->remote_port >> 16; key.sport = msg->remote_port >> 16;
key.dport = bpf_htonl(msg->local_port) >> 16; key.dport = bpf_htonl(msg->local_port) >> 16;
key.family = msg->family; key.family = SOCKMAP_AF_INET; // normalizado, ver extract_key_from_ops
// DEBUG TEMPORÁRIO
bpf_printk("MSG sip=%x dip=%x", key.sip4, key.dip4);
bpf_printk("MSG sport=%u dport=%u size=%u", key.sport, key.dport, msg->size);
// Tenta redirecionar diretamente para o socket par. // Tenta redirecionar diretamente para o socket par.
int ret = bpf_msg_redirect_hash(msg, &sock_hash, &key, BPF_F_INGRESS); int ret = bpf_msg_redirect_hash(msg, &sock_hash, &key, BPF_F_INGRESS);
bpf_printk("MSG redirect ret=%d", ret);
if (ret == SK_PASS) { if (ret == SK_PASS) {
stats_inc(1); // Redirecionamento OK — caminho acelerado stats_inc(1); // Redirecionamento OK — caminho acelerado
return SK_PASS; return SK_PASS;
+9 -9
View File
@@ -52,12 +52,12 @@ ip link set dev wg0 xdp obj /home/rodrigo/server-mv/xdp/xdp_wg_mtu_optimizer.o s
# então esse link precisa ser reaplicado se a rede "dockernet" for recriada. # então esse link precisa ser reaplicado se a rede "dockernet" for recriada.
ip link set dev br-02f7820020b4 xdp obj /home/rodrigo/server-mv/xdp/bridge_xdp.o sec xdp ip link set dev br-02f7820020b4 xdp obj /home/rodrigo/server-mv/xdp/bridge_xdp.o sec xdp
# eBPF sockmap: DESATIVADO DE PROPÓSITO. Causou timeout/connection reset # eBPF sockmap: DESATIVADO DE PROPÓSITO. O bug de porta (byte-order
# em conexões container-a-container (nginx->redis/mariadb/anatomia-web), # assimétrico entre local_port/remote_port em extract_key_from_ops e
# incluindo depois de corrigir o bug de NAT (masquerade sem -o enp0s6, # bpf_msg_redirect) já foi corrigido e resolveu conexões pequenas/médias,
# já corrigido em nftables.conf) — ou seja, o bug é na lógica de # mas arquivos grandes (streaming sustentado) ainda travam — suspeita de
# match/redirect do sk_msg em bpf/sockmap.bpf.c, não só no NAT. Não # backpressure/sendfile no caminho de redirect do sk_msg, ainda em
# reativar em produção sem antes rastrear com bpf_printk num ambiente # investigação com bpf_printk. Não reativar em produção até confirmar.
# de teste. Ganho é só performance (bypass de socket local entre # Ganho é só performance (bypass de socket local entre containers no
# containers no mesmo host); o site funciona normalmente sem isso. # mesmo host); o site funciona normalmente sem isso.
# cd /home/docker/ebpf-sockmap/ && make load # cd /home/rodrigo/server-mv/ebpf-sockmap/ && make load