Aller au contenu principal
Version: 1.0.0

bitmapper

bitmapper répond à la question « à quoi cette machine parle-t-elle réellement, et quel processus le fait ? »

bitcollector lit l'état de l'hôte. bitscanner regarde vers l'extérieur, vers le segment. bitmapper observe le trafic lui-même — les paquets en direct sur les interfaces de l'hôte, regroupés en flux et attribués au processus propriétaire de la socket.

À lire avant de bâtir quoi que ce soit autour de bitmapper

bitmapper livre des enregistrements de flux, pas du trafic. Depuis 0.1.0-ga, le moteur de capture est relié à l'exportateur, et les connexions suivies atteignent la plateforme sous forme d'enregistrements de flux : extrémités, ports, protocole, compteurs d'octets et de paquets, état de la connexion et — lorsqu'il peut être déterminé, ce qui est rare aujourd'hui — le processus attribué. Aucun octet de charge utile, aucune capture de paquets, et pas la ligne de commande du processus, qui transporte couramment des identifiants en arguments. Si vous attendiez qu'un PCAP arrive quelque part, ce n'est pas ce que cet agent envoie.

L'attribution de processus fonctionne à peine dans le binaire publié, et le correctif n'est pas encore publié. Dans 0.1.0-ga, l'attribution est tentée une seule fois, à la création de la connexion, contre un instantané des tables de sockets qui ne peut pas contenir la socket d'une nouvelle connexion sortante, et il n'existe aucune inférence à partir des sockets en écoute — les flux arrivent donc souvent sans aucun processus (mesuré sur l'hôte de développement : 0 flux attribué sur 12), et destination_process n'est jamais renseigné. Le remplacement — rattrapage par flux, sur les deux extrémités, et un champ attribution distinguant une correspondance directe de socket (socket) d'une inférence (listening_socket) — n'est pas publié. Cette inférence est encadrée : elle n'est tirée que pour une adresse de cet hôte, que lorsqu'un seul processus exactement est prouvé détenteur de la socket en écoute, et qu'en TCP — un flux UDP ne la porte donc jamais. Lisez l'attribution de processus avant de bâtir quoi que ce soit dessus.

C'est un binaire, sur une seule plateforme. 0.1.0-ga est publié pour linux/amd64 uniquement. Il n'existe aucun téléchargement macOS, Windows ou arm64, et les raisons sont exposées ci-dessous plutôt que laissées comme un vide sur une page de téléchargement.

Il ne capture pas tous les protocoles par défaut. capture.protocols vaut par défaut ["tcp", "udp"], que votre fichier de configuration la mentionne ou non : un déploiement d'origine ne voit donc ni ICMP, ni ICMPv6, ni SCTP, ni GRE, ni ESP, ni AH — le noyau les abandonne et rien dans la sortie ne le signale. L'élargir tient en une ligne.

Des parties de cet agent ne sont toujours pas construites. Il n'y a pas d'agrégation de topologie de services, pas de métriques Prometheus et pas de détection de politique réseau ; plusieurs clés de configuration sont analysées sans rien faire, et il en va de même de toutes les options de la ligne de commande sauf --config. Ce qui n'est pas câblé en donne la liste, et elle mérite d'être lue avant de bâtir quoi que ce soit autour d'une clé.

Version0.1.0-ga, commit 64ebc64, compilée avec go1.25.12
Plateformeslinux/amd64 uniquement — macOS, Windows et arm64 ne sont pas publiés, et pourquoi
Chaîne d'approvisionnementCompilation reproductible, SBOM CycloneDX, signature cosign, attestation du SBOM, SHA256SUMS signé — même clé de famille que les autres bits
ÉtatCapture, attribue et suit les connexions ; exporte des enregistrements de flux vers la plateforme
Surface réseau entranteAucun service en écoute. Les serveurs HTTP/WebSocket et gRPC existent dans l'arborescence du code mais ne sont jamais construits, et tous deux sont livrés désactivés
Trafic sortanthttps uniquement — enregistrement, battement de cœur et enregistrements de flux vers l'endpoint que vous configurez
PrivilègesÉlevés : root, ou CAP_NET_RAW + CAP_NET_ADMIN
Prérequis de compilationCGO et libpcap, liée statiquement dans le binaire publié

Sa place parmi les bits​

Les quatre agents répondent à quatre questions différentes, et la frontière est ce que chacun regarde :

