249 lines
12 KiB
C
249 lines
12 KiB
C
// 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 <bpf/bpf_helpers.h>
|
|
#include <bpf/bpf_endian.h>
|
|
|
|
// ─────────────────────────────────────────────────────────
|
|
// 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);
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────
|
|
// Verifica se um IP (em network byte order / big-endian) é
|
|
// endereço privado RFC-1918 ou loopback.
|
|
//
|
|
// 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…).
|
|
// 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.
|
|
// Só registramos conexões onde ambos os lados são privados
|
|
// (i.e., comunicação intra-host / intra-container).
|
|
// ─────────────────────────────────────────────────────────
|
|
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
|
|
if (o1 == 127) return true; // 127.0.0.0/8 loopback
|
|
return false;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────
|
|
// 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.
|
|
static __always_inline void
|
|
extract_key_from_ops(struct bpf_sock_ops *ops, struct sock_key *key)
|
|
{
|
|
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 = ops->family;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────
|
|
// 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;
|
|
|
|
// Só processamos conexões IPv4
|
|
if (skops->family != 2) // AF_INET
|
|
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: {
|
|
// ── 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)) {
|
|
stats_inc(3); // conta conexões externas ignoradas
|
|
return 0;
|
|
}
|
|
|
|
struct sock_key key = {};
|
|
extract_key_from_ops(skops, &key);
|
|
|
|
// 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 = {};
|
|
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.
|
|
// ─────────────────────────────────────────────────────────
|
|
SEC("sk_msg")
|
|
int bpf_msg_redirect(struct sk_msg_md *msg)
|
|
{
|
|
// 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".
|
|
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 = msg->family;
|
|
|
|
// 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";
|