fixed4339
This commit is contained in:
1 parent
26d0220ecb
commit
13d0584df9
2 files changed
+108
-37
No files matched your search
@@ -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,9 +251,7 @@ 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_printk("DEL state=%u sport=%u dport=%u", new_state, key.sport, key.dport);
|
|
||||||
bpf_map_delete_elem(&sock_hash, &key);
|
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;
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in new issue
Block a user