RegardeRépond à
bitcollectorL'état propre de l'hôte« Qu'est-ce que cette machine, et dans quel état est-elle ? »
bitscannerLe segment autour de l'hôte« Qu'y a-t-il d'autre sur ce réseau dont je n'ai pas connaissance ? »
bitmapperLes paquets qui traversent les interfaces de l'hôte — TCP et UDP par défaut, davantage si vous le demandez« À quoi cette machine parle-t-elle, et quel processus le fait ? »
bitenforcerLa configuration de l'hôte« Cette machine est-elle conforme à la politique que j'ai définie ? »

Une table de voisins vous dit qu'un équipement existe. Une capture vous dit qu'on lui parle, par quoi, et à quelle fréquence — c'est là toute la différence entre un inventaire et une carte des dépendances.

Ce qui fonctionne aujourd'hui​

Présent et câblé dans le chemin d'exécution de la commande capture :

  • Capture de paquets en direct — libpcap via gopacket, liée statiquement dans le binaire Linux publié, avec filtre BPF, snaplen, mode promiscuité et taille du tampon de capture.
  • Filtrage de capture côté noyau — capture.filter, capture.protocols, capture.ports et capture.exclude_ports sont compilées en une seule expression BPF appliquée par le noyau : le trafic exclu n'est jamais copié dans l'agent. L'expression compilée est journalisée au démarrage. Attention au défaut : capture.protocols vaut ["tcp", "udp"] même si votre fichier l'omet, ce qui signifie qu'ICMP, SCTP, GRE et ESP ne sont pas capturés sauf demande explicite. Voir le filtrage.
  • Capture multi-interfaces — une liste nommée d'interfaces, ou bien toute interface active et non-loopback si vous n'en nommez aucune.
  • Attribution de processus — présente, mais fonctionnant à peine dans le binaire publié. PID, nom du processus, chemin de l'exécutable et utilisateur, résolus depuis les tables de sockets de l'hôte. Dans 0.1.0-ga, la recherche est tentée une seule fois, à la création de la connexion, contre un instantané qui ne peut pas contenir la socket d'une nouvelle connexion sortante : les flux ne portent donc souvent aucun processus, et destination_process n'est jamais renseigné. Le rattrapage par flux, l'attribution des deux extrémités et le champ de confiance attribution (socket vs listening_socket) ne sont pas dans le binaire publié, qui ne comporte aucune inférence à partir des sockets en écoute — voir l'attribution de processus pour ce qui est livré aujourd'hui et ce qui le remplace. Ce remplacement refuse aussi plutôt que de deviner : aucune identité n'est prise sur une socket en écoute si l'adresse n'est pas sur cet hôte ou s'il n'est pas prouvé qu'un seul processus la détient — une socket nginx de l'hôte de développement est détenue par 17 processus — et il n'infère jamais en UDP, qui n'a pas d'état LISTEN. Un champ processus vide est un refus, et non une panne.
  • Suivi de connexions à états — appariement bidirectionnel des flux, une machine à états TCP (SYN_SENT → ESTABLISHED → … → CLOSED), des compteurs de paquets et d'octets par connexion, une table plafonnée en taille avec éviction, et un ramasse-miettes périodique des connexions inactives.
  • Flux d'événements interne au processus — les événements de paquet, de nouvelle connexion et de statistiques sont diffusés aux abonnés à l'intérieur du processus.
  • Statistiques périodiques — compteurs de paquets, d'octets, d'abandons et de connexions écrits en JSON structuré selon performance.stats_interval.
  • Intégration au plan de contrôle — enregistrement de l'agent et battement de cœur lorsque control_plane.* est configuré.
  • Export de flux — chaque connexion suivie passe du chemin de capture à l'exportateur et est envoyée en POST à la plateforme sous forme d'enregistrement de flux. Chaque flux produit au moins un enregistrement, et le dernier est marqué comme final avec des compteurs complets ; une connexion de longue durée est rafraîchie une fois par batch_export.batch_interval si ses compteurs ont bougé, de sorte que vous la voyez avant qu'elle ne se termine.
  • Un filtrage d'export qui filtre réellement — output.filter (exclude_cidrs, exclude_loopback, protocols, ports) est appliqué à l'entrée, avant qu'un enregistrement ne soit construit : un flux exclu n'est donc jamais conservé en mémoire et ne peut jamais être envoyé. Les exclusions portent sur l'une ou l'autre extrémité : exclure 10.0.0.0/8 signifie « ne parlez pas de ce réseau à Cert-IX », et rapporter que 10.1.2.3 a ouvert une connexion vers l'extérieur en parle tout aussi complètement. Une entrée qui ne peut pas être compilée refuse l'exécution plutôt que de se dégrader en « tout exporter ».
  • Validation de configuration à échec immédiat — l'agent refuse de démarrer sur une configuration invalide ou rendue non sûre par omission, plutôt que de démarrer dans un état affaibli.

