From f0d697f7e524b007ac83f1fee938344dd91207db Mon Sep 17 00:00:00 2001 From: Rodrigo Bertoldo de Sousa Martins Date: Mon, 14 Sep 2026 14:43:47 -0300 Subject: [PATCH] fixed4341 --- ebpf-sockmap/bpf/sockmap.bpf.c | 114 +++++++++++++-------------------- 1 file changed, 43 insertions(+), 71 deletions(-) diff --git a/ebpf-sockmap/bpf/sockmap.bpf.c b/ebpf-sockmap/bpf/sockmap.bpf.c index e29a874..4e00fd7 100644 --- a/ebpf-sockmap/bpf/sockmap.bpf.c +++ b/ebpf-sockmap/bpf/sockmap.bpf.c @@ -68,40 +68,31 @@ static __always_inline void stats_inc(__u32 idx) // 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. +// sendo, na prática, IPv4. // // 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. +// como AF_INET6 do lado de quem aceitou. // -// 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. +// Primeira tentativa de correção: extrair o IPv4 mapeado dos campos +// local_ip6/remote_ip6 (::ffff:a.b.c.d). Essa rota foi ABANDONADA — o +// verificador do BPF desta kernel (7.1.12 UEK/aarch64) rejeita QUALQUER +// leitura desses dois campos do contexto ("dereference of modified ctx +// ptr ... disallowed"), não importa como o C é escrito (array, ponteiro, +// valores separados). Parece ser uma lacuna real do conversor de acesso +// ao contexto desta kernel especificamente pra bpf_sock_ops/sk_msg_md, +// não algo contornável só reescrevendo o programa. // -// Recebe os 4 words POR VALOR (w0..w3), não um ponteiro/array — passar -// o endereço de um campo array do ctx (ex: &ops->local_ip6[0]) pra uma -// função separada é "aritmética de ponteiro modificado no ctx", que o -// verificador do BPF rejeita (bpf_sock_ops/sk_msg_md só permitem acesso -// direto a campos em offsets conhecidos, reescrito pelo compilador; -// tirar o endereço de um array de campo quebra esse reconhecimento). -// Cada chamador lê os 4 elementos diretamente (ops->local_ip6[0] etc) -// e passa os valores, não o array. -static __always_inline bool ipv4_mapped(__u32 w0, __u32 w1, __u32 w2, __u32 w3, __u32 *out_ip4) -{ - if (w0 != 0 || w1 != 0) - return false; - if (bpf_ntohl(w2) != 0x0000ffff) - return false; - *out_ip4 = w3; - return true; -} - +// Correção que FUNCIONA: os campos local_ip4/remote_ip4 continuam +// preenchidos pelo kernel mesmo quando family reporta AF_INET6 — Linux +// mantém sk->sk_rcv_saddr/sk_daddr (a representação IPv4) atualizados +// mesmo em sockets dual-stack, para compatibilidade. Então simplesmente +// ignoramos o valor de "family" pra fins de extração de IP e sempre +// lemos local_ip4/remote_ip4 — sem tocar em nenhum campo ip6, sem +// esbarrar na limitação do verificador. // ───────────────────────────────────────────────────────── // Verifica se um IP (em network byte order / big-endian) é // endereço privado RFC-1918 ou loopback. @@ -156,37 +147,25 @@ 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. -// 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. +// Retorna false só se family não for AF_INET nem AF_INET6 (IPv6 nativo +// de verdade, ou algo inesperado) — quem chama trata isso como "não é +// pra registrar". Ver o bloco de comentário acima sobre por que +// AF_INET6 é aceito e por que os campos ip6 não são usados. static __always_inline bool extract_key_from_ops(struct bpf_sock_ops *ops, struct sock_key *key) { - __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[0], ops->local_ip6[1], ops->local_ip6[2], ops->local_ip6[3], &local_ip4) || - !ipv4_mapped(ops->remote_ip6[0], ops->remote_ip6[1], ops->remote_ip6[2], ops->remote_ip6[3], &remote_ip4)) - return false; - } else { + if (ops->family != SOCKMAP_AF_INET && ops->family != SOCKMAP_AF_INET6) return false; - } - key->sip4 = local_ip4; - key->dip4 = remote_ip4; + // local_ip4/remote_ip4 lidos direto, sem checar family — ver + // explicação acima. Se algum dia isso deixar de valer (kernel + // diferente que realmente zera esses campos em conexões IPv6), o + // sintoma volta a ser is_local_ip() rejeitando 0.0.0.0, caindo com + // segurança no fallback (nunca quebra, só perde a aceleração). + key->sip4 = ops->local_ip4; + key->dip4 = ops->remote_ip4; key->sport = bpf_htonl(ops->local_port) >> 16; key->dport = ops->remote_port >> 16; - // 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; } @@ -203,11 +182,12 @@ int bpf_sockops_handler(struct bpf_sock_ops *skops) { __u32 op = skops->op; - // 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 + // Aceita AF_INET e AF_INET6 aqui — 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. + // Ver o comentário grande acima de extract_key_from_ops pra entender + // por que os dois são tratados igual (local_ip4/remote_ip4 valem + // pros dois casos). if (skops->family != SOCKMAP_AF_INET && skops->family != SOCKMAP_AF_INET6) return 0; @@ -296,28 +276,20 @@ 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". 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[0], msg->remote_ip6[1], msg->remote_ip6[2], msg->remote_ip6[3], &sip4) || - !ipv4_mapped(msg->local_ip6[0], msg->local_ip6[1], msg->local_ip6[2], msg->local_ip6[3], &dip4)) { - stats_inc(2); // IPv6 de verdade — nunca deveria estar no mapa, mas por segurança cai no fallback - return SK_PASS; - } - } else { + // convertido pra esse domínio via "bpf_htonl(...) >> 16". Mesmo + // tratamento de family (aceita AF_INET6, lê local_ip4/remote_ip4 do + // mesmo jeito) — precisa bater com o que extract_key_from_ops grava + // no REGISTRO, senão o par nunca é encontrado. + if (msg->family != SOCKMAP_AF_INET && msg->family != SOCKMAP_AF_INET6) { stats_inc(2); return SK_PASS; } + // local_ip4/remote_ip4 lidos direto, sem checar family — mesma razão + // documentada em extract_key_from_ops. struct sock_key key = {}; - key.sip4 = sip4; // IP remoto = IP do par - key.dip4 = dip4; // IP local = nosso IP + key.sip4 = msg->remote_ip4; // IP remoto = IP do par + key.dip4 = msg->local_ip4; // IP local = nosso IP key.sport = msg->remote_port >> 16; key.dport = bpf_htonl(msg->local_port) >> 16; key.family = SOCKMAP_AF_INET; // normalizado, ver extract_key_from_ops