
Como Envoy xDS, pero para filtros eBPF
Como Envoy xDS, pero para filtros eBPF.
Netfence se ejecuta como un daemon en tus hosts de VM/contenedores e inyecta automáticamente programas de filtro eBPF en cgroups e interfaces de red, con un servidor DNS integrado que resuelve dominios permitidos y llena la lista de IPs permitidas.
Los daemons de Netfence pueden manejarse a través de su API local de socket Unix, o conectarse a un plano de control central que implementes vía gRPC para sincronizar listas permitidas/denegadas con tu backend.
Tu plano de control envía reglas de red como ALLOW *.pypi.org o ALLOW 10.0.0.0/16 a las interfaces/cgroups adjuntos. Cuando una VM/contenedor consulta DNS, Netfence lo resuelve, añade las IPs al filtro eBPF, y descarta el tráfico hacia IPs desconocidas antes de que salga del host, con una sobrecarga de ruta caliente que es efectivamente indistinguible de una conexión de socket normal en los benchmarks actuales.
En modo lista permitida, el enlace local IPv4 (169.254.0.0/16) ya no está permitido automáticamente
por defecto — por lo que el servicio de metadatos en la nube (169.254.169.254) está bloqueado a menos
que se incluya explícitamente en la lista permitida. Esto es deliberado: el servicio de metadatos es un
objetivo de robo de credenciales, y las cargas de trabajo en sandbox no deben poder alcanzarlo
implícitamente. Localhost (127.0.0.0/8, ::1) y el descubrimiento de vecinos IPv6
(fe80::/10, ff02::/16) permanecen permitidos por defecto para que la conectividad básica y NDP
sigan funcionando. Para permitir el servicio de metadatos para una carga de trabajo, incluya
169.254.169.254/32 en la lista permitida (una anulación de exclusión por adjunto a través del plano de control
es un seguimiento planificado).
El broadcast IPv4 (255.255.255.255) y el multicast (224.0.0.0/4) no tienen exclusión y están sujetos a la política, por lo que en modo TC lista permitida el tráfico como las renovaciones DHCP broadcast se bloquea a menos que se incluya explícitamente en la lista permitida. Las comprobaciones de exclusión se ejecutan antes de la lista denegada, por lo que un rango excluido solo puede bloquearse desactivando su exclusión — y dado que el enlace local IPv4 ahora está desactivado por defecto, el modo lista denegada también puede bloquear el servicio de metadatos.
Algunos beneficios importantes de esta solución que otras opciones normalmente no soportan:
secretdata.someattacker.com)Hasta donde sé, ninguna otra solución ofrece todas estas características juntas.
Limitación conocida: los adjuntos a cgroup filtran a nivel de socket (hooks connect/sendmsg),
por lo que un proceso con CAP_NET_RAW puede crear paquetes sin procesar que los eviten. Use
un adjunto TC (interfaz), que filtra a nivel de dispositivo, para cargas de trabajo que
puedan tener CAP_NET_RAW.
Sin embargo, esto tiene un poco más de sobrecarga que algo como httpjail.
Estos números se midieron en el gate Linux Docker privilegiado en linux/arm64
usando make bench-docker. Los valores son medianas de cinco muestras.
El benchmark de socket caliente utiliza sockets UDP conectados para aislar el
costo del hook eBPF cgroup/connect4 de la latencia del handshake TCP. En esta ruta, DNS ya
ha resuelto el dominio, la IP aún está dentro del TTL, y la IP/CIDR ya
está presente en el mapa eBPF.
| Ruta | Latencia mediana |
|---|---|
| Conexión de socket normal, sin eBPF | ~2.647 us |
| Lista permitida caliente, acierto LPM protegido | ~2.691 us |
| Lista permitida caliente, acierto de host exacto DNS | ~2.741 us |
| Fallo en lista permitida, bloqueo local | ~1.652 us |
La dispersión medida entre las rutas de conexión normal, LPM protegido y host exacto DNS está dentro del ruido de la muestra.
No existe la ruta "fallo en kernel pregunta al proceso padre" hoy. Un fallo en lista permitida de cgroup es decidido localmente por eBPF y se bloquea inmediatamente.
Estos números miden la ruta del servidor DNS, no la ruta de conexión de socket caliente.
| Ruta | Latencia mediana |
|---|---|
| Consulta proxy en frío, función de política en proceso | ~31.336 us |
| Consulta proxy en caliente | ~27.964 us |
| Consulta lista permitida en frío con upstream local | ~53.510 us |
| Consulta lista permitida en caliente con upstream local | ~53.432 us |
Las filas en frío se sincronizan a través de la barrera de mutación de adjunto real y borran
el gráfico de propiedad del benchmark y la instantánea del mapa exacto falsa entre consultas.
El temporizador se ejecuta continuamente para preservar la localidad del planificador UDP, mientras que ns/op
resta el fixture-reset-ns/op de tiempo de pared reportado por separado (incluyendo
cualquier cola del manejador anterior después de que el cliente recibiera su paquete) y por lo tanto
mide el Exchange actual del cliente. raw-total-ns/op reporta ambos juntos.
El restablecimiento preserva los dominios de política configurados y el almacenamiento de respaldo, y el
benchmark afirma una adición física al mapa exacto por consulta. Las filas en caliente preparan
la propiedad una vez y afirman una adición física a lo largo de la ejecución.
Los microbenchmarks internos de propiedad a continuación son diagnósticos de escalabilidad, no filas de aceptación de ruta de consulta DNS de extremo a extremo. El helper en caché se conserva solo para pruebas y benchmarks; envuelve un registro a la vez y repite la validación de dominio. Tanto este tráfico como el del resolvedor normal atraviesan la barrera de mutación de adjunto, mientras que el tráfico normal del resolvedor admite cada respuesta completa como una transacción.
| Diagnóstico interno de escalabilidad | Mediana actual | Memoria / asignaciones |
|---|---|---|
| Admisión de clave nueva en frío, gráfico de propiedad vacío | ~370.3 ns | 232 B, 5 allocs/op |
| Admisión de clave nueva en frío, 4095 entradas no relacionadas | ~451.2 ns | 232 B, 5 allocs/op |
| Presión de capacidad física y reemplazo LRU | ~3.820 ms | ~4.23 MB (4,226,243 B), 4,336 allocs/op |
| Comprobación previa de presupuesto físico agotado | ~611.9 ns | 344 B, 9 allocs/op |
| Presión máxima de bordes, respuesta de 64 direcciones | ~6.849 ms | ~7.66 MB (7,658,774 B), 2,233 allocs/op |
| Guarda de trabajo máximo de gráfico, plan completo permitido | ~2.763 ms | ~4.26 MB (4,264,386 B), 3,074 allocs/op |
| Guarda de trabajo máximo de gráfico, rechazo por proyección agotada | ~10.935 us | 8.76 KB (8,760 B), 14 allocs/op |
| Operación de presupuesto de rotación cerca del techo numérico | ~18.98 ns | 0 B, 0 allocs/op |
| Instantánea coherente de estadísticas de propiedad | ~2.094 ns | 0 B, 0 allocs/op |
| Escaneo de expiración sin operación en 4095 entradas | ~74.849 us/scan | 0 B, 0 allocs/op |
+------------------+ +-------------------------+ | Your Control |<------->| Daemon (per host) | | Plane (gRPC) | stream | | +------------------+ | +-------------------+ | | | DNS Server | | | | (per-attachment) | | | +-------------------+ | +-------------------------+ | +------+------+ | | TC Filter Cgroup Filter (veth, eth) (containers)
Cada adjunto obtiene una dirección DNS única (puerto) aprovisionada por el daemon. Los contenedores/VMs deben configurarse para usar su dirección DNS asignada; filtrar el tráfico DNS normal de la carga de trabajo no lo redirige de forma transparente.
### Topología y comportamiento del resolver DNS
`dns.listen_addr` debe identificar una dirección IPv4 o IPv6 concreta a la que cada
carga de trabajo adjunta pueda llegar. Las direcciones comodín se rechazan porque
no pueden ser anunciadas como endpoints del resolver. Un nombre de host configurado se resuelve
una vez que el daemon se inicia y la IP concreta resultante se utiliza para el enlace,
anuncio, persistencia y arranque del filtro. El valor predeterminado `127.0.0.1` es
apropiado solo cuando la carga de trabajo comparte el espacio de nombres de red del daemon; un
contenedor o VM en otro espacio de nombres normalmente necesita una dirección de host/puente alcanzable en su lugar.```yaml
dns:
listen_addr: 10.0.0.1
port_min: 11000
port_max: 11500
# Daemon-global fallback when DnsConfig.upstream_servers is empty.
upstream: 1.1.1.1:53
# Hard daemon ceilings for each attachment's bounded DNS exact ownership.
# Zero/unset uses these defaults (max_ips_per_family instead derives from
# filter.max_dns_rule_entries).
max_ips_per_family: 4096
max_ips_per_response: 64
max_ips_per_policy_domain: 1024
max_tracked_domains: 1024
max_ownership_edges: 8192
# Rolling physical-admission/LRU mutation budget and slow-planning work
# allowance. The window is daemon-global and immutable until restart;
# DnsConfig.max_churn_units may only lower the daemon ceiling.
max_churn_units: 8192
churn_window: 1m
Attach devuelve la dns_address concreta; configure esa dirección exacta como el
resolutor de la carga de trabajo. Netfence instala una entrada de permiso /32 o
/128 protegida y no caducante para la IP del listener, de modo que el modo de lista blanca pueda arrancar sin
una regla DNS-IP del plano de control. Los filtros actuales aplican prefijos IP, no
puertos de destino, por lo que esa entrada protegida permite cada puerto en la IP del listener
(no solo su puerto DNS); esto es especialmente importante para modelos de amenaza de adjunción a cgroups. Utilice una IP de listener dedicada cuando esa accesibilidad más amplia no sea
aceptable.
El punto final asignado sirve tanto UDP como TCP. Las respuestas UDP se truncan al
límite de 512 bytes de un cliente heredado o al tamaño EDNS anunciado y llevan TC cuando
es necesario, permitiendo que la carga de trabajo reintente el mismo punto final a través de TCP. Para la resolución ascendente,
una respuesta UDP truncada se reintenta a través de TCP contra el mismo
ascendente primero. Una falla de transporte, SERVFAIL o REFUSED avanza entonces al
siguiente ascendente configurado en orden.
DnsConfig.upstream_servers anula el dns.upstream global del daemon para una
adjunción. Las entradas usan sintaxis host:port (literales IPv6 entre corchetes), se
canonicalizan y deduplican en orden de primera aparición, y están limitadas a ocho servidores
únicos. Una lista vacía selecciona la reserva global.
En modos de filtrado, Netfence elimina los parámetros ipv4hint e ipv6hint de las
respuestas HTTPS/SVCB, incluyendo sus referencias mandatory correspondientes,
porque las direcciones sugeridas no han pasado la admisión del filtro de forma independiente.
El modo deshabilitado conserva las respuestas ascendentes sin cambios.
Los contadores de consultas DNS son mutuamente excluyentes: dns_queries_allowed cuenta
consultas permitidas por política y respondidas con éxito (incluyendo NXDOMAIN),
dns_queries_blocked cuenta respuestas REFUSED por política y
dns_queries_errors cuenta resolución, proxy, admisión de filtro, escritura de respuesta y
otras rutas de error. Una consulta incrementa exactamente un depósito.
Cada respuesta que lleva direcciones en un modo de filtrado DNS es admitida en el
nivel HASH IPv4/IPv6 exacto de la adjunción como una transacción antes de que cualquier
dirección A/AAAA de sus secciones de respuesta, autoridad o adicional sea devuelta. Si
la respuesta completa no puede ser representada, el resolutor devuelve SERVFAIL
sin dirección y conserva el conjunto de trabajo previamente admitido. Las decisiones PROXY
que devuelven direcciones deben establecer add_to_filter; de lo contrario también fallan
en cerrado como SERVFAIL. El modo DNS deshabilitado es la excepción explícita de paso directo.
Las entradas exactas llevan bordes TTL desde la consulta normalizada hasta el propietario de la política coincidente. Eliminar o denegar un dominio elimina rápidamente sus últimas direcciones exactas solo DNS, mientras que una dirección compartida sobrevive a otro propietario de consulta activo y un CIDR superpuesto del plano de control continúa independientemente en el nivel LPM protegido. Las respuestas de permitir por defecto y permitir explícito de DENYLIST DNS también se rastrean, incluso mientras DENYLIST de paquetes ignora los permitidos exactos, por lo que un cambio posterior a modo ALLOWLIST puede usar direcciones en caché ya devueltas sin una consulta repetida.
Todo el estado de propiedad del espacio de usuario normal/activo está limitado por las cinco configuraciones de propiedad
anteriores. Los bordes provisionales sintéticos restaurados están exentos de esos
límites lógicos para que no puedan ser olvidados antes de la reconciliación, pero permanecen
limitados por los mapas exactos IPv4/IPv6 físicos. Los dominios de política configurados y los dominios de consulta activa comparten
max_tracked_domains, y cada registro TTL (consulta, propietario coincidente, IP) consume
una ranura max_ownership_edges.
Bajo presión, la admisión proyecta primero los bordes TTL caducados. Luego reclama
bordes lógicos completos de consulta/propietario con la menor garantía física antes
de la actualidad, seguido de LRU determinista observado por el resolutor (las IP canónicas rompen
empates). Una expulsión física elimina toda la clave DNS exacta y todos sus propietarios
DNS. La IP física entrante y el borde exacto entrante (IP, consulta, propietario) están protegidos
para la transacción de respuesta. La propiedad provisional restaurada protege su clave física hasta la reconciliación autoritativa,
pero los metadatos DNS normales no relacionados que comparten esa clave aún pueden ser reclamados.
Los permite autoritativos del plano de control/sistema y cada denegación permanecen en niveles LPM separados y
nunca son candidatos para la reclamación DNS.
El presupuesto de cambio continuo por adjunción cobra una unidad por una nueva clave física
exacta y una por cada clave física DNS activa expulsada; un reemplazo completo de antiguo a nuevo
por lo tanto cuesta dos. Las actualizaciones, la reclamación solo lógica, la caducidad y la eliminación por política cuestan cero. Los eventos permanecen activos mientras su edad sea menor que
dns.churn_window y caducan en el límite exacto. DnsConfig.max_churn_units
solo puede reducir el techo del daemon; cero lo hereda. La ventana no puede ser
cambiada por el plano de control, y cambiar el techo/ventana del daemon requiere un
reinicio. Reducir y luego aumentar un límite de adjunción no olvida el
historial aún activo.
Un libro de trabajo continuo separado limita la costosa planificación del grafo de propiedad.
Las actualizaciones rápidas y las admisiones ordinarias nunca lo tocan. Antes de que una ruta de presión clone
o analice el grafo, Netfence cobra unidades de trabajo estables derivadas de las claves
físicas actuales, bordes de propiedad, dominios rastreados y tamaño de respuesta en relación con
sus techos inmutables de daemon/mapa. Ese intento de cobro se retiene incluso cuando
el plan resulta imposible o una transacción de mapa exacto posterior falla, cerrando
la ruta de reintento sin mutación para presión de CPU/asignación sin cambiar la
contabilidad de cambio físico transaccional anterior. Con los techos predeterminados, la
asignación admite ocho pases de grafo equivalentes máximos por ventana; reducir
DnsConfig.max_churn_units retiene al menos uno. Reducir y luego aumentar el límite
nunca reescala u olvida el historial de trabajo activo.
Cuando ningún estado DNS elegible puede satisfacer un límite, o se agota cualquiera de las dos asignaciones continuas,
Netfence conserva el conjunto de trabajo admitido y devuelve SERVFAIL
sin devolver la dirección no admitida. Las fallas de capacidad incrementan
map_full_drops; los estrangulamientos de cambio físico y planificación de trabajo no lo hacen.
Los latidos exponen el mapa exacto actual, la capacidad y los valores máximos de generación de procesos
más las expulsiones LRU DNS acumuladas, todas las fallas de admisión y un recuento
agregado de estrangulamiento de presupuesto que cubre ambas guardas continuas. Los registros de presión/recuperación de
capacidad, presupuesto físico y presupuesto de trabajo tienen límite de velocidad independiente.
Los operadores pueden esperar la recuperación de TTL/ventana, reducir la rotación de respuesta/dominio o los
intentos repetidos de presión de capacidad, o aumentar DnsConfig.max_churn_units hasta el
techo daemon dns.max_churn_units. Aumentar el techo del daemon requiere un
reinicio; aumentar filter.max_dns_rule_entries también requiere recrear la adjunción porque los mapas fijados no se pueden redimensionar en el lugar.
Los CIDR autoritativos del plano de control y las reglas del sistema del daemon utilizan cuatro
mapas LPM independientes y no expulsables: permitir/denegar × IPv4/IPv6. Cada mapa tiene
ranuras filter.max_rule_entries. El arranque /32 o /128 del listener DNS es un
permiso del sistema y cuenta contra el mapa de permisos protegidos correspondiente. Las entradas de host exacto DNS
permanecen en sus mapas separados y no pueden consumir estas ranuras. Ninguna regla protegida se expulsa por LRU: los permisos explícitos, las reglas del sistema
y cada denegación permanecen hasta una eliminación autorizada o un reemplazo completo.
Un SubscribedAck o BulkUpdate completo se canonicaliza y su ocupación
final se verifica para los cuatro mapas antes de la mutación. La capacidad se basa en el
estado final, por lo que reemplazar claves en un mapa lleno es válido; el estado sobredimensionado se
rechaza sin expulsar o aceptar parcialmente reglas. Los supervivientes no se eliminan ni se vuelven a agregar.
Si una llamada al sistema de mapa posterior falla, Netfence restaura y verifica el inventario exacto de los cuatro mapas anterior a la llamada. El modo probado después de la reversión
es el modo antiguo o BLOCK_ALL (normalmente BLOCK_ALL), por lo que el daemon aún mantiene la adjunción
en fallo en cerrado hasta que un reintento autoritativo completo tenga éxito.
El estado de seguridad de la política protegida se persiste con la adjunción y se exporta en
latidos. BLOCK_ALL por sí mismo es un modo configurado normal y saludable:
policy_degraded es falso cuando policy_degraded_reason está vacío. Una mutación protegida riesgosa
que comienza mientras BLOCK_ALL está saludable primero registra en diario
protected_policy_mutation_in_progress. Esto es un diario de fallos transitorio, no
un diagnóstico de fallo estable: la operación en vivo puede publicar su modo previsto
antes de la última guarda de borrado del diario, y la finalización exitosa borra el
diario mismo. Si el inicio lo encuentra después de un fallo, el inicio primero fuerza y
prueba BLOCK_ALL, luego persiste
protected_policy_mutation_interrupted. Los códigos de razón de degradación estables son:
protected_policy_mutation_interruptedauthoritative_protected_policy_failedincremental_deny_install_failedincremental_allow_removal_failedincremental_mode_change_failedEstas son clasificaciones estables, nunca texto sin procesar de llamada al sistema/almacén. El
diario en curso es un límite interno de fallo persistido; las estadísticas de latidos
se serializan con la mutación propietaria y por lo tanto observan ya sea su borrado exitoso
o una conversión de fallo estable, no el diario intermedio en vivo.
Las razones de degradación/interrupción estables mantienen la aplicación de paquetes en
BLOCK_ALL probado y rechazan comandos incrementales de CIDR y modo de paquete.
Los cambios de configuración DNS independientes y la caducidad TTL DNS pueden continuar bajo
esa retención probada, pero no pueden borrar la razón estable ni reactivar la política de paquete.
La recuperación de una razón estable requiere un estado deseado completo de LPM y DNS:
aplicar BulkUpdate a través del plano de control o la API local (o responder a un
Subscribed fresco de una adjunción restaurada con SubscribedAck).
Netfence prepara el estado protegido completo,
aplica el estado DNS autoritativo, activa el modo solicitado y borra
la razón duradera solo después de que cada paso tenga éxito. Prefiera un command_id único
en un BulkUpdate de recuperación del plano de control y requiera un
CommandResult exitoso; la API local rechaza command_id porque su RPC unario
ya informa éxito o fallo.
Los latidos exponen las entradas físicas actuales, la capacidad máxima y el
máximo de generación del daemon de forma independiente para los cuatro mapas protegidos. El
arranque está incluido; las entradas fijadas adoptadas inicializan el máximo de la nueva generación.
map_full_drops es acumulativo e incluye rechazos de capacidad protegida. Si una lectura de ocupación falla, el daemon retiene la última instantánea probada y emite una advertencia como máximo una vez cada 30 segundos en lugar de inventar nuevos recuentos. Para recuperarse de la presión, reduzca las reglas deseadas completas por debajo de cada capacidad por mapa y reintente la actualización completa. Aumentar filter.max_rule_entries es solo en tiempo de carga y requiere recrear una adjunción fijada existente. Si el daemon no puede probar BLOCK_ALL o registrar de forma duradera su marcador de seguridad, detiene la admisión de mutación; repare la falla de mapa/almacén y reinicie en lugar de asumir que la aplicación se reabrió.
Ejecute el daemon, el cual:
DaemonService) para adjunciones, política e inspecciónControlPlane.Connect)Inicie el daemon:```bash
netfenced start
netfenced start --config /etc/netfence/config.yaml
**Verificar el estado del daemon:**```bash
netfenced status
Sin un control_plane.url, un nuevo adjunto se confirma en modo paquete/DNS deshabilitado y puede configurarse inmediatamente a través de la API local o CLI. No se requiere ningún proceso de plano de control para el flujo de trabajo independiente documentado en “Por adjunto”.
La API gRPC local no tiene autenticación por RPC. El acceso al sistema de archivos de su socket Unix es el límite de autorización, y cada proceso que pueda conectarse es un administrador de red de host completamente confiable: puede adjuntar o desvincular programas eBPF del host, reemplazar políticas de paquetes y DNS, y abrir o cerrar tráfico de carga de trabajo. Mantenga restringida la membresía del grupo del socket y proteja el directorio padre del socket.```yaml
socket: /run/netfence/netfence.sock
socket_group: netfence-admin
`NETFENCE_SOCKET` y `NETFENCE_SOCKET_GROUP` son las variables de entorno equivalentes. Al iniciar, el daemon vincula el socket en un directorio de staging privado, establece su grupo y modo `0660` mientras es inalcanzable, y luego lo publica atómicamente. El daemon elimina cualquier socket Unix preexistente en el destino configurado — no distingue un socket obsoleto de uno propiedad de otro daemon activo — por lo que exactamente un daemon debe poseer una ruta de socket. Se niega a eliminar un objetivo que no sea un socket. En Linux, el renombrado sin reemplazo evita sobrescribir una nueva ruta creada después de esa eliminación; el apagado elimina la ruta publicada solo mientras aún identifica el inodo del socket propio del daemon. Un grupo inválido, fallo de propiedad/modo, o un objetivo que no sea un socket, falla al iniciar sin publicar un endpoint permisivo.
### Seguridad del transporte del plano de control (TLS / mTLS / token portador)
El canal del plano de control es la superficie de ataque de mayor valor en el sistema (quien lo controle puede enviar reglas `ALLOW` a cada carga de trabajo), por lo que el daemon **falla cerrado**: si `control_plane.url` está configurado, la configuración debe elegir explícitamente un transporte — ya sea un bloque `control_plane.tls` o `control_plane.insecure: true`. Una URL sin ninguno es rechazada al iniciar; no hay un valor predeterminado implícito en texto plano. (Este es un cambio de comportamiento deliberado: versiones anteriores se conectaban silenciosamente al plano de control sin cifrar.)```yaml
control_plane:
url: cp.internal:443
tls:
# CA bundle used to verify the control-plane server certificate.
# Path to a PEM file or inline PEM; omit to use the system root pool.
ca: /etc/netfence/cp-ca.pem
# Client certificate + key (path or inline PEM). Setting BOTH enables
# mTLS: the daemon presents this cert to the control plane. Setting only
# one is a config error.
cert: /etc/netfence/daemon.pem
key: /etc/netfence/daemon.key
# Optional hostname override for server certificate verification (SNI),
# e.g. when dialing by IP.
server_name: cp.internal
# Optional bearer token, sent as `authorization: Bearer <token>` metadata
# on every control-plane RPC. Refused on a plaintext channel unless
# `insecure: true` was explicitly set (so a misconfiguration can't leak it).
auth_token: "..."
TLS solo con raíces del sistema (certificado de servidor emitido por CA pública, sin mTLS) es solo un bloque vacío:```yaml control_plane: url: cp.example.com:443 tls: {}
Plaintext para desarrollo local es una opción explícita (mutuamente excluyente con `tls`):```yaml
control_plane:
url: localhost:9000
insecure: true
Los certificados y las claves se cargan una sola vez al inicio, por lo que una ruta/PEM incorrecta provoca un error claro al arrancar en lugar de aparecer en cada reconexión.
El daemon envía pings keepalive HTTP/2 en la conexión del plano de control para que una ruta muerta silenciosamente (tirón de cable, mapeo NAT caído, ruta agujero negro) sea detectada y derribada en aproximadamente keepalive_time + keepalive_timeout — en lugar de permanecer en CONNECTED durante minutos hasta el tiempo de espera de retransmisión TCP del kernel mientras cada consulta DNS proxyada consume todo su tiempo de espera. Las reconexiones se espacian mediante un retroceso exponencial con fluctuación (comienza en 1s, se duplica, fluctuación ±20%, limitado a reconnect_backoff_max); el retroceso se reinicia al mínimo solo después de que una conexión se haya mantenido saludable durante 30 segundos, por lo que un plano de control que acepta conexiones y las suelta inmediatamente sigue retrocediendo en lugar de ser golpeado en el mínimo.```yaml
control_plane:
keepalive_time: 30s
keepalive_timeout: 10s
reconnect_backoff_max: 30s
Los valores cero/no establecidos significan los valores predeterminados — **no** deshabilitan el keepalive ni el backoff. Su plano de control debe permitir esta cadencia de ping en su política de cumplimiento de gRPC keepalive (ver más abajo), o rechazará el daemon con `ENHANCE_YOUR_CALM (too_many_pings)`.
### Reinicios, fallos y actualizaciones del daemon (estado BPF fijado)
El daemon fija los enlaces BPF y los mapas de reglas de cada attachment a bpffs (`filter.bpf_pin_dir`, predeterminado `/sys/fs/bpf/netfence`, un directorio por ID de attachment). Debido a que el estado fijado es mantenido por el kernel — no por el proceso del daemon — **la aplicación continúa mientras el daemon está inactivo**: un fallo (`kill -9`), una parada controlada o una actualización deja aplicando la última política conocida (modo + todas las reglas), y el siguiente inicio del daemon readopta el estado fijado tal cual. La restauración nunca vuelve a adjuntar o reescribe los mapas en vivo, por lo que no hay una ventana en la que una carga de trabajo en lista blanca esté bloqueada o un destino bloqueado esté permitido, y no hay attachment duplicado.
El comportamiento de parada es una configuración explícita (`filter.detach_on_stop`):```yaml
filter:
# false (default): stopping the daemon KEEPS ENFORCING — filters stay
# attached via their bpffs pins and are re-adopted on the next start
# (fail-closed across restarts/upgrades).
# true: stopping the daemon detaches filters and removes their pins —
# traffic is unfiltered while the daemon is down (explicit fail-open).
detach_on_stop: false
# bpffs directory for pinned state. Must be on a bpffs mount; the daemon
# mounts bpffs at /sys/fs/bpf if needed (privileged). An explicit "" turns
# pinning off entirely (BPF state then dies with the process).
bpf_pin_dir: /sys/fs/bpf/netfence
# Capacity of each authoritative/system LPM map (allowed/denied per family).
# Protected entries are non-evictable; the DNS listener bootstrap consumes
# one slot in its address family. Changing pinned-map capacity requires
# recreating the attachment.
max_rule_entries: 4096
# Independent capacity of each DNS-derived exact-host HASH map (IPv4/IPv6).
# These entries can never consume or evict authoritative/deny capacity.
max_dns_rule_entries: 4096
Un Detach explícito (RPC/CLI), o la eliminación de un objetivo activo de propiedad coherente, destruye el estado anclado junto con el attachment. Al reiniciar, la ausencia del objetivo no autoriza suposiciones: los pines persistentes futuros, no confirmados, mixtos o de otro modo no verificables se conservan y el inicio se aborta para inspección.
Los directorios de pines son un formato de persistencia versionado. El marcador de esquema se ancla al final, solo después de que existan todos los mapas y enlaces requeridos. Al actualizar un attachment de nivel pre-exacto, se anclan los dos nuevos mapas exactos vacíos con un marcador en progreso, se reemplaza atómicamente el programa de cada enlace mientras se reutilizan los mapas autoritativos activos, se verifica la identidad programa/mapa y se confirma el marcador al final. Un bloqueo o una actualización ambigua deja el marcador sin confirmar; en el siguiente inicio, se vuelven a actualizar todos los enlaces utilizando los mismos mapas. Las generaciones de programas antiguas y nuevas aplican la misma política LPM autoritativa durante ese estado mixto acotado, por lo que la migración nunca desancla ni recrea un filtro viable. Los conjuntos de pines desconocidos, incompletos o no verificables se conservan y abortan el inicio para inspección, en lugar de ser adivinados.
Notas sobre el estado readoptado:
SyncRequest, luego una declaración Subscribed completa para cada attachment restaurado que aún necesita reconciliación. Responda con un SubscribedAck fresco: su modo, CIDRs, TTL y configuración DNS son el estado deseado completo. El daemon aplica un delta (los CIDR sin cambios nunca se eliminan) y borra el marcador de restauración solo después de que se aplique toda la confirmación. Un tiempo de espera, desconexión o fallo de validación deja la aplicación sin cambios. Un fallo de aplicación de filtro/mapa/DNS/almacenamiento puede dejar un delta parcial, pero la reconciliación no utiliza una limpieza masiva del mapa ni elimina/vuelve a agregar supervivientes sin cambios; el marcador de restauración permanece establecido y el daemon reintenta después de una conexión posterior.SubscribedAck fresco; esa confirmación autoritativa reemplaza exactamente sus tiempos de vida, incluido acortar un plazo o convertir una regla provisionalmente permanente nuevamente en una regla de TTL finito. Las claves DNS exactas restauradas se inventarian y representan mediante propietarios provisionales acotados (las capacidades reales del mapa anclado son el límite); la primera configuración DNS autoritativa descarta toda reclamación sintética, elimina las claves que quedan sin propietario y conserva una clave solo cuando tiene por separado un propietario activo normal. Un inventario no canónico/en conflicto aborta la restauración sin adivinar ni publicar parcialmente metadatos de propiedad.netfenced apply-rules después de cada reinicio del daemon para reemplazar el estado de paquete provisional y restaurar su estado deseado completo y TTL.REFUSED hasta que se aplique un BulkUpdate o SubscribedAck completo. Un modo DNS explícitamente DISABLED permanece reenviando. Si alguno de los listeners UDP/TCP confirmados muere inesperadamente más tarde, el attachment se pone en cuarentena en IP BLOCK_ALL y se reporta como un error de cancelación de suscripción.Su sistema de orquestación llama a la API local del daemon.
RPC:``` DaemonService.Attach(interface_name: "veth123", tc_direction: TC_DIRECTION_INGRESS, metadata: {vm_id: "abc"}) // or DaemonService.Attach(cgroup_path: "/sys/fs/cgroup/...", metadata: {container_id: "xyz"})
**CLI:**```bash
# Attach to a host-side veth peer or VM tap (TC) - use ingress direction
netfenced attach --interface veth123 --direction ingress --metadata vm_id=abc
# Attach to a cgroup
netfenced attach --cgroup /sys/fs/cgroup/... --metadata container_id=xyz
# Attach to an uplink inside the workload's own netns (TC) - egress is the default
netfenced attach --interface eth0 --metadata tenant=acme,env=prod
Dirección de TC: el campo tc_direction (CLI --direction) selecciona a qué hook de TCX se adjunta el filtro, y elegir el correcto depende de en qué lado del enlace se encuentra la interfaz:
| Interfaz | Dirección correcta | Por qué |
|---|---|---|
Enlace ascendente (p. ej. eth0), o cualquier interfaz dentro del propio netns de la carga de trabajo | egress (predeterminado) | Los paquetes salientes de la carga de trabajo se transmiten a través de él; su dirección de destino es el destino real. |
Par veth del lado del host o VM tap (p. ej. fcr-*) | ingress | Los paquetes salientes de la carga de trabajo llegan al host en esa interfaz. El egress allí vería en su lugar el tráfico de retorno host→carga de trabajo y filtraría por la dirección propia de la carga de trabajo en lugar del destino real. |
La dirección solo se aplica a los adjuntos de interfaz (TC); se ignora para los adjuntos de cgroup.
control_plane.url, el daemon envía Subscribed{id, target, type, metadata} y espera SubscribedAck con la configuración inicial (modo, CIDRs, reglas DNS). Si el plano de control no responde dentro del tiempo de espera (predeterminado 5s, configurable mediante control_plane.subscribe_ack_timeout), el adjunto se revierte y la llamada de adjunto falla. Las fallas de validación y otras previas a la confirmación siguen la misma regla de reversión ordinaria.BLOCK_ALL duradero en lugar de revertirlo de manera destructiva. Con un tiempo de espera limitado, Attach devuelve un error que contiene el ID del adjunto retenido; el llamante puede descubrir ese ID coincidiendo con el objetivo en List, y el plano de control debe recuperarlo con un BulkUpdate completo.subscribe_ack_timeout: 0, un nuevo Attach regresa después de poner en cola Subscribed; un ack posterior aún se valida y aplica. Este valor cero no deshabilita la reconciliación de adjuntos restaurados: los intentos de restauración esperan hasta 5s en segundo plano y reintentan en una conexión posterior si es necesario. Si ese ack posterior encuentra presión protegida, el adjunto ya devuelto se retiene en BLOCK_ALL; SubscribedAck no emite ni CommandResult ni error Unsubscribed, y el daemon no vuelve a impulsar automáticamente la declaración antes del reinicio. Detecte policy_degraded más su razón, ocupación/capacidad y map_full_drops en Heartbeat, luego envíe un BulkUpdate completo con command_id para obtener un resultado de recuperación explícito.Unsubscribed automáticamenteRPC:``` DaemonService.Detach(id)
**CLI:**```bash
netfenced detach --id <attachment-id>
Listar archivos adjuntos:```bash netfenced list netfenced list --all # fetch all pages
### Política local e inspección
Cada mutación local es una codificación CLI delgada de la única RPC
`DaemonService.ApplyCommand(ControlCommand)`. Proporcione el ID de archivo adjunto
devuelto por `attach`:```bash
# Packet policy and protected CIDRs.
netfenced set-mode <id> allowlist
netfenced allow-cidr <id> 10.0.0.0/8
netfenced allow-cidr <id> 192.0.2.10/32 --ttl 5m
netfenced deny-cidr <id> 10.20.0.0/16
netfenced remove-cidr <id> 10.20.0.0/16 --list deny
# --list accepts allow, deny, or both (the default).
# DNS policy.
netfenced set-dns-mode <id> denylist
netfenced allow-domain <id> example.com --subdomains
netfenced deny-domain <id> blocked.example.com
netfenced remove-domain <id> blocked.example.com
# Deterministic current-policy inspection as protobuf JSON.
netfenced rules <id>
Los modos de paquete son disabled, allowlist, denylist y block-all; los modos DNS son disabled, allowlist, denylist y proxy. El modo DNS proxy requiere un plano de control configurado y accesible. La coincidencia de dominios utiliza el sufijo de coincidencia más específico; cuando reglas de permitir y denegar igualmente específicas coinciden, gana la denegación. Las CIDR y los dominios se canonican. Los TTL, enumeraciones, CIDR, dominios, selectores y mensajes anidados negativos, mal formados o de otro modo inválidos son rechazados antes de la mutación, por lo que un comando inválido es una no-operación de política.
Para un reemplazo completo, apply-rules lee la forma JSON existente del protobuf BulkUpdate desde un archivo o stdin:```bash
cat >rules.json <<'JSON'
{
"mode": "POLICY_MODE_ALLOWLIST",
"allowCidrs": [{"cidr": "10.0.0.0/8"}],
"dns": {
"mode": "DNS_MODE_DENYLIST",
"denyDomains": [{"domain": "blocked.example.com", "includeSubdomains": true}]
}
}
JSON
netfenced apply-rules --file rules.json
netfenced apply-rules --file - < rules.json
`ApplyCommand` solo acepta `SetMode`, `AllowCIDR`, `DenyCIDR`, `RemoveCIDR`, `BulkUpdate`, `SetDnsMode`, `AllowDomain`, `DenyDomain` y `RemoveDomain`. Las variantes de sync/ack solo de flujo, comandos desconocidos o vacíos, y valores locales de `command_id` son rechazados. `BulkUpdate` es también la única operación local que puede recuperar una política de paquetes degradada estable; debe contener el estado deseado completo de LPM y DNS.
El selector opcional `ControlCommand.remove_cidr_list` puede apuntar a la lista de permitidos, la lista de denegados, o ambas cuando la variante de comando es `RemoveCIDR`. Su valor heredado no especificado y el explícito `BOTH` ambos eliminan de ambas listas, preservando el comportamiento original del protocolo.
Las mutaciones locales y del plano de control comparten un analizador sintáctico, barrera de mutación, registro TTL, ruta de recuperación de fallo cerrado y propietario de política. Deliberadamente no hay arbitraje de propiedad entre local y plano de control: las operaciones conflictivas en una lista de políticas individual surten efecto en su orden de confirmación, independientemente de la fuente. En particular, una `BulkUpdate` completa posterior del plano de control o `SubscribedAck` puede reemplazar el estado local.
`GetRules`/`netfenced rules` devuelve una instantánea coherente y determinista del registro de espacio de usuario. Cada CIDR reporta lista de permitidos/denegados, `policyOwned` local o del plano de control, `systemOwned` del daemon, `expiresAt` absoluto, `provisional` restaurado, y el último estado `installed` del kernel confirmado. Una entrada instalada sin propietario es un reintento de eliminación fallido, no una política deseada. La salida DNS es la `DnsConfig` efectiva normalizada en vivo. La inspección intencionalmente no enumera mapas protegidos del kernel ni expone entradas de caché de DNS exact-host resueltas dinámicamente; use la telemetría de latido para la ocupación de mapas protegidos.
## En el plano de control (tú implementas esto)
Implementa `ControlPlane.Connect` RPC - un flujo bidireccional:
Configura la política de cumplimiento de keepalive de tu servidor gRPC para permitir la cadencia de ping del daemon (`control_plane.keepalive_time`, por defecto 30s): establece `MinTime` en o por debajo de ese intervalo y `PermitWithoutStream: true`. La política predeterminada de gRPC (5 minutos) trata los pings del daemon como abusivos y cierra la conexión con `ENHANCE_YOUR_CALM (too_many_pings)`. En Go:```go
grpc.NewServer(grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
MinTime: 10 * time.Second,
PermitWithoutStream: true,
}))
Recibir del demonio:
SyncRequest al conectar/reconectar (enumera los adjuntos actuales)Subscribed cuando se añaden nuevos adjuntos, y después de SyncRequest para adjuntos restaurados que aún necesitan estado autoritativo frescoUnsubscribed cuando se eliminan adjuntosHeartbeat con estadísticasCommandResult{command_id, id, success, error} — resultado de cualquier comando que hayas enviado con un command_id no vacío (nonce de correlación opcional en ControlCommand; los comandos sin uno no producen resultado). success es verdadero solo si el comando se aplicó completamente — un BulkUpdate aplicado parcialmente informa fallo con el error agregado. Los resultados son de mejor esfuerzo: trata un resultado faltante como desconocido, no como fallido.Enviar al demonio:
SyncAck después de recibir SyncRequestSubscribedAck{mode, cidrs, dns_config} después de recibir Subscribed (requerido - el demonio espera esto)SetMode{mode} - cambiar el modo de política de filtro IPAllowCIDR{cidr, ttl} / DenyCIDR / RemoveCIDR (opcionalmente seleccionar permitir, denegar o ambos; no especificado conserva el comportamiento heredado de "ambos")SetDnsMode{mode} - cambiar el modo de filtrado DNSAllowDomain{dominio} / DenyDomain / RemoveDomain (la coincidencia más específica gana; denegar gana en un empate de igual especificidad)BulkUpdate{mode, cidrs, dns_config} - sincronización completa de estadoCuando el plano de control recibe Subscribed, debe responder con un SubscribedAck completo. Para un nuevo adjunto, el demonio normalmente espera ese acuse antes de devolver éxito al llamante local. Para un adjunto restaurado, el handshake se ejecuta en segundo plano mientras la política fijada conocida sigue aplicándose. Usa los metadatos para identificar la VM/inquilino/contenedor y devolver el modo completo deseado, CIDRs (incluyendo TTLs) y el estado DNS; una configuración DNS omitida significa deshabilitado con listas de dominio vacías.
SyncRequest es el punto de reconciliación autoritativo: en cada (re)conexión es el primer evento en el flujo y lista el conjunto completo de adjuntos actuales del demonio. Reconciliá tu vista contra él — añade adjuntos que no conocías, elimina los que faltan en la lista. Al reconectar, el demonio purga eventos que estaban en cola contra la conexión anterior (la sincronización los reemplaza), por lo que no verás Heartbeats obsoletos, Unsubscribeds para adjuntos ya ausentes de la sincronización, o CommandResults de la conexión muerta reproducidos después. Dos casos límite permanecen por diseño, y tu plano de control DEBE manejarlos idempotentemente:
Subscribed puede seguir a un SyncRequest que ya lista el mismo id. Esto ocurre cuando el acuse de un nuevo adjunto estaba pendiente durante la reconexión, y deliberadamente para cada adjunto restaurado hasta que un acuse autoritativo se aplique completamente. Trátalo como una actualización, responde con un SubscribedAck completo y fresco, y nunca lo descartes como duplicado. SyncRequest reconcilia el inventario de adjuntos; SubscribedAck reconcilia la política deseada.Unsubscribed para un id de adjunto desconocido o ya eliminado como una no-operación.AllowCIDR/DenyCIDR, y las listas CIDR en SubscribedAck/BulkUpdate) llevan un TTL opcional. Las reglas con TTL son eliminadas por un recolector del demonio una vez que expiran (intervalo de escaneo ttl_janitor_interval, por defecto 1s); las reglas sin TTL son permanentes.AllowCIDR/DenyCIDR extienden un CIDR hasta la fecha límite posterior — nunca acortan una — y una readición incremental sin TTL la hace permanente. En contraste, el estado completo en SubscribedAck/BulkUpdate reemplaza exactamente cada vida útil del plano de control, por lo que la reconciliación autoritativa puede acortar un TTL o cambiar permanente a finito sin eliminar/readicionar la entrada del mapa activo. Usa RemoveCIDR para eliminar una regla incremental temprano.dns.min_filter_ttl (por defecto 60s; cero/no establecido significa el valor por defecto, no "sin límite"). Por lo tanto, un TTL aguas arriba de cero vive durante el límite inferior; un TTL PROXY omitido se establece explícitamente por defecto a 300s antes de aplicar el límite inferior. Una regla CIDR permanente o de mayor duración que cubra la misma dirección permanece instalada independientemente en el nivel LPM protegido cuando la propiedad exacta del DNS expira.filter.max_rule_entries por adjunto (por defecto 4096 cada uno); ver “Capacidad CIDR protegida y recuperación de fallo-cerrado” arriba. Las direcciones de host derivadas de DNS usan mapas HASH de coincidencia exacta separados, dimensionados por filter.max_dns_rule_entries (por defecto 4096 por familia IP), por lo que no pueden consumir ni desalojar capacidad autoritativa o de denegar. La admisión completa de respuesta DNS valida/canonicaliza cada dirección y verifica la capacidad física y lógica antes de la mutación. Un error del kernel del mapa exacto DNS restaura su instantánea exacta anterior a la llamada; si esa reversión no puede ser probada, el resolvedor suprime la respuesta y pone en cuarentena el adjunto en IP duradero BLOCK_ALL antes de aceptar otra mutación.