diff --git a/ebpf-sockmap/bpf/sockmap.bpf.c b/ebpf-sockmap/bpf/sockmap.bpf.c index 7024e8f..a81cc1d 100644 --- a/ebpf-sockmap/bpf/sockmap.bpf.c +++ b/ebpf-sockmap/bpf/sockmap.bpf.c @@ -60,6 +60,39 @@ static __always_inline void stats_inc(__u32 idx) __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) é // 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 // qual seja a codificação exata do kernel — o que importa é que os dois // 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) { - key->sip4 = ops->local_ip4; - key->dip4 = ops->remote_ip4; + __u32 local_ip4, 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->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; - // Só processamos conexões IPv4 - if (skops->family != 2) // AF_INET + // Aceita AF_INET e AF_INET6 aqui (o filtro real de família acontece + // 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; switch (op) { @@ -145,25 +207,24 @@ int bpf_sockops_handler(struct bpf_sock_ops *skops) case BPF_SOCK_OPS_PASSIVE_ESTABLISHED_CB: // Conexão ativa: cliente conectou a um servidor 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 ─────── // 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 - // little-endian o primeiro octeto fica no byte menos significativo. - if (!is_local_ip(skops->local_ip4) || !is_local_ip(skops->remote_ip4)) { + if (!is_local_ip(key.sip4) || !is_local_ip(key.dip4)) { stats_inc(3); // conta conexões externas ignoradas return 0; } - struct sock_key key = {}; - extract_key_from_ops(skops, &key); - - // 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); + if (skops->family == SOCKMAP_AF_INET6) + bpf_printk("REG via IPv6 mapeado: op=%u sip=%x dip=%x", op, key.sip4, key.dip4); // BPF_ANY: atualiza mesmo se já existir (cobre reconexões keepalive) int ret = bpf_sock_hash_update(skops, &sock_hash, &key, BPF_ANY); - bpf_printk("REG update ret=%d", ret); if (ret == 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_TIME_WAIT) { struct sock_key key = {}; - extract_key_from_ops(skops, &key); - // DEBUG TEMPORÁRIO - bpf_printk("DEL state=%u sport=%u dport=%u", new_state, key.sport, key.dport); - bpf_map_delete_elem(&sock_hash, &key); + if (extract_key_from_ops(skops, &key)) + bpf_map_delete_elem(&sock_hash, &key); } 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 // 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 = {}; - key.sip4 = msg->remote_ip4; // IP remoto = IP do par - key.dip4 = msg->local_ip4; // IP local = nosso IP + key.sip4 = sip4; // IP remoto = IP do par + key.dip4 = dip4; // IP local = nosso IP key.sport = msg->remote_port >> 16; key.dport = bpf_htonl(msg->local_port) >> 16; - key.family = msg->family; - - // 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); + key.family = SOCKMAP_AF_INET; // normalizado, ver extract_key_from_ops // Tenta redirecionar diretamente para o socket par. int ret = bpf_msg_redirect_hash(msg, &sock_hash, &key, BPF_F_INGRESS); - bpf_printk("MSG redirect ret=%d", ret); if (ret == SK_PASS) { stats_inc(1); // Redirecionamento OK — caminho acelerado return SK_PASS; diff --git a/rc.local b/rc.local index f914fb8..da0977f 100755 --- a/rc.local +++ b/rc.local @@ -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. 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 -# em conexões container-a-container (nginx->redis/mariadb/anatomia-web), -# incluindo depois de corrigir o bug de NAT (masquerade sem -o enp0s6, -# já corrigido em nftables.conf) — ou seja, o bug é na lógica de -# match/redirect do sk_msg em bpf/sockmap.bpf.c, não só no NAT. Não -# reativar em produção sem antes rastrear com bpf_printk num ambiente -# de teste. Ganho é só performance (bypass de socket local entre -# containers no mesmo host); o site funciona normalmente sem isso. -# cd /home/docker/ebpf-sockmap/ && make load +# eBPF sockmap: DESATIVADO DE PROPÓSITO. O bug de porta (byte-order +# assimétrico entre local_port/remote_port em extract_key_from_ops e +# bpf_msg_redirect) já foi corrigido e resolveu conexões pequenas/médias, +# mas arquivos grandes (streaming sustentado) ainda travam — suspeita de +# backpressure/sendfile no caminho de redirect do sk_msg, ainda em +# investigação com bpf_printk. Não reativar em produção até confirmar. +# Ganho é só performance (bypass de socket local entre containers no +# mesmo host); o site funciona normalmente sem isso. +# cd /home/rodrigo/server-mv/ebpf-sockmap/ && make load