fixed4341
This commit is contained in:
1 parent
28b7aa2266
commit
f0d697f7e5
1 file changed
+43
-71
@@ -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
|
// 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).
|
// (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
|
// 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
|
// Evidência que confirmou isso: em produção, o Nextcloud (Apache
|
||||||
// dual-stack) tinha 0 de 184 conexões aceitas registradas no sockhash —
|
// 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,
|
// 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
|
// 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
|
// 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
|
// como AF_INET6 do lado de quem aceitou.
|
||||||
// "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
|
// Primeira tentativa de correção: extrair o IPv4 mapeado dos campos
|
||||||
// (::ffff:a.b.c.d, RFC 4291 §2.5.5.2: 80 bits zero + 16 bits 0xffff +
|
// local_ip6/remote_ip6 (::ffff:a.b.c.d). Essa rota foi ABANDONADA — o
|
||||||
// 32 bits do IPv4) e extrai o IPv4 equivalente.
|
// 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
|
// Correção que FUNCIONA: os campos local_ip4/remote_ip4 continuam
|
||||||
// o endereço de um campo array do ctx (ex: &ops->local_ip6[0]) pra uma
|
// preenchidos pelo kernel mesmo quando family reporta AF_INET6 — Linux
|
||||||
// função separada é "aritmética de ponteiro modificado no ctx", que o
|
// mantém sk->sk_rcv_saddr/sk_daddr (a representação IPv4) atualizados
|
||||||
// verificador do BPF rejeita (bpf_sock_ops/sk_msg_md só permitem acesso
|
// mesmo em sockets dual-stack, para compatibilidade. Então simplesmente
|
||||||
// direto a campos em offsets conhecidos, reescrito pelo compilador;
|
// ignoramos o valor de "family" pra fins de extração de IP e sempre
|
||||||
// tirar o endereço de um array de campo quebra esse reconhecimento).
|
// lemos local_ip4/remote_ip4 — sem tocar em nenhum campo ip6, sem
|
||||||
// Cada chamador lê os 4 elementos diretamente (ops->local_ip6[0] etc)
|
// esbarrar na limitação do verificador.
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────
|
||||||
// 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.
|
||||||
@@ -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
|
// 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.
|
||||||
// Retorna false se a conexão não puder ser reduzida a um par IPv4 (IPv6
|
// Retorna false só se family não for AF_INET nem AF_INET6 (IPv6 nativo
|
||||||
// nativo de verdade, fora do escopo deste programa) — quem chama deve
|
// de verdade, ou algo inesperado) — quem chama trata isso como "não é
|
||||||
// tratar isso como "não é pra registrar", igual ao antigo filtro de
|
// pra registrar". Ver o bloco de comentário acima sobre por que
|
||||||
// family, só que agora sem descartar dual-stack por engano.
|
// AF_INET6 é aceito e por que os campos ip6 não são usados.
|
||||||
static __always_inline bool
|
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)
|
||||||
{
|
{
|
||||||
__u32 local_ip4, remote_ip4;
|
if (ops->family != SOCKMAP_AF_INET && ops->family != SOCKMAP_AF_INET6)
|
||||||
|
|
||||||
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 {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
key->sip4 = local_ip4;
|
// local_ip4/remote_ip4 lidos direto, sem checar family — ver
|
||||||
key->dip4 = remote_ip4;
|
// 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->sport = bpf_htonl(ops->local_port) >> 16;
|
||||||
key->dport = ops->remote_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;
|
key->family = SOCKMAP_AF_INET;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -203,11 +182,12 @@ int bpf_sockops_handler(struct bpf_sock_ops *skops)
|
|||||||
{
|
{
|
||||||
__u32 op = skops->op;
|
__u32 op = skops->op;
|
||||||
|
|
||||||
// Aceita AF_INET e AF_INET6 aqui (o filtro real de família acontece
|
// Aceita AF_INET e AF_INET6 aqui — cortar por family==AF_INET logo no
|
||||||
// dentro de extract_key_from_ops, que resolve IPv6 mapeado pra IPv4
|
// topo, como a versão antiga fazia, foi o que descartava
|
||||||
// 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.
|
// 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)
|
if (skops->family != SOCKMAP_AF_INET && skops->family != SOCKMAP_AF_INET6)
|
||||||
return 0;
|
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
|
// 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". Mesma
|
// convertido pra esse domínio via "bpf_htonl(...) >> 16". Mesmo
|
||||||
// resolução de IPv6-mapeado-pra-IPv4 também — precisa bater com o
|
// tratamento de family (aceita AF_INET6, lê local_ip4/remote_ip4 do
|
||||||
// que extract_key_from_ops grava no REGISTRO, senão o par nunca é
|
// mesmo jeito) — precisa bater com o que extract_key_from_ops grava
|
||||||
// encontrado (era exatamente o bug do Nextcloud dual-stack).
|
// no REGISTRO, senão o par nunca é encontrado.
|
||||||
__u32 sip4, dip4;
|
if (msg->family != SOCKMAP_AF_INET && msg->family != SOCKMAP_AF_INET6) {
|
||||||
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 {
|
|
||||||
stats_inc(2);
|
stats_inc(2);
|
||||||
return SK_PASS;
|
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 = {};
|
struct sock_key key = {};
|
||||||
key.sip4 = sip4; // IP remoto = IP do par
|
key.sip4 = msg->remote_ip4; // IP remoto = IP do par
|
||||||
key.dip4 = dip4; // IP local = nosso IP
|
key.dip4 = msg->local_ip4; // 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 = SOCKMAP_AF_INET; // normalizado, ver extract_key_from_ops
|
key.family = SOCKMAP_AF_INET; // normalizado, ver extract_key_from_ops
|
||||||
|
|||||||
Reference in new issue
Block a user