Ce qui n'est pas câblé​

Énoncé explicitement, pour que personne n'ait à le découvrir en lisant le code source :

  • Les serveurs HTTP/WebSocket et gRPC ne sont jamais construits. Comme rien ne les construit, le middleware de sécurité placé derrière eux — authentification, liste d'autorisation d'adresses IP, limitation de débit, journalisation d'audit — ne s'exécute pas. Ne considérez pas la configuration security.* comme une protection aujourd'hui. Le bon côté est celui qu'il faut dire honnêtement : aucun serveur n'étant construit, l'agent n'expose aucun port entrant.
  • Aucune agrégation de topologie de services. Les types ServiceTopology et ServiceEdge existent mais ne sont jamais construits : il n'y a donc pas encore de graphe de topologie.
  • Aucune métrique Prometheus et aucun endpoint /metrics.
  • Aucune détection de politique réseau.

Clés de configuration qui sont validées mais ne font rien​

Elles s'analysent correctement, et l'agent les accepte, mais aucun code ne les consomme :

output.backend, output.kafka, output.nats, output.redis, output.webhook, output.file, kubernetes.*, storage.*, logging.*, performance.batch_size, performance.connection_pooling, performance.memory_limit, performance.cpu_limit.

Les options de la ligne de commande relèvent de la même catégorie, et elles sont plus nombreuses que les clés. --log-level et --log-format sont déclarées, documentées dans --help, et lues par rien du tout : le journaliseur est un journaliseur zap de production figé (JSON, niveau info). Les options de capture (--interfaces, --filter, --snaplen, --promiscuous, --buffer-size, --workers, --queue-size, --stats-interval) sont liées sous leur nom d'option alors que la configuration est lue depuis des clés imbriquées : elles sont donc analysées puis ignorées. --enable-grpc et --enable-http valent true par défaut dans --help et désignent des serveurs qui ne sont jamais construits. --config est la seule option qui change le comportement. Ne prenez pas bitmapper --help pour une description de ce que fait cette version.

Trois choses ne figurent pas dans cette liste, et les distinctions comptent :

  • output.filter est lue et appliquée sur le chemin d'export depuis 0.1.0-ga. Elle figurait auparavant dans cette liste, et tant que l'agent n'exportait rien du tout ce n'était que du poids mort. À partir du moment où des enregistrements de flux ont commencé à quitter l'hôte, elle serait devenue une fausse affirmation de confidentialité — un opérateur excluant un sous-réseau sensible n'obtenant aucune protection, aucune erreur, et aucun moyen de le savoir. Elle est désormais appliquée à l'entrée. Quatre clés qui ne pouvaient agir sur rien (export_packets, export_stats, min_packet_size, require_payload) ont été supprimées du schéma plutôt que laissées à s'analyser, et l'agent les nomme au démarrage si votre fichier en contient encore une.
  • capture.protocols, capture.ports et capture.exclude_ports sont compilées dans le filtre BPF du noyau et appliquées à cet endroit. Une version antérieure de cette page listait les trois comme validées-mais-inertes, et c'était faux — voir la correction ci-dessous.
  • capture.exclude_interfaces est lue et appliquée sur le chemin de capture actif. Voir choisir les interfaces.
Correction : capture.exclude_ports est un contrôle de confidentialité qui fonctionne

Jusqu'à 0.1.0-ga, cette page comptait capture.protocols, capture.ports et capture.exclude_ports parmi les clés qui « s'analysent sans rien faire », et la page sur la capture le répétait. C'était l'inverse de la vérité : les trois sont compilées dans le filtre BPF et appliquées par le noyau avant qu'un paquet n'atteigne l'agent.

capture.exclude_ports est celle sur laquelle agir. La configuration livrée avec bitmapper fixe exclude_ports: [22] — « ne pas capturer SSH » — et cela fonctionne. Si vous avez lu l'ancien texte et supprimé cette clé comme du poids mort, vous avez retiré un contrôle qui vous protégeait et commencé à collecter du trafic que vous aviez délibérément exclu, sans rien qui signale le changement. Vérifiez ce que dit votre configuration déployée avant de supposer que vous n'êtes pas concerné.

