
Comme Envoy xDS, mais pour les filtres eBPF
Comme Envoy xDS, mais pour les filtres eBPF.
Netfence s'exécute en tant que démon sur vos hôtes de VM/containers et injecte automatiquement des programmes de filtrage eBPF dans les cgroups et les interfaces réseau, avec un serveur DNS intégré qui résout les domaines autorisés et remplit la liste blanche d'adresses IP.
Les démons Netfence peuvent être pilotés uniquement via leur API locale sur socket Unix, ou se connecter à un plan de contrôle central que vous implémentez via gRPC pour synchroniser les listes blanches/noires avec votre backend.
Votre plan de contrôle envoie des règles réseau comme ALLOW *.pypi.org ou ALLOW 10.0.0.0/16 aux interfaces/cgroups attachés. Quand une VM/container interroge le DNS, Netfence le résout, ajoute les IP au filtre eBPF, et bloque le trafic vers des IP inconnues avant qu'il ne quitte l'hôte, avec une surcharge de chemin préchauffé pratiquement indistinguable d'un socket connect normal dans les benchmarks actuels.
En mode liste blanche, le lien-local IPv4 (169.254.0.0/16) n'est plus automatiquement autorisé par défaut — donc le service de métadonnées cloud (169.254.169.254) est bloqué sauf s'il est explicitement ajouté à la liste blanche. C'est délibéré : le service de métadonnées est une cible de vol d'identifiants, et les charges de travail en bac à sable ne doivent pas pouvoir l'atteindre implicitement. Localhost (127.0.0.0/8, ::1) et la découverte de voisinage IPv6 (fe80::/10, ff02::/16) restent autorisés par défaut pour que la connectivité de base et le NDP continuent de fonctionner. Pour autoriser le service de métadonnées pour une charge de travail, ajoutez 169.254.169.254/32 à la liste blanche (une dérogation par attachement via le plan de contrôle est prévue dans une prochaine version).
La diffusion IPv4 (255.255.255.255) et le multicast (224.0.0.0/4) n'ont pas de dérogation et sont soumis à la politique, donc en mode liste blanche TC, le trafic comme les broadcasts de renouvellement DHCP est bloqué sauf autorisation explicite. Les vérifications de dérogation s'effectuent avant la liste noire, donc une plage dérogée ne peut être bloquée qu'en désactivant sa dérogation — et comme le lien-local IPv4 est maintenant désactivé par défaut, le mode liste noire peut aussi bloquer le service de métadonnées.
Quelques avantages majeurs de cette solution que les autres options ne proposent généralement pas :
secretdata.someattacker.com)À ma connaissance, aucune autre solution n'offre toutes ces fonctionnalités ensemble.
Limitation connue : les attachements cgroup filtrent au niveau socket (hooks connect/sendmsg), donc un processus avec CAP_NET_RAW peut fabriquer des paquets bruts qui les contournent. Utilisez un attachement TC (interface), qui filtre au niveau de la couche dispositif, pour les charges de travail pouvant détenir CAP_NET_RAW.
Cependant, cela a un peu plus de surcharge que quelque chose comme httpjail.
Ces mesures ont été effectuées dans la passerelle Docker privilégiée sur linux/arm64 en utilisant make bench-docker. Les valeurs sont les médianes de cinq échantillons.
Le benchmark de socket à chaud utilise des sockets UDP connectés pour isoler le coût du hook eBPF cgroup/connect4 de la latence de la poignée de main TCP. Dans ce chemin, le DNS a déjà résolu le domaine, l'IP est toujours dans le TTL, et l'IP/CIDR est déjà présente dans la map eBPF.
| Chemin | Latence médiane |
|---|---|
| Connexion socket normale, sans eBPF | ~2,647 us |
| Liste blanche à chaud, hit LPM protégé | ~2,691 us |
| Liste blanche à chaud, hit hôte exact DNS | ~2,741 us |
| Échec de liste blanche, blocage local | ~1,652 us |
L'écart mesuré entre les chemins de connexion normal, LPM protégé et hôte exact DNS se situe dans le bruit de l'échantillon.
Il n'existe pas aujourd'hui de chemin « échec noyau demande au processus parent ». Un échec de liste blanche cgroup est décidé localement par eBPF et est bloqué immédiatement.
Ces chiffres mesurent le chemin du serveur DNS, pas le chemin de connexion socket à chaud.
| Chemin | Latence médiane |
|---|---|
| Requête proxy à froid, fonction de politique dans le processus | ~31,336 us |
| Requête proxy à chaud | ~27,964 us |
| Requête liste blanche à froid avec amont local | ~53,510 us |
| Requête liste blanche à chaud avec amont local | ~53,432 us |
Les lignes « à froid » synchronisent via la barrière de mutation d'attachement réelle et effacent le graphe de propriété du benchmark et l'instantané de map exacte fictive entre les requêtes. Le minuteur tourne en continu pour préserver la localité du planificateur UDP, tandis que ns/op soustrait le fixture-reset-ns/op (incluant toute queue du gestionnaire précédent après que le client a reçu son paquet) et mesure ainsi l'Exchange du client actuel. raw-total-ns/op rapporte les deux ensemble. La réinitialisation préserve les domaines de politique configurés et le stockage de sauvegarde, et le benchmark affirme une addition physique de map exacte par requête. Les lignes « à chaud » amorcent la propriété une fois et affirment une addition physique sur l'ensemble de l'exécution.
Les microbenchmarks de propriété interne ci-dessous sont des diagnostics de passage à l'échelle, pas des lignes d'acceptation de chemin de requête DNS de bout en bout. L'helper en cache n'est conservé que pour les tests et benchmarks ; il encapsule un enregistrement à la fois et répète la validation du domaine. Lui et le trafic normal du résolveur traversent la barrière de mutation d'attachement, tandis que le trafic normal du résolveur admet chaque réponse complète comme une transaction.
+------------------+ +-------------------------+ | Your Control |<------->| Daemon (per host) | | Plane (gRPC) | stream | | +------------------+ | +-------------------+ | | | DNS Server | | | | (per-attachment) | | | +-------------------+ | +-------------------------+ | +------+------+ | | TC Filter Cgroup Filter (veth, eth) (containers)
Chaque attachement reçoit une adresse DNS unique (port) provisionnée par le démon. Les conteneurs/VM doivent être configurés pour utiliser leur adresse DNS assignée ; le filtrage du trafic DNS ordinaire des charges de travail ne le redirige pas de manière transparente.
### Topologie et comportement du résolveur DNS
`dns.listen_addr` doit identifier une adresse IPv4 ou IPv6 concrète que chaque
charge de travail attachée peut atteindre. Les adresses génériques sont rejetées car elles
ne peuvent pas être annoncées comme points de terminaison de résolveur. Un nom d'hôte configuré est résolu
une fois au démarrage du démon et l'adresse IP concrète résultante est utilisée pour la liaison,
l'annonce, la persistance et l'amorçage du filtre. La valeur par défaut `127.0.0.1` est
appropriée uniquement lorsque la charge de travail partage l'espace de noms réseau du démon ; un
conteneur ou une VM dans un autre espace de noms a normalement besoin d'une adresse hôte/pont accessible à la place.```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 renvoie l'adresse dns_address concrète ; configurez cette adresse exacte comme résolveur de la charge de travail. Netfence installe une entrée d'autorisation protégée et non expirante /32 ou /128 pour l'IP d'écoute afin que le mode liste blanche puisse démarrer sans règle DNS-IP du plan de contrôle. Les filtres actuels appliquent les préfixes IP, pas les ports de destination, donc cette entrée protégée autorise chaque port sur l'IP d'écoute (pas seulement son port DNS) ; ceci est particulièrement important pour les modèles de menace d'attachement cgroup. Utilisez une IP d'écoute dédiée lorsque cette atteignabilité plus large n'est pas acceptable.
Le point de terminaison assigné dessert à la fois UDP et TCP. Les réponses UDP sont tronquées à la limite de 512 octets d'un client legacy ou à sa taille EDNS annoncée et portent le flag TC lorsque nécessaire, permettant à la charge de travail de réessayer le même point de terminaison via TCP. Pour la résolution en amont, une réponse UDP tronquée est réessayée via TCP contre le même amont en premier. Un échec de transport, SERVFAIL, ou REFUSED passe alors au prochain amont configuré dans l'ordre.
DnsConfig.upstream_servers remplace le daemon-global dns.upstream pour un attachement. Les entrées utilisent la syntaxe host:port (IPv6 littéral entre crochets), sont canonisées et dédupliquées dans l'ordre de première apparition, et sont limitées à huit serveurs uniques. Une liste vide sélectionne le repli global.
Dans les modes de filtrage, Netfence supprime les paramètres ipv4hint et ipv6hint des réponses HTTPS/SVCB, y compris leurs références mandatory correspondantes, car les adresses suggérées n'ont pas passé indépendamment l'admission du filtre. Le mode désactivé préserve les réponses en amont inchangées.
Les compteurs de requêtes DNS sont mutuellement exclusifs : dns_queries_allowed compte les requêtes autorisées par la politique et ayant reçu une réponse réussie (y compris NXDOMAIN), dns_queries_blocked compte les réponses REFUSED de la politique, et dns_queries_errors compte les chemins d'erreur du résolveur, proxy, admission de filtre, écriture de réponse et autres. Une requête incrémente exactement un compartiment.
Chaque réponse contenant des adresses dans un mode DNS de filtrage est admise dans le niveau HASH IPv4/IPv6 exact de l'attachement en une seule transaction avant qu'une adresse A/AAAA de ses sections réponse, autorité ou additionnelle ne soit renvoyée. Si la réponse complète ne peut pas être représentée, le résolveur renvoie SERVFAIL sans adresse et préserve l'ensemble de travail précédemment admis. Les décisions PROXY qui renvoient des adresses doivent définir add_to_filter ; sinon, elles échouent également en mode fermé comme SERVFAIL. Le mode DNS désactivé est l'exception explicite de passage direct.
Les entrées exactes portent les arêtes TTL de la requête normalisée au propriétaire de politique correspondant. La suppression ou le refus d'un domaine supprime rapidement ses dernières adresses exactes DNS uniquement, tandis qu'une adresse partagée survit à un autre propriétaire de requête actif et un CIDR de plan de contrôle qui se chevauche continue indépendamment dans le niveau LPM protégé. Les réponses DNS DENYLIST avec autorisation par défaut et autorisation explicite sont également suivies, même si le DENYLIST de paquets ignore les autorisations exactes, de sorte qu'un passage ultérieur en mode ALLOWLIST peut utiliser les adresses mises en cache déjà renvoyées sans une nouvelle requête.
Tout état de possession normal/actif de l'espace utilisateur est limité par les cinq paramètres de possession ci-dessus. Les arêtes synthétiques provisoires restaurées sont exemptes de ces limites logiques afin de ne pas être oubliées avant la réconciliation, mais restent limitées par les cartes exactes physiques IPv4/IPv6. Les domaines de politique configurés et les domaines de requête actifs partagent max_tracked_domains, et chaque enregistrement TTL (requête, propriétaire correspondant, IP) consomme un emplacement max_ownership_edges.
En cas de pression, l'admission projette d'abord les arêtes TTL expirées. Elle récupère ensuite les arêtes logiques complètes requête/propriétaire avec le moins de dommages collatéraux physiques avant la récence, suivies par un LRU déterministe observé par le résolveur (l'IP canonique départage les égalités). Une éviction physique supprime toute la clé exacte DNS et tous ses propriétaires DNS. L'IP physique entrante et l'arête exacte entrante (IP, requête, propriétaire) sont protégées pour la transaction de réponse. La possession provisoire restaurée protège sa clé physique jusqu'à la réconciliation faisant autorité, mais les métadonnées DNS normales non liées partageant cette clé peuvent encore être récupérées. Les autorisations du plan de contrôle/système faisant autorité et chaque refus restent dans des niveaux LPM séparés et ne sont jamais candidats à la récupération DNS.
Le budget de recyclage glissant par attachement facture une unité pour une nouvelle clé exacte physique et une pour chaque clé DNS physique active évincée ; un remplacement complet de l'ancienne par la nouvelle coûte donc deux. Les rafraîchissements, la récupération logique uniquement, l'expiration et la suppression de politique coûtent zéro. Les événements restent actifs tant que leur âge est inférieur à dns.churn_window et expirent à la limite exacte. DnsConfig.max_churn_units peut seulement abaisser le plafond du daemon ; zéro en hérite. La fenêtre ne peut pas être modifiée par le plan de contrôle, et changer le plafond/fenêtre du daemon nécessite un redémarrage. Abaisser puis relever une limite d'attachement n'oublie pas l'historique toujours actif.
Un registre de travail glissant séparé limite la planification coûteuse du graphe de possession. Les rafraîchissements rapides et les admissions ordinaires n'y touchent jamais. Avant qu'un chemin de pression ne clone ou n'analyse le graphe, Netfence facture des unités de travail stables dérivées des clés physiques actuelles, des arêtes de possession, des domaines suivis et de la taille de réponse par rapport à leurs plafonds immuables de daemon/carte. Cette tentative de charge est conservée même si le plan s'avère impossible ou qu'une transaction ultérieure de carte exacte échoue, fermant le chemin de réessai sans mutation pour la pression CPU/allocation sans changer la comptabilité transactionnelle de recyclage physique ci-dessus. Aux plafonds par défaut, la tolérance admet huit passages de graphe équivalents maximum par fenêtre ; abaisser DnsConfig.max_churn_units en conserve au moins un. Abaisser puis relever la limite ne remet jamais à l'échelle ni n'oublie l'historique de travail actif.
Lorsqu'aucun état DNS éligible ne peut satisfaire une limite, ou que l'une ou l'autre des tolérances glissantes est épuisée, Netfence préserve l'ensemble de travail admis et renvoie SERVFAIL sans renvoyer l'adresse non admise. Les échecs de capacité incrémentent map_full_drops ; les limitations de recyclage physique et de planification de travail ne le font pas. Les battements de cœur exposent les valeurs actuelles de la carte exacte, la capacité et les valeurs maximales de génération de processus, ainsi que les évictions cumulées LRU DNS, tous les échecs d'admission et un compteur agrégé de limitation de budget couvrant les deux gardiens glissants. Les journaux de pression/récupération de capacité, budget physique et budget de travail sont limités en fréquence indépendamment. Les opérateurs peuvent attendre la récupération TTL/fenêtre, réduire le recyclage des réponses/domaines ou les tentatives répétées de pression de cap, ou augmenter DnsConfig.max_churn_units jusqu'au plafond du daemon dns.max_churn_units. Augmenter le plafond du daemon nécessite un redémarrage ; augmenter filter.max_dns_rule_entries nécessite également de recréer l'attachement car les cartes épinglées ne peuvent pas être redimensionnées sur place.
Les CIDR faisant autorité du plan de contrôle et les règles du système daemon utilisent quatre cartes LPM indépendantes et non évictables : autoriser/refuser × IPv4/IPv6. Chaque carte a des emplacements filter.max_rule_entries. L'amorçage /32 ou /128 de l'écouteur DNS est une autorisation système et compte dans la carte d'autorisation protégée correspondante. Les entrées d'hôte exact DNS restent dans leurs cartes séparées et ne peuvent pas consommer ces emplacements. Aucune règle protégée n'est jamais évincée LRU : les autorisations explicites, les règles système et chaque refus restent jusqu'à une suppression autorisée ou un remplacement complet.
Un SubscribedAck ou BulkUpdate complet est canonisé et son occupation finale est vérifiée pour les quatre cartes avant mutation. La capacité est basée sur l'état final, donc remplacer des clés dans une carte pleine est valide ; un état surdimensionné est rejeté sans évincer ni accepter partiellement des règles. Les survivants ne sont pas supprimés et réajoutés. Si un appel système de carte échoue ultérieurement, Netfence restaure et vérifie l'inventaire exact des quatre cartes avant l'appel. Le mode prouvé après restauration est l'ancien mode ou BLOCK_ALL (normalement BLOCK_ALL), donc le daemon maintient toujours l'attachement en échec fermé jusqu'à ce qu'une nouvelle tentative autoritaire complète réussisse.
L'état de sécurité de la politique protégée est persisté avec l'attachement et exporté dans les battements de cœur. BLOCK_ALL en soi est un mode configuré normal et sain : policy_degraded est faux lorsque policy_degraded_reason est vide. Une mutation protégée risquée qui commence alors que BLOCK_ALL est sain journalise d'abord protected_policy_mutation_in_progress. Il s'agit d'un journal de plantage transitoire, pas d'un diagnostic d'échec stable : l'opération en cours peut publier son mode prévu avant la sauvegarde finale d'effacement du journal, et une complétion réussie efface le journal lui-même. Si le démarrage le trouve après un plantage, le démarrage force et prouve d'abord BLOCK_ALL, puis persiste protected_policy_mutation_interrupted. Les codes de raison de dégradation stable sont :
protected_policy_mutation_interruptedauthoritative_protected_policy_failedincremental_deny_install_failedincremental_allow_removal_failedincremental_mode_change_failedCe sont des classifications stables, jamais du texte brut d'appel système/store. Le journal en cours est une limite de plantage interne persistée ; les statistiques de battement de cœur sont sérialisées avec la mutation propriétaire et observent donc soit son effacement réussi, soit une conversion d'échec stable, pas le journal intermédiaire en direct. Les raisons stables de dégradation/interruption maintiennent l'application des paquets dans BLOCK_ALL prouvé et rejettent les commandes CIDR incrémentales et de mode paquet. Les modifications de configuration DNS indépendantes et l'expiration TTL DNS peuvent continuer sous cette emprise prouvée, mais ne peuvent pas effacer la raison stable ni réactiver la politique de paquets. La récupération d'une raison stable nécessite un état LPM complet et DNS souhaité : appliquer BulkUpdate via le plan de contrôle ou l'API locale (ou répondre au Subscribed frais d'un attachement restauré avec SubscribedAck). Netfence met en scène l'état protégé complet, applique l'état DNS faisant autorité, active le mode demandé et efface la raison durable seulement après que chaque étape réussit. Préférez un command_id unique sur un BulkUpdate de récupération du plan de contrôle et exigez un CommandResult réussi ; l'API locale rejette command_id car son résultat RPC unaire rapporte déjà succès ou échec.
Les battements de cœur exposent les entrées physiques actuelles, la capacité dure et les valeurs maximales de génération de daemon indépendamment pour les quatre cartes protégées. L'amorçage est inclus ; les entrées épinglées adoptées initialisent la valeur maximale de la nouvelle génération. map_full_drops est cumulatif et inclut les rejets de capacité protégée. Si une lecture d'occupation échoue, le daemon conserve le dernier instantané prouvé et émet un avertissement au maximum une fois toutes les 30 secondes au lieu d'inventer de nouveaux comptes. Pour récupérer de la pression, réduisez les règles souhaitées complètes en dessous de chaque capacité par carte et réessayez la mise à jour complète. L'augmentation de filter.max_rule_entries n'est possible qu'au chargement et nécessite de recréer un attachement épinglé existant. Si le daemon ne peut pas prouver BLOCK_ALL ou enregistrer durablement son marqueur de sécurité, il arrête l'admission de mutation ; réparez la panne de carte/store et redémarrez plutôt que de supposer que l'application est rouverte.
Exécutez le daemon, qui :
DaemonService) pour les attachements, la politique et l'inspectionControlPlane.Connect)Démarrez le daemon :```bash
netfenced start
netfenced start --config /etc/netfence/config.yaml
**Vérifier le statut du démon :**```bash
netfenced status
Sans control_plane.url, un nouvel attachement se fait en mode packet/DNS désactivé et peut être configuré immédiatement via l'API locale ou la CLI. Aucun processus de plan de contrôle n'est requis pour le workflow autonome documenté sous « Par attachement ».
L'API gRPC locale n'a pas d'authentification par RPC. L'accès au système de fichiers à son socket Unix est la frontière d'autorisation, et tout processus capable de se connecter est un administrateur réseau hôte entièrement de confiance : il peut attacher ou détacher des programmes eBPF de l'hôte, remplacer la politique de paquets et DNS, et ouvrir ou fermer le trafic de charge de travail. Maintenez une appartenance étroite au groupe de socket et protégez le répertoire parent du socket.```yaml
socket: /run/netfence/netfence.sock
socket_group: netfence-admin
`NETFENCE_SOCKET` et `NETFENCE_SOCKET_GROUP` sont les variables d'environnement équivalentes. Au démarrage, le démon associe la socket dans un répertoire de préparation privé, définit son groupe et son mode `0660` tant qu'elle est inaccessible, puis la publie de manière atomique. Le démon supprime toute socket Unix préexistante à la cible configurée — il ne fait pas la distinction entre une socket obsolète et une socket appartenant à un autre démon actif — donc un seul démon doit posséder un chemin de socket. Il refuse de supprimer une cible qui n'est pas une socket. Sous Linux, le renommage sans remplacement empêche d'écraser un nouveau chemin créé après cette suppression ; l'arrêt ne supprime le chemin publié que s'il identifie toujours l'inode de la socket du démon. Un groupe invalide, un échec de propriété/mode, ou une cible non-socket entraîne un échec au démarrage sans publier un point de terminaison permissif.
### Sécurité du transport du plan de contrôle (TLS / mTLS / jeton porteur)
Le canal du plan de contrôle est la surface d'attaque la plus précieuse du système (quiconque le contrôle peut pousser des règles `ALLOW` vers chaque charge de travail), donc le démon **échoue en mode fermé** : si `control_plane.url` est défini, la configuration doit explicitement choisir un transport — soit un bloc `control_plane.tls` soit `control_plane.insecure: true`. Une URL sans l'un ni l'autre est rejetée au démarrage ; il n'y a pas de valeur par défaut implicite en texte clair. (Il s'agit d'un changement de comportement délibéré : les versions plus anciennes contactaient silencieusement le plan de contrôle sans chiffrement.)```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 avec uniquement les racines système (certificat serveur émis par une CA publique, pas de mTLS) n'est qu'un bloc vide :```yaml control_plane: url: cp.example.com:443 tls: {}
Le texte en clair pour le développement local est un opt-in explicite (mutuellement exclusif avec `tls`):```yaml
control_plane:
url: localhost:9000
insecure: true
Les certificats et les clés sont chargés une fois au démarrage, de sorte qu'un mauvais chemin/PEM fait échouer le démarrage avec une erreur claire au lieu d'apparaître à chaque reconnexion.
Le démon envoie des pings keepalive HTTP/2 sur la connexion du plan de contrôle afin qu'un chemin silencieusement mort (câble débranché, mappage NAT supprimé, route blackholée) soit détecté et fermé approximativement en keepalive_time + keepalive_timeout — au lieu de rester CONNECTED pendant des minutes jusqu'au délai de retransmission TCP du noyau tandis que chaque requête DNS proxyée consomme son délai d'attente complet. Les reconnexions sont espacées par un backoff exponentiel avec gigue (démarre à 1s, double, ±20% de gigue, plafonné à reconnect_backoff_max) ; le backoff se réinitialise au minimum uniquement après qu'une connexion soit restée saine pendant 30s, de sorte qu'un plan de contrôle qui accepte les connexions et les abandonne immédiatement continue à reculer au lieu d'être martelé au minimum.```yaml
control_plane:
keepalive_time: 30s
keepalive_timeout: 10s
reconnect_backoff_max: 30s
Les valeurs nulles/non définies signifient les valeurs par défaut — elles ne **désactivent** pas le keepalive ou le backoff. Votre plan de contrôle doit autoriser cette cadence de ping dans sa politique d'application du keepalive gRPC (voir ci-dessous), sinon il rejettera le démon avec
`ENHANCE_YOUR_CALM (too_many_pings)`.
### Redémarrages, plantages et mises à niveau du démon (état BPF épinglé)
Le démon épingle les liens BPF et les cartes de règles de chaque attachement sur bpffs
(`filter.bpf_pin_dir`, par défaut `/sys/fs/bpf/netfence`, un répertoire par
ID d'attachement). Étant donné que l'état épinglé est détenu par le noyau — pas le processus du démon
processus — **l'application continue lorsque le démon est hors service** : un plantage
(`kill -9`), un arrêt gracieux, ou une mise à niveau laisse la dernière politique connue
(mode + toutes les règles) en application, et le prochain démarrage du démon réadopte l'état épinglé
tel quel. La restauration ne rattache ni ne réécrit jamais les cartes actives, donc il n'y a
aucune fenêtre où une charge de travail autorisée est bloquée ou une destination bloquée
est autorisée, et aucun attachement en double.
Le comportement d'arrêt est une configuration explicite (`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 explicite (RPC/CLI), ou la suppression d'une cible vivante possédée de manière cohérente, détruit l'état épinglé avec l'attachement. Au redémarrage, l'absence de cible n'autorise pas de devinette : les épingles persistées futures, non validées, mixtes ou autrement invérifiables sont préservées et le démarrage avorte pour inspection.
Les répertoires d'épingles sont un format de persistance versionné. Le marqueur de schéma est épinglé en dernier, uniquement après que chaque carte et lien requis existe. La mise à niveau d'un attachement pré-exact-tier épingle les deux nouvelles cartes exactes vides avec un marqueur en cours, remplace atomiquement le programme de chaque lien tout en réutilisant les cartes autoritatives actives, vérifie l'identité programme/carte, et valide le marqueur en dernier. Un crash ou une mise à jour ambiguë laisse le marqueur non validé ; le prochain démarrage remet à jour chaque lien en utilisant les mêmes cartes. Les anciennes et nouvelles générations de programmes appliquent la même politique LPM autoritative pendant cet état mixte borné, donc la migration ne désépingle ni ne recrée jamais un filtre viable. Les ensembles d'épingles inconnus, incomplets ou invérifiables sont préservés et font avorter le démarrage pour inspection au lieu d'être devinés.
Notes sur l'état ré-adopté :
SyncRequest, puis une déclaration Subscribed complète pour chaque attachement restauré nécessitant encore une réconciliation. Répondre avec un SubscribedAck frais : son mode, CIDRs, TTLs et configuration DNS sont l'état désiré complet. Le démon applique un delta (les CIDRs inchangés ne sont jamais supprimés), et efface le marqueur de restauration uniquement après que l'ack complet s'applique. Un délai d'attente, une déconnexion ou un échec de validation laisse l'application inchangée. Un échec d'application filtre/carte/DNS/stockage peut laisser un delta partiel, mais la réconciliation n'utilise pas un effacement de carte en bloc ou une suppression/ajout des survivants inchangés ; le marqueur de restauration reste défini et le démon réessaie après une connexion ultérieure.SubscribedAck arrive ; cet ack autoritatif remplace exactement leurs durées de vie, y compris le raccourcissement d'une échéance ou la transformation d'une règle provisoirement permanente en une règle à TTL fini. Les clés DNS exactes restaurées sont inventoriées et représentées par des propriétaires provisoires bornés (les capacités réelles des cartes épinglées sont la borne) ; la première configuration DNS autoritative rejette toute réclamation synthétique, supprime les clés laissées sans propriétaire, et préserve une clé uniquement lorsqu'elle a séparément un propriétaire normal actif. Un inventaire non canonique/en collision avorte la restauration sans deviner ni publier partiellement les métadonnées de propriété.Les règles de domaine DNS, les surcharges en amont par attachement et les limites DNS d'attachement sont un état désiré d'exécution fourni via l'API locale ou le plan de contrôle ; elles ne sont pas persistées. Un attachement restauré dont le dernier mode DNS était ALLOWLIST, DENYLIST ou PROXY démarre son résolveur dans une posture ALLOWLIST vide, retournant REFUSED jusqu'à ce qu'un BulkUpdate ou SubscribedAck complet s'applique. Un mode DNS explicitement DISABLED reste en forwarding. Si l'un ou l'autre des écouteurs UDP/TCP engagés meurt de manière inattendue plus tard, l'attachement est mis en quarantaine en IP BLOCK_ALL et signalé comme une désinscription d'erreur.
Votre système d'orchestration appelle l'API locale du démon.
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
Direction TC : le champ tc_direction (CLI --direction) sélectionne sur quel hook TCX le filtre se fixe, et choisir le bon dépend du côté du lien sur lequel se trouve l'interface :
La direction s'applique uniquement aux fixations d'interface (TC) ; elle est ignorée pour les fixations cgroup.
control_plane.url est configuré, le daemon envoie
Subscribed{id, target, type, metadata} et attend SubscribedAck avec
la configuration initiale (mode, CIDRs, règles DNS). Si le plan de contrôle ne répond pas
dans le délai imparti (5s par défaut, configurable via
control_plane.subscribe_ack_timeout), la fixation est annulée et l'appel
d'attache échoue. La validation et autres échecs avant validation suivent la même
règle de rollback ordinaire.BLOCK_ALL durable au lieu de l'annuler de manière destructive. Avec un délai d'attente limité, Attach renvoie une erreur contenant l'ID de fixation conservé ; l'appelant peut découvrir cet ID en faisant correspondre la cible dans List, et le plan de contrôle doit le récupérer avec un BulkUpdate complet.subscribe_ack_timeout: 0, un nouvel Attach retourne après avoir mis en file d'attente
; un accusé de réception ultérieur est toujours validé et appliqué. Cette valeur nulle
ne désactive pas la réconciliation de fixation restaurée : les tentatives de restauration attendent
jusqu'à 5s en arrière-plan et réessayent lors d'une connexion ultérieure si nécessaire. Si
cet accusé ultérieur rencontre une pression protégée, la fixation déjà retournée est
conservée en ; n'émet ni ni
erreur , et le daemon ne relance pas automatiquement la
déclaration avant redémarrage. Détectez plus sa raison,
occupancy/capacity, et dans , puis envoyez un
complet avec pour obtenir un résultat de récupération explicite.RPC :``` DaemonService.Detach(id)
**CLI:**```bash
netfenced detach --id <attachment-id>
Liste des pièces jointes :```bash netfenced list netfenced list --all # fetch all pages
### Politique locale et inspection
Chaque mutation locale est un encodage CLI léger de l'unique
`DaemonService.ApplyCommand(ControlCommand)` RPC. Fournissez l'ID de pièce jointe retourné par `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>
Les modes de paquets sont disabled, allowlist, denylist et block-all ; les modes DNS sont disabled, allowlist, denylist et proxy. Le mode DNS proxy nécessite un plan de contrôle configuré et accessible. La correspondance de domaine utilise le suffixe de correspondance le plus spécifique ; lorsque des règles d'autorisation et de refus de spécificité égale correspondent toutes les deux, le refus l'emporte. Les CIDR et domaines sont canonicalisés. Les TTLs, enums, CIDR, domaines, sélecteurs et messages imbriqués négatifs, malformés ou autrement invalides sont rejetés avant la mutation, donc une commande invalide est une opération nulle de politique.
Pour un remplacement complet, apply-rules lit la forme JSON protobuf BulkUpdate existante à partir d'un fichier ou de 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` n'accepte que `SetMode`, `AllowCIDR`, `DenyCIDR`, `RemoveCIDR`, `BulkUpdate`, `SetDnsMode`, `AllowDomain`, `DenyDomain` et `RemoveDomain`. Les variantes de synchronisation/acquittement exclusives au flux, les commandes inconnues ou vides, ainsi que les valeurs `command_id` locales sont rejetées. `BulkUpdate` est également la seule opération locale capable de restaurer une politique de paquets stable dégradée ; elle doit contenir l'état souhaité complet du LPM et du DNS.
Le sélecteur optionnel `ControlCommand.remove_cidr_list` peut cibler la liste d'autorisation, la liste de refus, ou les deux lorsque la variante de commande est `RemoveCIDR`. Sa valeur héritée non spécifiée ainsi que `BOTH` explicite suppriment des deux listes, préservant le comportement original du protocole.
Les mutations locales et du plan de contrôle partagent un même analyseur, une barrière de mutation, un registre TTL, un chemin de récupération en mode défaillance-fermée et un propriétaire de politique. Il n'y a délibérément aucun arbitrage de propriété entre local et plan de contrôle : les opérations conflictuelles sur une liste de politique individuelle prennent effet dans leur ordre d'engagement, quelle que soit la source. En particulier, un `BulkUpdate` ou `SubscribedAck` complet ultérieur du plan de contrôle peut remplacer l'état local.
`GetRules`/`netfenced rules` renvoie un instantané cohérent et déterministe du registre espace utilisateur. Chaque CIDR rapporte la liste d'autorisation/refus, `policyOwned` local ou plan de contrôle, `systemOwned` du démon, `expiresAt` absolu, `provisional` restauré, et le dernier état `installed` engagé dans le noyau. Une entrée installée sans aucun propriétaire est une tentative de suppression ayant échoué, et non une politique souhaitée. La sortie DNS est la configuration `DnsConfig` effective, normalisée et vivante. L'inspection n'énumère intentionnellement pas les cartes protégées du noyau ni n'expose les entrées de cache d'hôte exact résolues dynamiquement ; utilisez la télémétrie heartbeat pour l'occupation des cartes protégées.
## Sur le plan de contrôle (vous implémentez ceci)
Implémentez l'appel RPC `ControlPlane.Connect` - un flux bidirectionnel :
Configurez la politique d'application de keepalive de votre serveur gRPC pour autoriser la cadence de ping du démon (`control_plane.keepalive_time`, 30s par défaut) : définissez `MinTime` à cet intervalle ou en dessous et `PermitWithoutStream : true`. La politique par défaut de gRPC (5 minutes) traite les pings du démon comme abusifs et ferme la connexion avec `ENHANCE_YOUR_CALM (too_many_pings)`. En Go :```go
grpc.NewServer(grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
MinTime: 10 * time.Second,
PermitWithoutStream: true,
}))
Réception du démon :
SyncRequest lors de la connexion/reconnexion (liste les pièces jointes actuelles)Subscribed lorsque de nouvelles pièces jointes sont ajoutées, et après SyncRequest pour les pièces jointes restaurées qui nécessitent encore un état faisant autorité à jourUnsubscribed lorsque des pièces jointes sont suppriméesHeartbeat avec des statistiquesCommandResult{command_id, id, success, error} — résultat de toute commande que vous avez envoyée avec un command_id non vide (nonce de corrélation optionnel sur ControlCommand ; les commandes sans command_id ne produisent aucun résultat). success est vrai uniquement si la commande s'est entièrement appliquée — une BulkUpdate partiellement appliquée signale un échec avec l'erreur agrégée. Les résultats sont fournis au mieux : traitez un résultat manquant comme inconnu, pas comme un échec.Envoi au démon :
SyncAck après réception de SyncRequestSubscribedAck{mode, cidrs, dns_config} après réception de Subscribed (obligatoire — le démon attend ceci)SetMode{mode} — modifier le mode de la politique de filtrage IPAllowCIDR{cidr, ttl} / DenyCIDR / RemoveCIDR (optionnellement sélectionner
autoriser, refuser, ou les deux ; si non spécifié, conserve le comportement « les deux » hérité)SetDnsMode{mode} — modifier le mode de filtrage DNSAllowDomain{domain} / DenyDomain / RemoveDomain (la correspondance la plus spécifique
gagne ; le refus l'emporte en cas d'égalité de spécificité)BulkUpdate{mode, cidrs, dns_config} — synchronisation complète de l'étatLorsque le plan de contrôle reçoit Subscribed, il doit répondre avec un SubscribedAck complet. Pour une nouvelle pièce jointe, le démon attend normalement cet accusé de réception avant de renvoyer un succès à l'appelant local. Pour une pièce jointe restaurée, la négociation s'effectue en arrière-plan tandis que la dernière politique connue et épinglée continue de s'appliquer. Utilisez les métadonnées pour identifier la machine virtuelle/locataire/conteneur et renvoyez le mode complet souhaité, les CIDR (y compris les TTL) et l'état DNS ; une configuration DNS omise signifie désactivé avec des listes de domaines vides.
SyncRequest est le point de réconciliation faisant autorité : à chaque reconnexion, il s'agit du premier événement sur le flux et il liste l'ensemble complet des pièces jointes actuelles du démon. Réconciliez votre vue avec cela — ajoutez les pièces jointes que vous ne connaissiez pas, supprimez celles absentes de la liste. Lors de la reconnexion, le démon purge les événements qui étaient en file d'attente sur la connexion précédente (la synchronisation les remplace), vous ne verrez donc pas de Heartbeat obsolètes, de Unsubscribed pour des pièces jointes déjà absentes de la synchronisation, ni de CommandResult de la connexion morte rejoués après celle-ci. Deux cas limites subsistent par conception, et votre plan de contrôle DOIT les traiter de manière idempotente :
Subscribed peut suivre un SyncRequest qui liste déjà le même identifiant. Cela se produit lorsque l'accusé de réception d'une nouvelle pièce jointe était en attente lors de la reconnexion, et délibérément pour chaque pièce jointe restaurée jusqu'à ce qu'un SubscribedAck faisant autorité s'applique complètement. Traitez-le comme une mise à jour, répondez avec un SubscribedAck complet et frais, et ne le rejetez jamais comme un doublon. SyncRequest réconcilie l'inventaire des pièces jointes ; SubscribedAck réconcilie la politique souhaitée.Unsubscribed pour un identifiant de pièce jointe inconnu ou déjà supprimé comme une opération sans effet.AllowCIDR/DenyCIDR et les listes CIDR dans SubscribedAck/BulkUpdate) comportent un TTL optionnel. Les règles avec TTL sont supprimées par un nettoyeur du démon une fois qu'elles expirent (intervalle de balayage ttl_janitor_interval, par défaut 1s) ; les règles sans TTL sont permanentes.AllowCIDR/DenyCIDR incrémentaux ré-ajoutés prolongent un CIDR jusqu'à la date limite ultérieure — ils ne la raccourcissent jamais — et un ré-ajout incrémental sans TTL le rend permanent. En revanche, l'état complet dans SubscribedAck/BulkUpdate remplace exactement chaque durée de vie du plan de contrôle, donc une réconciliation faisant autorité peut raccourcir un TTL ou changer une règle permanente en durée limitée sans supprimer/réajouter l'entrée de la carte en direct. Utilisez RemoveCIDR pour supprimer une règle incrémentale plus tôt.dns.min_filter_ttl (par défaut 60s ; zéro/non défini signifie la valeur par défaut, pas « pas de plafond »). Un TTL amont de zéro vit donc pour la durée du plancher ; un TTL PROXY omis est explicitement par défaut à 300s avant d'appliquer le plancher. Une règle CIDR permanente ou plus longue couvrant la même adresse reste installée indépendamment dans le niveau de correspondance de préfixe le plus long (LPM) protégé lorsque la propriété DNS exacte expire.| Diagnostic interne de passage à l'échelle | Médiane actuelle | Mémoire / allocations |
|---|
| Admission de nouvelle clé à froid, graphe de propriété vide | ~370,3 ns | 232 B, 5 allocs/op |
| Admission de nouvelle clé à froid, 4.095 entrées non liées | ~451,2 ns | 232 B, 5 allocs/op |
| Pression de capacité physique et remplacement LRU | ~3,820 ms | ~4,23 MB (4.226.243 B), 4.336 allocs/op |
| Pré-vérification de budget physique épuisé | ~611,9 ns | 344 B, 9 allocs/op |
| Pression de bord maximale, réponse à 64 adresses | ~6,849 ms | ~7,66 MB (7.658.774 B), 2.233 allocs/op |
| Garde de travail de graphe maximal, plan complet autorisé | ~2,763 ms | ~4,26 MB (4.264.386 B), 3.074 allocs/op |
| Garde de travail de graphe maximal, rejet de pré-projection épuisée | ~10,935 us | 8,76 KB (8.760 B), 14 allocs/op |
| Opération de budget de rotation près du plafond numérique | ~18,98 ns | 0 B, 0 allocs/op |
| Instantané cohérent des statistiques de propriété | ~2,094 ns | 0 B, 0 allocs/op |
| Scan d'expiration sans opération sur 4.095 entrées | ~74,849 us/scan | 0 B, 0 allocs/op |
netfenced apply-rules| Interface | Direction correcte | Pourquoi |
|---|
Lien montant (ex. eth0), ou toute interface à l'intérieur du propre netns de la charge de travail | egress (par défaut) | Les paquets sortants de la charge de travail sont transmis à travers elle ; leur adresse de destination est la véritable destination. |
Paire veth côté hôte ou tap VM (ex. fcr-*) | ingress | Les paquets sortants de la charge de travail arrivent à l'hôte sur cette interface. L'egress y verrait plutôt le trafic de retour hôte→charge de travail et filtrerait par l'adresse propre de la charge de travail plutôt que par la véritable destination. |
SubscribedBLOCK_ALLSubscribedAckCommandResultUnsubscribedpolicy_degradedmap_full_dropsHeartbeatBulkUpdatecommand_idUnsubscribed automatiquementfilter.max_rule_entries par pièce jointe (par défaut 4096 chacune) ; voir « Capacité CIDR protégée et récupération en mode échec-fermé » ci-dessus. Les adresses hôtes dérivées du DNS utilisent des cartes HASH de correspondance exacte distinctes, dimensionnées par filter.max_dns_rule_entries (par défaut 4096 par famille IP), donc elles ne peuvent pas consommer ni expulser la capacité d'autorisation ou de refus. L'admission complète de la réponse DNS valide/canonise chaque adresse et prévérifie la capacité physique et logique avant mutation. Une erreur du noyau de la carte exacte DNS restaure son instantané exact avant l'appel ; si ce rollback ne peut être prouvé, le résolveur supprime la réponse et met en quarantaine la pièce jointe dans un BLOCK_ALL IP durable avant d'accepter une autre mutation.