// SPDX-License-Identifier: GPL-2.0 // // sockmap.bpf.c — Programa eBPF para acelerar comunicação entre containers Docker // // Este programa faz duas coisas: // 1) sock_ops: intercepta conexões TCP sendo estabelecidas e registra // os sockets num mapa (sockhash). // 2) sk_msg: quando um socket registrado envia dados, redireciona // diretamente para o socket do outro lado, PULANDO toda a pilha // de rede do kernel (TCP/IP, netfilter, bridge, veth, etc). // // Resultado: comunicação entre containers no MESMO host fica muito mais rápida // porque os dados vão direto de um socket para o outro na memória. #include "vmlinux.h" #include #include // ───────────────────────────────────────────────────────── // Chave que identifica uma conexão TCP (4-tupla + família) // ───────────────────────────────────────────────────────── struct sock_key { __u32 sip4; // IP de origem (source IP) __u32 dip4; // IP de destino (destination IP) __u32 sport; // Porta de origem (source port) __u32 dport; // Porta de destino (destination port) __u32 family; // Família do socket (AF_INET = 2) }; // ───────────────────────────────────────────────────────── // Mapa SOCKHASH: armazena sockets indexados pela chave acima // Capacidade: 65535 entradas (conexões simultâneas) // ───────────────────────────────────────────────────────── struct { __uint(type, BPF_MAP_TYPE_SOCKHASH); __uint(max_entries, 65535); __type(key, struct sock_key); __type(value, int); } sock_hash SEC(".maps"); // ───────────────────────────────────────────────────────── // Mapa para estatísticas // Index 0 = conexões registradas no sockhash // Index 1 = mensagens redirecionadas com sucesso (caminho acelerado) // Index 2 = fallback: par não encontrado (caminho normal) // Index 3 = conexões ignoradas (IPs externos/filtrados) // ───────────────────────────────────────────────────────── struct { __uint(type, BPF_MAP_TYPE_ARRAY); __uint(max_entries, 4); __type(key, __u32); __type(value, __u64); } stats SEC(".maps"); // Incrementa um contador de estatísticas static __always_inline void stats_inc(__u32 idx) { __u64 *val = bpf_map_lookup_elem(&stats, &idx); if (val) __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. // // 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. // // 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. // // 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 seja, tráfego ENTRE containers na // dockernet, que é o único caso que se beneficia do sockmap. // // Por que isso importa: // O sock_ops roda para TODAS as conexões TCP do cgroup raiz, // incluindo tráfego externo (HTTPS do mundo, metadata AWS…) e // tráfego de LOOPBACK dentro de um único container. // Registrar IPs públicos no sockhash é inútil e perigoso: // o par nunca será encontrado, o sk_msg vai ao fallback e // entradas mortas poluem o mapa. // Loopback (127.0.0.0/8) é EXCLUÍDO de propósito: conexão de um // processo consigo mesmo (ou com outro processo no MESMO container) // já não atravessa bridge/veth/netfilter — não tem nada a ganhar com // o bypass do sockmap, só risco. Foi justamente uma rajada de // centenas de mensagens grandes pro loopback do nginx (127.0.0.6) // que apareceu correlacionada com travamentos de página em produção. // Só registramos conexões onde ambos os lados são privados // RFC-1918 (i.e., comunicação DE VERDADE entre containers). // ───────────────────────────────────────────────────────── static __always_inline bool is_local_ip(__u32 ip_be) { // local_ip4 / remote_ip4 são big-endian (network byte order). // Em x86 (little-endian) o valor numérico da u32 aparece // byte-invertido: para 10.15.0.45 (0A 0F 00 2D em memória), // a u32 lida vale 0x2D000F0A — ou seja, ip_be >> 24 = 45 (ERRADO!). // bpf_ntohl() converte para host order, colocando o primeiro // octeto (10) no byte mais significativo, onde >> 24 extrai // corretamente 10. __u32 ip = bpf_ntohl(ip_be); __u8 o1 = (__u8)(ip >> 24); __u8 o2 = (__u8)(ip >> 16); if (o1 == 10) return true; // 10.0.0.0/8 if (o1 == 172 && o2 >= 16 && o2 <= 31) return true; // 172.16.0.0/12 if (o1 == 192 && o2 == 168) return true; // 192.168.0.0/16 return false; } // ───────────────────────────────────────────────────────── // DECISÃO DE ESCOPO (depois de 4 rodadas de bugs encontrados e // corrigidos — porta, dual-stack, loopback, tamanho — todos legítimos, // mas o problema de fundo continuou aparecendo especificamente em // tráfego HTTP via proxy: nginx↔Nextcloud, nginx↔Atlas, nginx↔OnlyOffice, // sempre — "upstream prematurely closed connection" / timeout, mesmo em // respostas pequenas, em serviços sem nenhuma relação entre si): // // Em TODOS os testes, sem uma única exceção, tráfego banco/cache // (MariaDB, Redis) redirecionou correto e nunca corrompeu nada — nem em // rajada, nem sob concorrência. Tráfego HTTP via proxy reverso // (streaming, HTTP/2, keep-alive, chunked) é o único padrão onde o // problema aparece, e é mais sensível a qualquer perturbação sutil no // fluxo de bytes. // // Em vez de continuar caçando o bug exato do mecanismo de redirect do // kernel, restringimos o REGISTRO no sockhash a conexões onde um dos // lados é porta de banco/cache. Tráfego HTTP nunca entra no mapa — // nunca é candidato a redirect, sempre usa o caminho normal de rede // (comprovadamente 100% íntegro, inclusive em teste isolado de 500MB // via nc). Perde-se a aceleração pra proxy HTTP; mantém-se pro tipo de // tráfego (muitos round-trips pequenos de banco/cache) onde o ganho de // latência é maior E onde já provamos que funciona sem risco. // // IMPORTANTE: key.sport/key.dport NÃO são o número literal da porta — // já passam pela transformação bpf_htonl(porta)>>16 (ver // extract_key_from_ops). Comparar direto com 3306/6379 nunca bateria. // Os valores abaixo são o RESULTADO dessa mesma transformação aplicada // às portas reais, verificados nos traces de produção desta sessão // (ex: MariaDB sempre apareceu com dport/sport=59916, nunca 3306) e // confirmados by matemática: bpf_htonl(3306)>>16 = 0xEA0C = 59916; // bpf_htonl(6379)>>16 = 0xEB18 = 60184. // ───────────────────────────────────────────────────────── #define SOCKMAP_PORT_MARIADB_ENC 59916 // 3306 já transformado #define SOCKMAP_PORT_REDIS_ENC 60184 // 6379 já transformado static __always_inline bool is_accelerated_key(struct sock_key *key) { return key->sport == SOCKMAP_PORT_MARIADB_ENC || key->dport == SOCKMAP_PORT_MARIADB_ENC || key->sport == SOCKMAP_PORT_REDIS_ENC || key->dport == SOCKMAP_PORT_REDIS_ENC; } // ───────────────────────────────────────────────────────── // Extrai a chave de conexão a partir do contexto sock_ops // (usado quando uma conexão TCP é estabelecida) // ───────────────────────────────────────────────────────── // BUG CORRIGIDO: o código antigo misturava dois domínios de codificação // de porta sem converter os dois pro mesmo formato. // - ops->local_port vem em HOST byte order (puro, ex: 6379) // - ops->remote_port vem com o valor em network byte order alojado nos // 16 bits SUPERIORES de um campo de 32 bits — não é um u32 comum em // network order, então bpf_ntohl() sozinho faz um byte-swap completo // de 32 bits e produz um número sem relação com a porta real. // A comparação `sport == local_port do outro lado` só funciona se AMBOS // os valores estiverem no MESMO domínio. Em vez de tentar decifrar o // valor "verdadeiro" da porta (o que exigiria confiar numa convenção que // não dá pra validar sem testar em kernel real), a correção normaliza os // dois pro domínio de remote_port: extrai remote_port com ">> 16" (sem // ntohl) e converte local_port PRA esse mesmo domínio com // "bpf_htonl(local_port) >> 16". Assim os dois lados de uma conexão // 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 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) { if (ops->family != SOCKMAP_AF_INET && ops->family != SOCKMAP_AF_INET6) return false; // 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; key->family = SOCKMAP_AF_INET; return true; } // ───────────────────────────────────────────────────────── // PROGRAMA 1: sock_ops // // Anexado ao cgroup, é chamado em eventos de socket TCP. // Quando uma conexão é ESTABELECIDA (ativa ou passiva), // registramos o socket no sockhash. // ───────────────────────────────────────────────────────── SEC("sockops") int bpf_sockops_handler(struct bpf_sock_ops *skops) { __u32 op = skops->op; // 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; switch (op) { // Conexão passiva: servidor aceitou uma conexão de um cliente 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). if (!is_local_ip(key.sip4) || !is_local_ip(key.dip4)) { stats_inc(3); // conta conexões externas ignoradas return 0; } // ── Escopo: só banco/cache (ver comentário em is_accelerated_key) ── if (!is_accelerated_key(&key)) { stats_inc(3); return 0; } 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); if (ret == 0) { stats_inc(0); // ── Hints de performance TCP ────────────────────────── // Ativa todos os callbacks relevantes de uma vez: // STATE_CB_FLAG : notifica mudanças de estado TCP // (necessário para limpar sockets fechados) // RTT_CB_FLAG : callback a cada RTT (permite ajuste dinâmico) __u32 flags = skops->bpf_sock_ops_cb_flags | BPF_SOCK_OPS_STATE_CB_FLAG | BPF_SOCK_OPS_RTT_CB_FLAG; bpf_sock_ops_cb_flags_set(skops, flags); } break; } // Mudança de estado TCP: limpa o mapa em qualquer transição // que indique encerramento, para não deixar sockets mortos. case BPF_SOCK_OPS_STATE_CB: { __u32 new_state = skops->args[1]; if (new_state == BPF_TCP_CLOSE || new_state == BPF_TCP_CLOSE_WAIT || new_state == BPF_TCP_FIN_WAIT1 || new_state == BPF_TCP_FIN_WAIT2 || new_state == BPF_TCP_TIME_WAIT) { struct sock_key key = {}; if (extract_key_from_ops(skops, &key)) bpf_map_delete_elem(&sock_hash, &key); } break; } // RTT callback: pode ser usado no futuro para métricas por conexão. // Por ora não fazemos nada aqui — o callback foi ativado para // manter o kernel notificando o sock_ops sobre a saúde da conexão. case BPF_SOCK_OPS_RTT_CB: break; default: break; } return 0; } // ───────────────────────────────────────────────────────── // PROGRAMA 2: sk_msg // // Anexado ao sockhash, é chamado quando qualquer socket // registrado envia dados (sendmsg/sendfile). // Redireciona os dados diretamente para o socket par, // pulando toda a pilha de rede. // ───────────────────────────────────────────────────────── // Acima deste tamanho, NÃO tentamos redirecionar — vai direto pro // fallback (caminho normal de rede, já comprovado correto e íntegro por // teste isolado de 500MB via nc). Motivo: evidência em produção (trace // ao vivo) mostrou que rajadas grandes e sustentadas pelo caminho // acelerado (BPF_F_INGRESS do sk_msg) corrompem a integridade do byte // stream o suficiente pra quebrar framing HTTP/2 no cliente // (ERR_HTTP2_PROTOCOL_ERROR), mesmo com bpf_msg_redirect_hash // retornando sucesso (SK_PASS) em 100% dos casos — ou seja, o redirect // "funciona" do ponto de vista da própria chamada, mas os dados que // chegam do outro lado saem malformados. Não é bug de chave/family (já // validamos ambos corretos); é uma limitação real do mecanismo de // redirect desta kernel sob volume alto. Mensagens pequenas (onde a // aceleração realmente importa — round-trips de banco/cache) continuam // sendo redirecionadas normalmente; só streams grandes (modelos 3D, // downloads) ficam de fora do caminho acelerado. #define SOCKMAP_MAX_REDIRECT_SIZE 16384 SEC("sk_msg") int bpf_msg_redirect(struct sk_msg_md *msg) { if (msg->size > SOCKMAP_MAX_REDIRECT_SIZE) { stats_inc(2); return SK_PASS; } // Construímos a chave do socket PAR (invertendo src/dst). // Se A→B enviou dados, queremos encontrar no mapa a entrada // que B registrou: {sip=B, dip=A, sport=portaB, dport=portaA} // // 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". 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 = 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 // Cinto e suspensório: mesmo que o registro já esteja restrito a // banco/cache, confere de novo aqui — garante que nenhum tráfego // HTTP tenta redirect mesmo que uma entrada antiga/inesperada exista. if (!is_accelerated_key(&key)) { stats_inc(2); return SK_PASS; } // Tenta redirecionar diretamente para o socket par. int ret = bpf_msg_redirect_hash(msg, &sock_hash, &key, BPF_F_INGRESS); if (ret == SK_PASS) { stats_inc(1); // Redirecionamento OK — caminho acelerado return SK_PASS; } // FALLBACK CRÍTICO: se o par não está no mapa (socket ainda não // registrado, já fechou, ou conexão externa ao cgroup), deixamos // os dados fluírem pelo caminho normal de rede em vez de // descartar (SK_DROP), o que causaria ERR_CONNECTION_CLOSED. stats_inc(2); return SK_PASS; } // Licença GPL obrigatória para usar helpers BPF char _license[] SEC("license") = "GPL";