capture.protocols demande l'attention inverse : elle vaut par défaut ["tcp", "udp"] que vous la fixiez ou non, si bien qu'un déploiement d'origine ne capture silencieusement ni ICMP, ni ICMPv6, ni SCTP, ni GRE, ni ESP, ni AH. Le défaut de protocole explique comment l'élargir.

Pourquoi elles sont malgré tout acceptées

Elles font partie du schéma de configuration et leur forme est validée : un fichier écrit avec elles se charge donc. C'est exactement pourquoi elles sont listées ici : une clé qui s'analyse sans erreur et ne fait rien est le genre de chose qui se lit comme une fonctionnalité opérationnelle. Considérez qu'une clé n'a aucun effet à moins qu'elle n'apparaisse sous ce qui fonctionne aujourd'hui.

Le package d'enrichissement Kubernetes a été supprimé plutôt que laissé en sommeil. Il n'avait aucun importateur et construisait son propre client HTTP, lequel suivait les redirections et pouvait faire fuiter un jeton de porteur de compte de service vers une destination en clair lorsqu'un kubeconfig pointait vers un serveur qui redirige. Le bloc kubernetes.* demeure dans le schéma et n'est désormais consommé par rien.

L'exécuter​

bitmapper est un binaire unique doté de trois commandes :

# What you have
bitmapper version
# Bitmapper 0.1.0-ga
# Git Commit: 64ebc64
# Build Date: 2026-08-10T05:56:16Z

# Which interfaces this host can capture on
bitmapper interfaces

# Capture, with the configuration you supply
bitmapper capture --config /etc/bitmapper/config.yaml

--config, --log-level et --log-format sont des options persistantes de la commande racine, mais seule --config fait quelque chose. Il n'existe aucun moyen d'augmenter le détail de journalisation pendant la mise en service : le journaliseur est figé en JSON, niveau info. Ce que l'agent vous dit au démarrage, c'est le filtre de capture compilé et les interfaces qu'il a retenues ; puis, à chaque performance.stats_interval, il journalise les compteurs de paquets, d'octets, d'abandons et de connexions — ce sont les preuves à lire pendant la mise en service d'un hôte.

Il lui faut des privilèges élevés​

La capture de paquets est une opération privilégiée. Sur la plateforme publiée :

PlateformeCe qu'il lui faut
LinuxCAP_NET_RAW et CAP_NET_ADMIN (ou root)

Il lui faut CGO — et le binaire publié l'a déjà​

La capture passe par gopacket/pcap, qui est une liaison vers une bibliothèque C. Une compilation avec CGO_ENABLED=0 se compile quand même, mais la capture et le listage des interfaces refusent de s'exécuter :

packet capture requires CGO (build with CGO_ENABLED=1 and libpcap installed)

C'est délibéré — un binaire qui ne capturerait silencieusement rien serait pire qu'un binaire qui dit pourquoi il ne le peut pas. C'est aussi le seul fait qui décide des plateformes publiées, et c'est pourquoi la chaîne de publication refuse tout binaire qui n'a pas été compilé avec CGO, qui n'est pas lié statiquement, ou qui ne contient pas libpcap. Voir plateformes.

Le téléchargement que vous obtenez n'exige aucune libpcap installée sur l'hôte : libpcap est liée statiquement dans le binaire publié.

Le secret d'enrôlement​

Si vous configurez control_plane.*, le secret d'enrôlement est lu depuis control_plane.enrollment_token_file. L'agent exige que ce soit un fichier ordinaire, en mode 0600 ou plus restrictif, appartenant à root ou à l'utilisateur propre de l'agent, et situé dans un répertoire qui n'est inscriptible ni par le groupe ni par tout le monde. Un répertoire dans lequel n'importe qui peut écrire permet à quelqu'un de remplacer le fichier : ne contrôler que le mode du fichier lui-même reviendrait donc à contrôler la mauvaise chose.

Le contrôle repose sur POSIX. Cela couvre tous les binaires publiés, puisque 0.1.0-ga n'est livré que sur Linux — mais il vaut la peine de savoir qu'il s'agit d'une propriété de la plateforme et non de l'agent, si une compilation Windows devait un jour apparaître.

control_plane:
gateway_url: "https://<your-cert-ix-agent-endpoint>"
ingest_url: "https://<your-cert-ix-ingest-endpoint>"
tenant_id: "<your-tenant-id>"
enrollment_token_file: "/etc/bitmapper/enrollment.token"

Les deux URL doivent être en https://, et ingest_url est obligatoire dès lors que le bloc du plan de contrôle est utilisé. tenant_id est envoyé dans l'en-tête X-Tenant-ID — c'est un identifiant, pas un secret d'authentification : ce n'est donc pas lui qui authentifie l'agent.

Le secret peut aussi provenir de l'environnement, sous le nom BITMAPPER_CONTROL_PLANE_ENROLLMENT_TOKEN, ou être entièrement remplacé par un certificat client mutual-TLS (control_plane.client_cert_file / client_key_file, le fichier de clé subissant le même contrôle de permissions). Renseigner control_plane.enrollment_token directement dans le YAML fonctionne et est déconseillé — un secret dans un fichier de configuration est un secret sur disque.

Ce qu'il coûte à l'hôte​

La capture de paquets est la chose la plus coûteuse que fasse le moindre des bits, et le coût croît avec le trafic plutôt qu'avec la taille de l'hôte. Deux réglages comptent avant tout :

  • Un filtre BPF est la réduction la moins coûteuse possible — il abandonne les paquets dans le noyau avant même qu'ils ne soient copiés vers l'agent.
  • La table des connexions est plafonnée en taille, avec éviction et ramasse-miettes périodique des entrées inactives, si bien qu'un hôte voyant passer beaucoup de connexions éphémères ne fait pas croître la table sans limite.

performance.memory_limit et performance.cpu_limit ne sont pas implémentées — elles figurent dans la liste ci-dessus. Bornez le processus avec les contrôles de votre système d'init (MemoryMax=, CPUQuota= dans une unité systemd) s'il vous faut un plafond réel.

Les flux en attente d'export sont bornés séparément par batch_export.max_buffered_connections, et non par performance.connection_table_size, dont la valeur par défaut placerait une seconde table à six chiffres sur un hôte de capture. Chaque abandon est compté et journalisé ; aucun n'est silencieux.

Plateformes​

0.1.0-ga publie un binaire signé. C'est la liste de plateformes la plus étroite de la famille, et chacune des omissions ci-dessous est une décision mesurée plutôt qu'un oubli :

PlateformePubliéePourquoi pas
Linux amd64Oui
Linux arm64NonNe peut pas être assemblée par un gcc amd64, et il n'existe pas de machine de compilation arm64
macOS amd64/arm64NonExige clang et le SDK macOS
Windows amd64NonSe compile, mais n'a jamais été exécutée sous Windows — voir ci-dessous

La cause unique derrière tout cela est que bitmapper est le seul bit qui ne peut pas être compilé de manière croisée. La capture, c'est gopacket → libpcap → CGO : une compilation exige donc une véritable chaîne d'outils C et une véritable libpcap pour la cible. Tous les autres bits sont en Go pur, et GOOS/GOARCH suffit.

Le piège que cela crée est précis et mérite d'être nommé. internal/capture porte un talon //go:build !cgo qui se compile sur toutes les plateformes et ne capture rien. Une compilation CGO_ENABLED=0 réussit donc — elle produit un binaire qui démarre, s'enregistre, émet des battements de cœur, se déclare en bonne santé, et ne voit jamais un paquet. La chaîne de publication contrôle par conséquent l'artefact plutôt que la commande de compilation : elle refuse tout binaire qui n'a pas été compilé avec CGO, qui n'est pas lié statiquement, ou qui ne contient pas libpcap.

Windows est le cas intéressant, et le cas honnête. Il se compile réellement — la prise en charge Windows de gopacket est en Go pur et charge wpcap.dll à l'exécution — nous pourrions donc le publier aujourd'hui. Il n'a jamais été exécuté sous Windows. C'est donc une lacune de test et non une lacune de chaîne d'outils, et le publier reviendrait à livrer un binaire signé dont personne n'a jamais vu fonctionner le chemin de capture. Un binaire signé qui ne peut pas capturer est pire que pas de binaire, car la signature se lit comme une affirmation d'aptitude. Il est retenu jusqu'à ce que quelqu'un l'ait exécuté.

Si vous avez besoin de ces données depuis un hôte macOS, Windows ou arm64 aujourd'hui, bitmapper n'est pas la réponse pour cet hôte. bitcollector est livré sur les cinq plateformes et rapporte les pairs réseau établis de cet hôte — une réponse plus grossière que l'attribution par flux, mais une vraie réponse sur la machine dont vous disposez.

Étapes suivantes​

Cette page vous a-t-elle été utile ?