From 51bbfa076957b35c7724ff0afa38dd7afae05066 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 19:20:10 +0900 Subject: [PATCH] 1.2.2: more NetFlow/IPFIX sampling forms, sampling=N by hand, templates shown on Settings, stopped sessions apart Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XEUxFQp8ivofZLm1o6KiXm --- CHANGELOG.md | 20 +++++- README.md | 6 +- cmd/traffic66/main.go | 2 +- docs/README.ar.md | 6 +- docs/README.bn.md | 6 +- docs/README.es.md | 6 +- docs/README.fr.md | 6 +- docs/README.hi.md | 6 +- docs/README.id.md | 6 +- docs/README.ja.md | 6 +- docs/README.ko.md | 6 +- docs/README.pt.md | 6 +- docs/README.ru.md | 6 +- docs/README.ur.md | 6 +- docs/README.zh.md | 6 +- internal/api/handlers.go | 20 +++++- internal/collector/collector.go | 14 ++++ internal/decode/nf/decoder.go | 73 +++++++++++++++++++- internal/decode/nf/fields.go | 51 +++++++++++++- internal/decode/nf/sampling_test.go | 103 ++++++++++++++++++++++++++++ internal/enrich/enrich.go | 23 +++++++ internal/web/static/app.js | 7 +- internal/web/static/i18n/ar.json | 7 +- internal/web/static/i18n/bn.json | 7 +- internal/web/static/i18n/en.json | 7 +- internal/web/static/i18n/es.json | 7 +- internal/web/static/i18n/fr.json | 7 +- internal/web/static/i18n/hi.json | 7 +- internal/web/static/i18n/id.json | 7 +- internal/web/static/i18n/ja.json | 7 +- internal/web/static/i18n/ko.json | 7 +- internal/web/static/i18n/pt.json | 7 +- internal/web/static/i18n/ru.json | 7 +- internal/web/static/i18n/ur.json | 7 +- internal/web/static/i18n/zh.json | 7 +- inventory.txt.example | 3 +- 36 files changed, 420 insertions(+), 65 deletions(-) create mode 100644 internal/decode/nf/sampling_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index a642ebc..cccaca2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,25 @@ The release notes on GitHub are taken from this file: the section whose heading is the version number. -## 1.0.4 +## 1.2.2 + +NetFlow and IPFIX sampling +- Sampling rates are found in more of the ways devices declare them: + samplingProbability (IPFIX 311); a rate per interface that is set on the + egress interface of a flow; a single sampler whose id the flow records do + not name; a rate declared under another observation domain of the same + device (line cards export under their own ids); a samplingInterval of 0 + next to samplerRandomInterval. +- A rate can be given by hand for a device that does not declare one: + sampling=N on its device line in Names (Settings). A rate the device + declares still wins; flows waiting for a rate go on with it at once. +- Settings shows the templates a device sent (template and field ids) when + its sampling rate is not declared, to see what it does send. +- Export sessions silent for an hour while the same device sends in another + way (a changed version or domain) are listed apart as stopped, not as + faults. + +## 1.2.1 Pages - Overview: **Total**, **Inbound** and **Outbound** above the bandwidth diff --git a/README.md b/README.md index c53e6e4..e315cd2 100644 --- a/README.md +++ b/README.md @@ -565,9 +565,11 @@ directly (see `inventory.txt.example`). Every line is optional. net 10.10.0.0/16 Office LAN net 203.0.113.0/24 Public servers country=JP -# device names; "unsampled" if it exports every packet (1:1) +# device names; "unsampled" if it exports every packet (1:1), +# sampling=N if it samples 1:N but does not say so in its export device 192.0.2.1 Core router device 192.0.2.9 Branch firewall unsampled +device 192.0.2.20 Edge router sampling=1000 # interface names, by device address and ifIndex; speed in bits per second iface 192.0.2.1 3 ISP uplink speed=1000000000 @@ -1091,7 +1093,7 @@ traffic66 then restarts instead of the machine running out of memory. | Symptom | Cause and fix | |---|---| | Device missing from **Settings** | Packets do not arrive: see [Check that flows arrive](#5-check-that-flows-arrive) | -| "waiting for the sampling rate" | The device has not sent its sampler options yet; most resend within minutes. If it never does, export them (`option sampler-table` on Cisco) or mark it `unsampled` in Names if it really is 1:1 | +| "waiting for the sampling rate" | The device has not sent its sampler options yet; most resend within minutes. If it never does, export them (`option sampler-table` on Cisco) or mark it `unsampled` in Names if it really is 1:1, or give its rate with `sampling=N` on its `device` line. **Settings** then lists the templates the device sent, to see what it declares | | Numbers lower than the interface counters | See **Interface check**: loss on the way, interfaces not sampled, or flows still in the device cache (active timeout longer than 60 s) | | Numbers higher than the interface counters | The same traffic sampled on two interfaces or two devices | | No countries or networks ("Unknown") | No database loaded: upload one on **Settings**, see [Countries](#8-countries-networks-and-threat-lists) | diff --git a/cmd/traffic66/main.go b/cmd/traffic66/main.go index 21ab769..dab6fba 100644 --- a/cmd/traffic66/main.go +++ b/cmd/traffic66/main.go @@ -296,7 +296,7 @@ func serve(args []string, demo bool) { pipe := pipeline.New(pipeline.Config{L2Overhead: f.l2}, st, inv, asn, thr) col := collector.New(pipe) col.NF.HoldFor = f.hold - col.NF.SetUnsampled(inv.Unsampled()) + col.SetSampling(inv.Unsampled(), inv.Sampling()) ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer stop() diff --git a/docs/README.ar.md b/docs/README.ar.md index 5f88b60..f7b2213 100644 --- a/docs/README.ar.md +++ b/docs/README.ar.md @@ -586,9 +586,11 @@ NetFlow/IPFIX (عدّادات الواجهات تتضمنه، وعدّ التد net 10.10.0.0/16 Office LAN net 203.0.113.0/24 Public servers country=JP -# device names; "unsampled" if it exports every packet (1:1) +# device names; "unsampled" if it exports every packet (1:1), +# sampling=N if it samples 1:N but does not say so in its export device 192.0.2.1 Core router device 192.0.2.9 Branch firewall unsampled +device 192.0.2.20 Edge router sampling=1000 # interface names, by device address and ifIndex; speed in bits per second iface 192.0.2.1 3 ISP uplink speed=1000000000 @@ -1109,7 +1111,7 @@ traffic66 الملف `license.json` في دليل البيانات مع رقم | العَرَض | السبب والحل | |---|---| | الجهاز غير ظاهر في **الإعدادات** | الحزم لا تصل: انظر [التحقق من وصول التدفقات](#5-check-that-flows-arrive) | -| "waiting for the sampling rate" | لم يرسل الجهاز خيارات أخذ العينات بعد؛ ومعظم الأجهزة تعيد إرسالها خلال دقائق. إن لم يفعل أبدًا، فصدّرها (`option sampler-table` على Cisco) أو علّم الجهاز بـ `unsampled` في الأسماء إن كان يصدّر فعلًا بنسبة 1:1 | +| "waiting for the sampling rate" | لم يرسل الجهاز خيارات أخذ العينات بعد؛ ومعظم الأجهزة تعيد إرسالها خلال دقائق. إن لم يفعل أبدًا، فصدّرها (`option sampler-table` على Cisco) أو علّم الجهاز بـ `unsampled` في الأسماء إن كان يصدّر فعلًا بنسبة 1:1، أو حدّد نسبته بـ `sampling=N` في سطر `device` الخاص به. بعد ذلك تعرض **الإعدادات** القوالب التي أرسلها الجهاز، لمعرفة ما يصرّح به | | الأرقام أقل من عدّادات الواجهات | انظر **مطابقة الواجهات**: فقد في الطريق، أو واجهات لا تؤخذ منها عينات، أو تدفقات ما زالت في ذاكرة الجهاز المؤقتة (مهلة التدفق النشط أطول من 60 ثانية) | | الأرقام أعلى من عدّادات الواجهات | تؤخذ عينات الحركة نفسها على واجهتين أو جهازين | | لا توجد دول أو شبكات ("غير معروف") | لم تُحمَّل أي قاعدة بيانات: ارفع واحدة في **الإعدادات**، وانظر [الدول](#8-countries-networks-and-threat-lists) | diff --git a/docs/README.bn.md b/docs/README.bn.md index eef16e1..c8b1982 100644 --- a/docs/README.bn.md +++ b/docs/README.bn.md @@ -595,9 +595,11 @@ entry। **টেক্সট হিসেবে সম্পাদনা (উ net 10.10.0.0/16 Office LAN net 203.0.113.0/24 Public servers country=JP -# device names; "unsampled" if it exports every packet (1:1) +# device names; "unsampled" if it exports every packet (1:1), +# sampling=N if it samples 1:N but does not say so in its export device 192.0.2.1 Core router device 192.0.2.9 Branch firewall unsampled +device 192.0.2.20 Edge router sampling=1000 # interface names, by device address and ifIndex; speed in bits per second iface 192.0.2.1 3 ISP uplink speed=1000000000 @@ -1123,7 +1125,7 @@ container-এর memory limit। `-memory`-র ভাগের প্রায | লক্ষণ | কারণ ও সমাধান | |---|---| | ডিভাইস **সেটিংস**-এ নেই | packet পৌঁছাচ্ছে না: [flow পৌঁছাচ্ছে কি না দেখুন](#5-check-that-flows-arrive) দেখুন | -| "waiting for the sampling rate" | ডিভাইস এখনও sampler options পাঠায়নি; বেশিরভাগই কয়েক মিনিটের মধ্যে আবার পাঠায়। কখনো না পাঠালে সেগুলো export করান (Cisco-তে `option sampler-table`) অথবা সত্যিই 1:1 হলে নাম-এ সেটিকে `unsampled` চিহ্নিত করুন | +| "waiting for the sampling rate" | ডিভাইস এখনও sampler options পাঠায়নি; বেশিরভাগই কয়েক মিনিটের মধ্যে আবার পাঠায়। কখনো না পাঠালে সেগুলো export করান (Cisco-তে `option sampler-table`) অথবা সত্যিই 1:1 হলে নাম-এ সেটিকে `unsampled` চিহ্নিত করুন, অথবা তার `device` লাইনে `sampling=N` দিয়ে rate দিন। তারপর **সেটিংস**-এ ডিভাইসের পাঠানো template-গুলো দেখা যায়, যাতে বোঝা যায় সেটি কী ঘোষণা করছে | | সংখ্যা interface counter-এর চেয়ে কম | **ইন্টারফেস মিলানো** দেখুন: পথে loss, interface sample হচ্ছে না, অথবা flow এখনও ডিভাইসের cache-এ (active timeout 60 s-এর বেশি) | | সংখ্যা interface counter-এর চেয়ে বেশি | একই ট্রাফিক দুটি interface বা দুটি ডিভাইসে sample হচ্ছে | | কোনো দেশ বা নেটওয়ার্ক নেই ("অজানা") | কোনো database লোড করা নেই: **সেটিংস**-এ একটি আপলোড করুন, [দেশ](#8-countries-networks-and-threat-lists) দেখুন | diff --git a/docs/README.es.md b/docs/README.es.md index 8ac2f2c..9d18cd4 100644 --- a/docs/README.es.md +++ b/docs/README.es.md @@ -609,9 +609,11 @@ las líneas son opcionales. net 10.10.0.0/16 Office LAN net 203.0.113.0/24 Public servers country=JP -# device names; "unsampled" if it exports every packet (1:1) +# device names; "unsampled" if it exports every packet (1:1), +# sampling=N if it samples 1:N but does not say so in its export device 192.0.2.1 Core router device 192.0.2.9 Branch firewall unsampled +device 192.0.2.20 Edge router sampling=1000 # interface names, by device address and ifIndex; speed in bits per second iface 192.0.2.1 3 ISP uplink speed=1000000000 @@ -1166,7 +1168,7 @@ máquina sin memoria. | Síntoma | Causa y solución | |---|---| | El equipo no aparece en **Configuración** | Los paquetes no llegan: vea [Comprobar que llegan los flujos](#5-check-that-flows-arrive) | -| "waiting for the sampling rate" | El equipo aún no ha enviado sus opciones de sampler; la mayoría las reenvía en pocos minutos. Si no lo hace nunca, expórtelas (`option sampler-table` en Cisco) o márquelo como `unsampled` en Nombres si de verdad es 1:1 | +| "waiting for the sampling rate" | El equipo aún no ha enviado sus opciones de sampler; la mayoría las reenvía en pocos minutos. Si no lo hace nunca, expórtelas (`option sampler-table` en Cisco) o márquelo como `unsampled` en Nombres si de verdad es 1:1, o indique su tasa con `sampling=N` en su línea `device`. **Configuración** muestra entonces las plantillas que envió el equipo, para ver qué declara | | Cifras por debajo de los contadores de interfaz | Vea **Verificación de interfaces**: pérdidas por el camino, interfaces sin muestrear o flujos aún en la caché del equipo (timeout activo superior a 60 s) | | Cifras por encima de los contadores de interfaz | El mismo tráfico se muestrea en dos interfaces o en dos equipos | | No hay países ni redes ("Desconocido") | No hay ninguna base de datos cargada: suba una en **Configuración**; vea [Países](#8-countries-networks-and-threat-lists) | diff --git a/docs/README.fr.md b/docs/README.fr.md index 2b8a160..eb66a16 100644 --- a/docs/README.fr.md +++ b/docs/README.fr.md @@ -625,9 +625,11 @@ fichier, et vous pouvez aussi l'éditer directement (voir net 10.10.0.0/16 Office LAN net 203.0.113.0/24 Public servers country=JP -# device names; "unsampled" if it exports every packet (1:1) +# device names; "unsampled" if it exports every packet (1:1), +# sampling=N if it samples 1:N but does not say so in its export device 192.0.2.1 Core router device 192.0.2.9 Branch firewall unsampled +device 192.0.2.20 Edge router sampling=1000 # interface names, by device address and ifIndex; speed in bits per second iface 192.0.2.1 3 ISP uplink speed=1000000000 @@ -1202,7 +1204,7 @@ lieu que la machine manque de mémoire. | Symptôme | Cause et solution | |---|---| | Équipement absent de **Paramètres** | Les paquets n'arrivent pas : voir [Vérifier que les flux arrivent](#5-check-that-flows-arrive) | -| "waiting for the sampling rate" | L'équipement n'a pas encore envoyé ses options de sampler ; la plupart les renvoient en quelques minutes. S'il ne le fait jamais, exportez-les (`option sampler-table` sur Cisco) ou marquez-le `unsampled` dans Noms s'il est réellement en 1:1 | +| "waiting for the sampling rate" | L'équipement n'a pas encore envoyé ses options de sampler ; la plupart les renvoient en quelques minutes. S'il ne le fait jamais, exportez-les (`option sampler-table` sur Cisco) ou marquez-le `unsampled` dans Noms s'il est réellement en 1:1, ou indiquez son taux avec `sampling=N` sur sa ligne `device`. **Paramètres** liste alors les templates envoyés par l'équipement, pour voir ce qu'il déclare | | Chiffres inférieurs aux compteurs d'interface | Voir **Contrôle des interfaces** : pertes en route, interfaces non échantillonnées, ou flux encore dans le cache de l'équipement (timeout actif supérieur à 60 s) | | Chiffres supérieurs aux compteurs d'interface | Le même trafic est échantillonné sur deux interfaces ou deux équipements | | Pas de pays ni de réseaux ("Inconnu") | Aucune base de données chargée : importez-en une dans **Paramètres**, voir [Pays](#8-countries-networks-and-threat-lists) | diff --git a/docs/README.hi.md b/docs/README.hi.md index f14e762..0e225b1 100644 --- a/docs/README.hi.md +++ b/docs/README.hi.md @@ -595,9 +595,11 @@ edit कर सकते हैं (`inventory.txt.example` देखें)। net 10.10.0.0/16 Office LAN net 203.0.113.0/24 Public servers country=JP -# device names; "unsampled" if it exports every packet (1:1) +# device names; "unsampled" if it exports every packet (1:1), +# sampling=N if it samples 1:N but does not say so in its export device 192.0.2.1 Core router device 192.0.2.9 Branch firewall unsampled +device 192.0.2.20 Edge router sampling=1000 # interface names, by device address and ifIndex; speed in bits per second iface 192.0.2.1 3 ISP uplink speed=1000000000 @@ -1128,7 +1130,7 @@ restart हो जाता है। | लक्षण | कारण और समाधान | |---|---| | डिवाइस **सेटिंग्स** में नहीं है | Packets नहीं पहुँच रहे: [जाँचें कि flows पहुँच रहे हैं](#5-check-that-flows-arrive) देखें | -| "waiting for the sampling rate" | डिवाइस ने अभी तक अपने sampler options नहीं भेजे; ज़्यादातर कुछ मिनटों में दोबारा भेज देते हैं। अगर कभी न भेजे, तो उन्हें export करवाएँ (Cisco पर `option sampler-table`) या अगर वह सच में 1:1 है तो नाम में उसे `unsampled` लिखें | +| "waiting for the sampling rate" | डिवाइस ने अभी तक अपने sampler options नहीं भेजे; ज़्यादातर कुछ मिनटों में दोबारा भेज देते हैं। अगर कभी न भेजे, तो उन्हें export करवाएँ (Cisco पर `option sampler-table`) या अगर वह सच में 1:1 है तो नाम में उसे `unsampled` लिखें, या उसकी `device` line पर `sampling=N` से rate दें। फिर **सेटिंग्स** में डिवाइस द्वारा भेजे गए templates दिखते हैं, जिससे पता चलता है कि वह क्या घोषित करता है | | आँकड़े interface counters से कम | **इंटरफ़ेस मिलान** देखें: रास्ते में loss, interfaces sample नहीं हो रहे, या flows अभी डिवाइस के cache में हैं (active timeout 60 s से लंबा) | | आँकड़े interface counters से ज़्यादा | वही ट्रैफ़िक दो interfaces या दो डिवाइसों पर sample हो रहा है | | कोई देश या नेटवर्क नहीं ("अज्ञात") | कोई database लोड नहीं है: **सेटिंग्स** पर एक अपलोड करें, [देश](#8-countries-networks-and-threat-lists) देखें | diff --git a/docs/README.id.md b/docs/README.id.md index f43f432..173be24 100644 --- a/docs/README.id.md +++ b/docs/README.id.md @@ -608,9 +608,11 @@ bersifat opsional. net 10.10.0.0/16 Office LAN net 203.0.113.0/24 Public servers country=JP -# device names; "unsampled" if it exports every packet (1:1) +# device names; "unsampled" if it exports every packet (1:1), +# sampling=N if it samples 1:N but does not say so in its export device 192.0.2.1 Core router device 192.0.2.9 Branch firewall unsampled +device 192.0.2.20 Edge router sampling=1000 # interface names, by device address and ifIndex; speed in bits per second iface 192.0.2.1 3 ISP uplink speed=1000000000 @@ -1159,7 +1161,7 @@ kehabisan memori. | Gejala | Penyebab dan solusi | |---|---| | Perangkat tidak muncul di **Pengaturan** | Paket tidak sampai: lihat [Pastikan flow masuk](#5-check-that-flows-arrive) | -| "waiting for the sampling rate" | Perangkat belum mengirim sampler options; kebanyakan mengirim ulang dalam beberapa menit. Jika tidak pernah, ekspor opsi itu (`option sampler-table` di Cisco) atau tandai perangkat sebagai `unsampled` di Nama jika memang 1:1 | +| "waiting for the sampling rate" | Perangkat belum mengirim sampler options; kebanyakan mengirim ulang dalam beberapa menit. Jika tidak pernah, ekspor opsi itu (`option sampler-table` di Cisco) atau tandai perangkat sebagai `unsampled` di Nama jika memang 1:1, atau beri rasionya dengan `sampling=N` di baris `device`-nya. **Pengaturan** lalu menampilkan template yang dikirim perangkat, untuk melihat apa yang dideklarasikannya | | Angka lebih rendah dari counter interface | Lihat **Pencocokan antarmuka**: loss di jalan, interface tidak di-sampling, atau flow masih di cache perangkat (active timeout lebih dari 60 detik) | | Angka lebih tinggi dari counter interface | Trafik yang sama di-sampling di dua interface atau dua perangkat | | Tidak ada negara atau jaringan ("Tidak diketahui") | Tidak ada basis data yang dimuat: unggah di **Pengaturan**, lihat [Negara](#8-countries-networks-and-threat-lists) | diff --git a/docs/README.ja.md b/docs/README.ja.md index a915d48..12a17dd 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -488,9 +488,11 @@ softflowd -i eth0 -n 192.0.2.50:2055 -v 9 -t maxlife=60 net 10.10.0.0/16 Office LAN net 203.0.113.0/24 Public servers country=JP -# device names; "unsampled" if it exports every packet (1:1) +# device names; "unsampled" if it exports every packet (1:1), +# sampling=N if it samples 1:N but does not say so in its export device 192.0.2.1 Core router device 192.0.2.9 Branch firewall unsampled +device 192.0.2.20 Edge router sampling=1000 # interface names, by device address and ifIndex; speed in bits per second iface 192.0.2.1 3 ISP uplink speed=1000000000 @@ -873,7 +875,7 @@ traffic66 は [PolyForm Noncommercial License 1.0.0](../LICENSE.md) と [Traffic | 症状 | 原因と対処 | |---|---| | **設定** に機器が表示されない | パケットが届いていません:[フローの受信を確認する](#5-check-that-flows-arrive) を参照 | -| "waiting for the sampling rate" | 機器がまだサンプラーオプションを送っていません。多くの機器は数分以内に再送します。いつまでも送られない場合はエクスポートを設定する(Cisco では `option sampler-table`)か、本当に 1:1 なら名前で `unsampled` を付けます | +| "waiting for the sampling rate" | 機器がまだサンプラーオプションを送っていません。多くの機器は数分以内に再送します。いつまでも送られない場合はエクスポートを設定する(Cisco では `option sampler-table`)か、本当に 1:1 なら名前で `unsampled` を付けるか、`device` 行に `sampling=N` でレートを指定します。その後 **設定** に機器が送ったテンプレートが一覧表示されるので、何を宣言しているか確認できます | | 数値がインターフェースカウンターより小さい | **インターフェース照合** を確認:途中でのロス、サンプリングされていないインターフェース、またはフローがまだ機器のキャッシュ内にある(アクティブタイムアウトが 60 秒より長い) | | 数値がインターフェースカウンターより大きい | 同じトラフィックを 2 つのインターフェースまたは 2 台の機器でサンプリングしています | | 国やネットワークが表示されない("不明") | データベースが読み込まれていません。**設定** でアップロードしてください:[国](#8-countries-networks-and-threat-lists) を参照 | diff --git a/docs/README.ko.md b/docs/README.ko.md index b963fb7..81d824a 100644 --- a/docs/README.ko.md +++ b/docs/README.ko.md @@ -571,9 +571,11 @@ traffic66에 도착하기 전에 유실됨, 샘플링 레이트를 아직 모름 net 10.10.0.0/16 Office LAN net 203.0.113.0/24 Public servers country=JP -# device names; "unsampled" if it exports every packet (1:1) +# device names; "unsampled" if it exports every packet (1:1), +# sampling=N if it samples 1:N but does not say so in its export device 192.0.2.1 Core router device 192.0.2.9 Branch firewall unsampled +device 192.0.2.20 Edge router sampling=1000 # interface names, by device address and ifIndex; speed in bits per second iface 192.0.2.1 3 ISP uplink speed=1000000000 @@ -1065,7 +1067,7 @@ Top 66은 약 9초이며, 소요 시간은 범위에 비례해 늘고 코어가 | 증상 | 원인과 해결 | |---|---| | **설정**에 장비가 없음 | 패킷이 도착하지 않음: [플로 수신 확인](#5-check-that-flows-arrive) 참조 | -| "waiting for the sampling rate" | 장비가 아직 샘플러 옵션을 보내지 않았습니다. 대부분 몇 분 안에 다시 보냅니다. 끝내 보내지 않으면 익스포트하도록 설정하거나(Cisco는 `option sampler-table`), 정말 1:1이라면 이름에서 `unsampled`로 표시합니다 | +| "waiting for the sampling rate" | 장비가 아직 샘플러 옵션을 보내지 않았습니다. 대부분 몇 분 안에 다시 보냅니다. 끝내 보내지 않으면 익스포트하도록 설정하거나(Cisco는 `option sampler-table`), 정말 1:1이라면 이름에서 `unsampled`로 표시하거나, 해당 `device` 줄에 `sampling=N`으로 비율을 지정합니다. 그러면 **설정**에 장비가 보낸 템플릿이 표시되어 무엇을 선언하는지 확인할 수 있습니다 | | 수치가 인터페이스 카운터보다 낮음 | **인터페이스 대조** 확인: 경로상 유실, 샘플링되지 않은 인터페이스, 또는 플로가 아직 장비 캐시에 있음(액티브 타임아웃이 60초보다 김) | | 수치가 인터페이스 카운터보다 높음 | 같은 트래픽을 두 인터페이스 또는 두 장비에서 샘플링함 | | 국가나 네트워크가 표시되지 않음("알 수 없음") | 로드된 데이터베이스가 없음: **설정**에서 업로드. [국가](#8-countries-networks-and-threat-lists) 참조 | diff --git a/docs/README.pt.md b/docs/README.pt.md index 7cd786a..f2d8987 100644 --- a/docs/README.pt.md +++ b/docs/README.pt.md @@ -609,9 +609,11 @@ Todas as linhas são opcionais. net 10.10.0.0/16 Office LAN net 203.0.113.0/24 Public servers country=JP -# device names; "unsampled" if it exports every packet (1:1) +# device names; "unsampled" if it exports every packet (1:1), +# sampling=N if it samples 1:N but does not say so in its export device 192.0.2.1 Core router device 192.0.2.9 Branch firewall unsampled +device 192.0.2.20 Edge router sampling=1000 # interface names, by device address and ifIndex; speed in bits per second iface 192.0.2.1 3 ISP uplink speed=1000000000 @@ -1161,7 +1163,7 @@ ficar sem memória. | Sintoma | Causa e solução | |---|---| | Equipamento não aparece em **Configurações** | Os pacotes não chegam: veja [Verificar se os fluxos estão chegando](#5-check-that-flows-arrive) | -| "waiting for the sampling rate" | O equipamento ainda não enviou as opções do sampler; a maioria reenvia em poucos minutos. Se nunca enviar, exporte-as (`option sampler-table` no Cisco) ou marque-o como `unsampled` em Nomes se ele for de fato 1:1 | +| "waiting for the sampling rate" | O equipamento ainda não enviou as opções do sampler; a maioria reenvia em poucos minutos. Se nunca enviar, exporte-as (`option sampler-table` no Cisco) ou marque-o como `unsampled` em Nomes se ele for de fato 1:1, ou informe a taxa com `sampling=N` na linha `device` dele. **Configurações** lista então os templates que o equipamento enviou, para ver o que ele declara | | Números abaixo dos contadores de interface | Veja **Conferência de interfaces**: perdas no caminho, interfaces não amostradas ou fluxos ainda no cache do equipamento (timeout ativo acima de 60 s) | | Números acima dos contadores de interface | O mesmo tráfego amostrado em duas interfaces ou dois equipamentos | | Sem países nem redes ("Desconhecido") | Nenhum banco de dados carregado: envie um em **Configurações**; veja [Países](#8-countries-networks-and-threat-lists) | diff --git a/docs/README.ru.md b/docs/README.ru.md index a882934..ed832ff 100644 --- a/docs/README.ru.md +++ b/docs/README.ru.md @@ -603,9 +603,11 @@ Enter; оно сразу сохраняется и показывается ве net 10.10.0.0/16 Office LAN net 203.0.113.0/24 Public servers country=JP -# device names; "unsampled" if it exports every packet (1:1) +# device names; "unsampled" if it exports every packet (1:1), +# sampling=N if it samples 1:N but does not say so in its export device 192.0.2.1 Core router device 192.0.2.9 Branch firewall unsampled +device 192.0.2.20 Edge router sampling=1000 # interface names, by device address and ifIndex; speed in bits per second iface 192.0.2.1 3 ISP uplink speed=1000000000 @@ -1150,7 +1152,7 @@ traffic66 -data /var/lib/traffic66 -listen "sflow=:6343,netflow=:2055,ipfix=:473 | Симптом | Причина и решение | |---|---| | Устройства нет на странице **Настройки** | Пакеты не доходят: см. [Проверка поступления потоков](#5-check-that-flows-arrive) | -| "waiting for the sampling rate" | Устройство ещё не прислало sampler options; большинство устройств повторяют их в течение нескольких минут. Если так и не пришлёт, включите их экспорт (`option sampler-table` на Cisco) или пометьте устройство как `unsampled` в разделе «Названия», если оно действительно экспортирует 1:1 | +| "waiting for the sampling rate" | Устройство ещё не прислало sampler options; большинство устройств повторяют их в течение нескольких минут. Если так и не пришлёт, включите их экспорт (`option sampler-table` на Cisco) или пометьте устройство как `unsampled` в разделе «Названия», если оно действительно экспортирует 1:1, либо укажите его коэффициент через `sampling=N` в строке `device`. На странице **Настройки** затем видны шаблоны, присланные устройством, — по ним понятно, что оно объявляет | | Цифры ниже счётчиков интерфейсов | См. **Сверка интерфейсов**: потери по пути, несэмплируемые интерфейсы или потоки ещё в кеше устройства (active timeout больше 60 с) | | Цифры выше счётчиков интерфейсов | Один и тот же трафик сэмплируется на двух интерфейсах или двух устройствах | | Нет стран и сетей («Неизвестно») | Не загружена база: загрузите её на странице **Настройки**, см. [Страны](#8-countries-networks-and-threat-lists) | diff --git a/docs/README.ur.md b/docs/README.ur.md index 85e3410..d02a052 100644 --- a/docs/README.ur.md +++ b/docs/README.ur.md @@ -596,9 +596,11 @@ entry۔ **متن کے طور پر ترمیم (ایڈوانسڈ)** وہ فائل net 10.10.0.0/16 Office LAN net 203.0.113.0/24 Public servers country=JP -# device names; "unsampled" if it exports every packet (1:1) +# device names; "unsampled" if it exports every packet (1:1), +# sampling=N if it samples 1:N but does not say so in its export device 192.0.2.1 Core router device 192.0.2.9 Branch firewall unsampled +device 192.0.2.20 Edge router sampling=1000 # interface names, by device address and ifIndex; speed in bits per second iface 192.0.2.1 3 ISP uplink speed=1000000000 @@ -1124,7 +1126,7 @@ container کی memory limit۔ `-memory` والے حصے کا تقریباً 2.5 | علامت | وجہ اور حل | |---|---| | ڈیوائس **ترتیبات** میں نہیں | packets نہیں پہنچ رہے: [جانچیں کہ flows پہنچ رہے ہیں](#5-check-that-flows-arrive) دیکھیں | -| "waiting for the sampling rate" | ڈیوائس نے ابھی تک اپنے sampler options نہیں بھیجے؛ زیادہ تر چند منٹ میں دوبارہ بھیج دیتی ہیں۔ اگر کبھی نہ بھیجے تو انہیں export کروائیں (Cisco پر `option sampler-table`) یا اگر وہ واقعی 1:1 ہے تو نام میں اسے `unsampled` لکھیں | +| "waiting for the sampling rate" | ڈیوائس نے ابھی تک اپنے sampler options نہیں بھیجے؛ زیادہ تر چند منٹ میں دوبارہ بھیج دیتی ہیں۔ اگر کبھی نہ بھیجے تو انہیں export کروائیں (Cisco پر `option sampler-table`) یا اگر وہ واقعی 1:1 ہے تو نام میں اسے `unsampled` لکھیں، یا اس کی `device` لائن پر `sampling=N` سے rate دیں۔ پھر **ترتیبات** میں ڈیوائس کے بھیجے ہوئے templates نظر آتے ہیں، جن سے پتا چلتا ہے کہ وہ کیا declare کرتی ہے | | اعداد interface counters سے کم | **انٹرفیس جانچ** دیکھیں: راستے میں loss، interfaces sample نہیں ہو رہے، یا flows ابھی ڈیوائس کی cache میں ہیں (active timeout 60 s سے لمبا) | | اعداد interface counters سے زیادہ | ایک ہی ٹریفک دو interfaces یا دو ڈیوائسز پر sample ہو رہی ہے | | کوئی ملک یا نیٹ ورک نہیں ("نامعلوم") | کوئی database لوڈ نہیں: **ترتیبات** پر ایک اپ لوڈ کریں، [ممالک](#8-countries-networks-and-threat-lists) دیکھیں | diff --git a/docs/README.zh.md b/docs/README.zh.md index 27ef965..ea35312 100644 --- a/docs/README.zh.md +++ b/docs/README.zh.md @@ -488,9 +488,11 @@ softflowd -i eth0 -n 192.0.2.50:2055 -v 9 -t maxlife=60 net 10.10.0.0/16 Office LAN net 203.0.113.0/24 Public servers country=JP -# device names; "unsampled" if it exports every packet (1:1) +# device names; "unsampled" if it exports every packet (1:1), +# sampling=N if it samples 1:N but does not say so in its export device 192.0.2.1 Core router device 192.0.2.9 Branch firewall unsampled +device 192.0.2.20 Edge router sampling=1000 # interface names, by device address and ifIndex; speed in bits per second iface 192.0.2.1 3 ISP uplink speed=1000000000 @@ -873,7 +875,7 @@ traffic66 以 [PolyForm Noncommercial License 1.0.0](../LICENSE.md) 和 [Traffic | 现象 | 原因与解决方法 | |---|---| | **设定** 中看不到设备 | 报文没有到达:见 [确认流数据已到达](#5-check-that-flows-arrive) | -| "waiting for the sampling rate" | 设备尚未发送采样器选项;大多数设备几分钟内会重发。如果一直不发,请配置导出(Cisco 上为 `option sampler-table`);如果确实是 1:1,可在名称中标记为 `unsampled` | +| "waiting for the sampling rate" | 设备尚未发送采样器选项;大多数设备几分钟内会重发。如果一直不发,请配置导出(Cisco 上为 `option sampler-table`);如果确实是 1:1,可在名称中标记为 `unsampled`,或在其 `device` 行用 `sampling=N` 指定采样率。之后 **设定** 会列出该设备发送的模板,可据此查看它声明了什么 | | 统计值低于接口计数器 | 查看 **接口对账**:途中丢包、有接口未采样,或流仍在设备缓存中(活动超时超过 60 秒) | | 统计值高于接口计数器 | 同一流量在两个接口或两台设备上被采样 | | 没有国家或网络信息("未知") | 未加载数据库:在 **设定** 上传一个,见 [国家](#8-countries-networks-and-threat-lists) | diff --git a/internal/api/handlers.go b/internal/api/handlers.go index 5ce1b01..2e6cd01 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -485,7 +485,15 @@ func (s *Server) sources(w http.ResponseWriter, r *http.Request) { now := time.Now() s.mu.Lock() var out []sourceOut - for _, si := range s.Col.Sources() { + var stale []map[string]any + srcs := s.Col.Sources() + active := map[string]bool{} + for _, si := range srcs { + if now.Sub(si.LastSeen) < 2*time.Minute { + active[si.Exporter] = true + } + } + for _, si := range srcs { o := sourceOut{SourceInfo: si, Status: "ok"} o.RecPS = srcRate.ps[srcKey(si)] if a, err := netip.ParseAddr(si.Exporter); err == nil { @@ -521,6 +529,12 @@ func (s *Server) sources(w http.ResponseWriter, r *http.Request) { if si.DecodeErrors > 0 && si.DecodeErrors*100 > si.Packets { issue("warn", "decode_errors") } + // silent for an hour while the same device sends otherwise (a + // changed export version or domain): listed apart, not as a fault + if now.Sub(si.LastSeen) > time.Hour && active[si.Exporter] { + stale = append(stale, map[string]any{"exporter": si.Exporter, "proto": si.Proto, "domain": si.Domain, "last_seen": si.LastSeen, "name": o.Name}) + continue + } out = append(out, o) } s.mu.Unlock() @@ -528,7 +542,7 @@ func (s *Server) sources(w http.ResponseWriter, r *http.Request) { for _, l := range s.Col.Listeners { ls = append(ls, map[string]any{"addr": l.Addr, "proto": l.Proto, "packets": l.Packets.Load(), "undecoded": l.Undecoded.Load(), "rcvbuf": l.RcvBuf}) } - res := map[string]any{"sources": out, "listeners": ls} + res := map[string]any{"sources": out, "listeners": ls, "stale": stale} if s.Capture != nil { res["captures"] = s.Capture() } @@ -638,7 +652,7 @@ func (s *Server) putInventory(w http.ResponseWriter, r *http.Request) { fail(w, err) return } - s.Col.NF.SetUnsampled(s.Inv.Unsampled()) + s.Col.SetSampling(s.Inv.Unsampled(), s.Inv.Sampling()) writeJSON(w, http.StatusOK, map[string]string{"ok": "saved"}) } diff --git a/internal/collector/collector.go b/internal/collector/collector.go index 9509fbc..cbae113 100644 --- a/internal/collector/collector.go +++ b/internal/collector/collector.go @@ -308,6 +308,9 @@ type SourceInfo struct { DecodeErrors uint64 `json:"decode_errors"` LastSeen time.Time `json:"last_seen"` ClockSkew time.Duration `json:"clock_skew_ns"` + // NetFlow/IPFIX: the templates received (id and field ids), shown when + // the sampling rate is not declared + TemplateInfo []string `json:"template_info,omitempty"` } // Sources lists every exporter seen. @@ -337,6 +340,7 @@ func (c *Collector) Sources() []SourceInfo { Packets: s.Packets, Records: s.Records, LostPct: lost, LostRecords: s.LostRecords, TransitPct: lost, Sampling: samp, SamplingState: s.SamplingState, Pending: s.Pending, NoTemplate: s.NoTemplate, Templates: s.Templates, LastSeen: s.LastSeen, ClockSkew: s.ClockSkew, LossComp: s.LossComp, + TemplateInfo: s.TemplateInfo, }) } c.mu.Lock() @@ -388,3 +392,13 @@ func (c *Collector) Sources() []SourceInfo { sort.Slice(out, func(i, j int) bool { return out[i].Exporter < out[j].Exporter }) return out } + +// SetSampling applies the names' sampling settings: devices that export +// every packet, and hand-set rates for devices that do not declare one. +// Records that were waiting for a rate go on with it. +func (c *Collector) SetSampling(unsampled map[netip.Addr]bool, manual map[netip.Addr]uint32) { + c.NF.SetUnsampled(unsampled) + if res := c.NF.SetManual(manual); len(res.Records) > 0 { + c.sink.Submit(res.Records) + } +} diff --git a/internal/decode/nf/decoder.go b/internal/decode/nf/decoder.go index a1e3091..9b8f5c0 100644 --- a/internal/decode/nf/decoder.go +++ b/internal/decode/nf/decoder.go @@ -31,6 +31,8 @@ type Stats struct { Sampling uint32 // exporter-wide rate if known Rates []uint32 // every declared rate (exporter, sampler, interface) Templates int + TemplateInfo []string // the templates seen and their field ids + Manual bool // the rate was set by hand (sampling= in the names) // LossComp is the multiplier currently applied to make up for records // lost in transit (1 = no loss). LossComp float64 @@ -74,6 +76,7 @@ type samplerRef struct { samplerID uint32 hasSampler bool inIf uint32 + outIf uint32 } type session struct { @@ -90,6 +93,8 @@ type session struct { pending []pendingRec + tmplDesc map[uint16]string // templates seen, for the sources page + initMs int64 seqInit bool @@ -127,6 +132,11 @@ type Decoder struct { MaxSessions int // Unsampled lists exporters known to export every packet. Unsampled map[netip.Addr]bool + // Manual is the sampling rate of exporters that do not declare it, + // from sampling= in the names; a declared rate takes precedence. + Manual map[netip.Addr]uint32 + // byAddr is the last rate an exporter declared, in any domain. + byAddr map[netip.Addr]uint32 } func NewDecoder() *Decoder { @@ -136,6 +146,8 @@ func NewDecoder() *Decoder { MaxPending: 200000, MaxSessions: 10000, Unsampled: map[netip.Addr]bool{}, + Manual: map[netip.Addr]uint32{}, + byAddr: map[netip.Addr]uint32{}, } } @@ -190,7 +202,17 @@ func (d *Decoder) Stats() []Stats { st := s.stats st.Pending = len(s.pending) st.Templates = len(s.templates) - st.Sampling = s.def + for _, v := range s.tmplDesc { + st.TemplateInfo = append(st.TemplateInfo, v) + } + sort.Strings(st.TemplateInfo) + if s.def == 0 && len(s.bySampler) == 0 && len(s.byIf) == 0 && d.Manual[s.stats.Exporter] > 0 { + st.Manual = true + st.Sampling = d.Manual[s.stats.Exporter] + } + if !st.Manual { + st.Sampling = s.def + } st.LossComp = s.comp seen := map[uint32]bool{} add := func(r uint32) { @@ -200,6 +222,9 @@ func (d *Decoder) Stats() []Stats { } } add(s.def) + if st.Manual { + add(st.Sampling) + } for _, r := range s.bySampler { add(r) } @@ -208,6 +233,8 @@ func (d *Decoder) Stats() []Stats { } sort.Slice(st.Rates, func(i, j int) bool { return st.Rates[i] < st.Rates[j] }) switch { + case st.Manual: + st.SamplingState = "manual" case s.assumed: st.SamplingState = "assumed1" case len(s.pending) > 0: @@ -272,12 +299,43 @@ func (d *Decoder) rateFor(s *session, rec *flow.Record, ref samplerRef, inRecord if rate == 0 { rate = s.byIf[ref.inIf] } + if rate == 0 && ref.outIf != 0 { + // sampling configured on the egress interface + rate = s.byIf[ref.outIf] + } if rate == 0 { rate = s.def } + if rate == 0 { + // one rate declared for a sampler or interface the record does not + // name: it is the only one there is + rate = s.onlyRate() + } + if rate == 0 { + // declared by the same device in another observation domain (line + // cards export under their own ids) + rate = d.byAddr[rec.Exporter] + } + if rate == 0 { + rate = d.Manual[rec.Exporter] + } return rate } +// onlyRate is the session's rate when all its declared rates agree. +func (s *session) onlyRate() uint32 { + var r uint32 + for _, m := range []map[uint32]uint32{s.bySampler, s.byIf} { + for _, x := range m { + if r != 0 && x != r { + return 0 + } + r = x + } + } + return r +} + // resolve fills in the sampling multiplier or queues the record. func (d *Decoder) resolve(s *session, rec flow.Record, ref samplerRef, inRecordRate uint32, now time.Time, out *[]flow.Record) { rate := d.rateFor(s, &rec, ref, inRecordRate) @@ -409,3 +467,16 @@ func (d *Decoder) SetUnsampled(m map[netip.Addr]bool) { d.Unsampled = m d.mu.Unlock() } + +// SetManual replaces the hand-set sampling rates; records waiting for a +// rate are released with them. +func (d *Decoder) SetManual(m map[netip.Addr]uint32) Result { + d.mu.Lock() + defer d.mu.Unlock() + d.Manual = m + var res Result + for _, s := range d.sessions { + d.retryPending(s, &res.Records) + } + return res +} diff --git a/internal/decode/nf/fields.go b/internal/decode/nf/fields.go index 0a433b2..caf8835 100644 --- a/internal/decode/nf/fields.go +++ b/internal/decode/nf/fields.go @@ -2,7 +2,10 @@ package nf import ( "encoding/binary" + "fmt" + "math" "net/netip" + "strings" "time" "github.com/githubflyideas/traffic66/internal/flow" @@ -64,6 +67,7 @@ const ( ieSamplingPktSpace = 306 ieSamplingSize = 309 ieSamplingPop = 310 + ieSamplingProb = 311 // samplingProbability, float64 // NetFlow v9 option scope types (RFC 3954 section 6.1). nf9ScopeSystem = 1 @@ -101,6 +105,7 @@ type acc struct { interval, pktInterval, pktSpace uint32 sampSize, sampPop uint32 + prob float64 samplerID uint32 hasSampler bool @@ -228,7 +233,16 @@ func (a *acc) set(f fieldSpec, v []byte, nf9Scope bool) { case ieSysInitMs: a.sysInitMs = int64(wire.Uint(v)) case ieSamplingInterval, ieSamplerRandom: - a.interval = uint32(wire.Uint(v)) + // both may be present; a zero in one must not hide the other + if x := uint32(wire.Uint(v)); x > 0 { + a.interval = x + } + case ieSamplingProb: + if len(v) == 8 { + if p := math.Float64frombits(binary.BigEndian.Uint64(v)); p > 0 && p <= 1 { + a.prob = p + } + } case ieSamplingPktIntvl: a.pktInterval = uint32(wire.Uint(v)) case ieSamplingPktSpace: @@ -251,6 +265,8 @@ func (a *acc) rate() uint32 { return a.sampPop / a.sampSize case a.interval > 0: return a.interval + case a.prob > 0: + return uint32(math.Round(1 / a.prob)) } return 0 } @@ -319,6 +335,7 @@ func parseTemplateSet(s *session, b []byte, src flow.Source, options bool) []uin break } s.templates[id] = t + s.describe(id, t) ids = append(ids, id) // v9 options template records are padded to 4 bytes per set, // not per record; one record per set is the common case. @@ -357,6 +374,7 @@ func parseTemplateSet(s *session, b []byte, src flow.Source, options bool) []uin break } s.templates[id] = t + s.describe(id, t) ids = append(ids, id) } return ids @@ -426,6 +444,7 @@ func (d *Decoder) decodeData(s *session, t *template, body []byte, h msgHeader, default: s.def = rate } + d.byAddr[addr] = rate sampleChanged = true continue } @@ -459,7 +478,7 @@ func (d *Decoder) emit(s *session, a *acc, h msgHeader, addr netip.Addr, domain default: r.Bytes, r.Packets = a.totBytes, a.totPkts } - ref := samplerRef{samplerID: a.samplerID, hasSampler: a.hasSampler, inIf: r.InIf} + ref := samplerRef{samplerID: a.samplerID, hasSampler: a.hasSampler, inIf: r.InIf, outIf: r.OutIf} inRec := a.rate() s.stats.Records++ if r.Bytes > 0 || r.Packets > 0 { @@ -472,7 +491,7 @@ func (d *Decoder) emit(s *session, a *acc, h msgHeader, addr netip.Addr, domain rev.InIf, rev.OutIf = r.OutIf, r.InIf rev.SrcAS, rev.DstAS = r.DstAS, r.SrcAS rev.Bytes, rev.Packets = a.revBytes, a.revPkts - d.resolve(s, rev, samplerRef{samplerID: a.samplerID, hasSampler: a.hasSampler, inIf: rev.InIf}, inRec, now, out) + d.resolve(s, rev, samplerRef{samplerID: a.samplerID, hasSampler: a.hasSampler, inIf: rev.InIf, outIf: rev.OutIf}, inRec, now, out) } } @@ -503,3 +522,29 @@ func (d *Decoder) replayHeld(s *session, id uint16, addr netip.Addr, domain uint d.decodeData(s, t, h.body, h.hdr, addr, domain, now, out) } } + +// describe keeps a short description of a template for the sources page: +// its id, whether it is an options template, and its field ids (scope +// fields marked with s, enterprise fields as pen:id). +func (s *session) describe(id uint16, t *template) { + var b strings.Builder + if t.options { + fmt.Fprintf(&b, "options %d:", id) + } else { + fmt.Fprintf(&b, "%d:", id) + } + for _, f := range t.fields { + b.WriteByte(' ') + if f.scope { + b.WriteByte('s') + } + if f.ent != 0 { + fmt.Fprintf(&b, "%d:", f.ent) + } + fmt.Fprintf(&b, "%d", f.id) + } + if s.tmplDesc == nil { + s.tmplDesc = map[uint16]string{} + } + s.tmplDesc[id] = b.String() +} diff --git a/internal/decode/nf/sampling_test.go b/internal/decode/nf/sampling_test.go new file mode 100644 index 0000000..afe2c42 --- /dev/null +++ b/internal/decode/nf/sampling_test.go @@ -0,0 +1,103 @@ +package nf + +import ( + "encoding/binary" + "math" + "net/netip" + "testing" + + "github.com/githubflyideas/traffic66/internal/encode" +) + +// a minimal IPFIX data template: addresses, ports, interfaces, counts +var ipfixFlowTmpl = encode.Template{ID: 400, Fields: []encode.Field{ + {ID: ieSrcIPv4, Len: 4}, {ID: ieDstIPv4, Len: 4}, {ID: ieSrcPort, Len: 2}, {ID: ieDstPort, Len: 2}, + {ID: ieProtocol, Len: 1}, {ID: ieInIf, Len: 4}, {ID: ieOutIf, Len: 4}, {ID: ieOctetDelta, Len: 8}, {ID: iePacketDelta, Len: 8}, +}} + +func ipfixFlow(in, out uint32) encode.Values { + return encode.Values{encode.A(cli), encode.A(srv), encode.U(50522, 2), encode.U(443, 2), encode.U(6, 1), + encode.U(uint64(in), 4), encode.U(uint64(out), 4), encode.U(1500, 8), encode.U(1, 8)} +} + +func f64(v float64) []byte { + b := make([]byte, 8) + binary.BigEndian.PutUint64(b, math.Float64bits(v)) + return b +} + +// decodeOne sends options (if any) and one data record, returns its multiplier +func decodeOne(t *testing.T, d *Decoder, domain uint32, opt *encode.Template, optVals encode.Values, rec encode.Values) float64 { + t.Helper() + m := encode.NewIPFIX() + m.AddTemplate(ipfixFlowTmpl) + if opt != nil { + m.AddTemplate(*opt) + m.AddData(opt.ID, []encode.Values{optVals}) + } + m.AddData(400, []encode.Values{rec}) + res, err := d.Decode(m.IPFIX(uint32(base.Unix()), 0, domain), exp, base) + if err != nil { + t.Fatal(err) + } + if len(res.Records) != 1 { + return 0 // held, waiting for a rate + } + return res.Records[0].Mult +} + +func TestSamplingForms(t *testing.T) { + // samplingProbability (311) as a float + opt := encode.Template{ID: 401, Scope: []encode.Field{{ID: ieObsDomainID, Len: 4}}, Fields: []encode.Field{{ID: ieSamplingProb, Len: 8}}} + if m := decodeOne(t, NewDecoder(), 1, &opt, encode.Values{encode.U(1, 4), f64(0.001)}, ipfixFlow(1, 2)); m != 1000 { + t.Fatalf("probability: mult %v", m) + } + // rate per interface, configured on the egress interface of the flow + opt = encode.Template{ID: 402, Scope: []encode.Field{{ID: ieInIf, Len: 4}}, Fields: []encode.Field{{ID: ieSamplingInterval, Len: 4}}} + if m := decodeOne(t, NewDecoder(), 1, &opt, encode.Values{encode.U(5, 4), encode.U(1000, 4)}, ipfixFlow(7, 5)); m != 1000 { + t.Fatalf("egress interface: mult %v", m) + } + // a sampler declared with an id the records never name: the only rate + opt = encode.Template{ID: 403, Scope: []encode.Field{{ID: ieObsDomainID, Len: 4}}, + Fields: []encode.Field{{ID: ieSelectorID, Len: 4}, {ID: ieSamplingPktIntvl, Len: 4}, {ID: ieSamplingPktSpace, Len: 4}}} + if m := decodeOne(t, NewDecoder(), 1, &opt, encode.Values{encode.U(1, 4), encode.U(9, 4), encode.U(1, 4), encode.U(511, 4)}, ipfixFlow(1, 2)); m != 512 { + t.Fatalf("only sampler: mult %v", m) + } + // a zero samplingInterval must not hide samplerRandomInterval + opt = encode.Template{ID: 404, Scope: []encode.Field{{ID: ieObsDomainID, Len: 4}}, + Fields: []encode.Field{{ID: ieSamplerRandom, Len: 4}, {ID: ieSamplingInterval, Len: 4}}} + if m := decodeOne(t, NewDecoder(), 1, &opt, encode.Values{encode.U(1, 4), encode.U(100, 4), encode.U(0, 4)}, ipfixFlow(1, 2)); m != 100 { + t.Fatalf("zero interval: mult %v", m) + } +} + +func TestSamplingOtherDomainAndManual(t *testing.T) { + // the rate is declared in one observation domain, the flows come in another + d := NewDecoder() + opt := encode.Template{ID: 405, Scope: []encode.Field{{ID: ieObsDomainID, Len: 4}}, Fields: []encode.Field{{ID: ieSamplingInterval, Len: 4}}} + m := encode.NewIPFIX() + m.AddTemplate(opt) + m.AddData(405, []encode.Values{{encode.U(9, 4), encode.U(2000, 4)}}) + d.Decode(m.IPFIX(uint32(base.Unix()), 0, 9), exp, base) + if got := decodeOne(t, d, 10, nil, nil, ipfixFlow(1, 2)); got != 2000 { + t.Fatalf("other domain: mult %v", got) + } + + // nothing declared: held; a rate set by hand releases it + d = NewDecoder() + if got := decodeOne(t, d, 1, nil, nil, ipfixFlow(1, 2)); got != 0 { + t.Fatalf("expected held, mult %v", got) + } + res := d.SetManual(map[netip.Addr]uint32{exp: 1000}) + if len(res.Records) != 1 || res.Records[0].Mult != 1000 { + t.Fatalf("manual: %+v", res.Records) + } + st := d.Stats()[0] + if st.SamplingState != "manual" || st.Sampling != 1000 || len(st.TemplateInfo) != 1 { + t.Fatalf("stats %+v", st) + } + // a rate the device declares wins over the hand-set one + if got := decodeOne(t, d, 1, &opt, encode.Values{encode.U(1, 4), encode.U(500, 4)}, ipfixFlow(1, 2)); got != 500 { + t.Fatalf("declared over manual: mult %v", got) + } +} diff --git a/internal/enrich/enrich.go b/internal/enrich/enrich.go index 194307f..05f4667 100644 --- a/internal/enrich/enrich.go +++ b/internal/enrich/enrich.go @@ -60,6 +60,7 @@ type Inventory struct { ifaces map[string]Iface // "exporter/ifindex" hosts map[netip.Addr]string unsampled map[netip.Addr]bool + sampling map[netip.Addr]uint32 // sampling=N on device lines snmp []SNMPTarget // autoIfs are interface names learned from the devices (SNMP ifName); // names written in the inventory always win. @@ -113,6 +114,7 @@ func (inv *Inventory) Parse(text string) error { ifs := map[string]Iface{} hosts := map[netip.Addr]string{} uns := map[netip.Addr]bool{} + samp := map[netip.Addr]uint32{} var snmps []SNMPTarget for n, line := range strings.Split(text, "\n") { line = strings.TrimSpace(line) @@ -161,6 +163,14 @@ func (inv *Inventory) Parse(text string) error { uns[a] = true continue } + if v, ok := strings.CutPrefix(strings.ToLower(w), "sampling="); ok { + n, err := strconv.ParseUint(strings.TrimPrefix(v, "1:"), 10, 32) + if err != nil || n == 0 { + return bad("sampling= needs a number such as sampling=1000") + } + samp[a] = uint32(n) + continue + } words = append(words, w) } devs[a] = strings.Join(words, " ") @@ -222,6 +232,7 @@ func (inv *Inventory) Parse(text string) error { sort.SliceStable(nets, func(i, j int) bool { return nets[i].Prefix.Bits() > nets[j].Prefix.Bits() }) inv.mu.Lock() inv.networks, inv.devices, inv.ifaces, inv.hosts, inv.unsampled, inv.snmp, inv.text = nets, devs, ifs, hosts, uns, snmps, text + inv.sampling = samp inv.mu.Unlock() return nil } @@ -334,6 +345,18 @@ func (inv *Inventory) Unsampled() map[netip.Addr]bool { return out } +// Sampling is the hand-set sampling rate of devices (sampling=N), used +// where a device does not declare its own. +func (inv *Inventory) Sampling() map[netip.Addr]uint32 { + inv.mu.RLock() + defer inv.mu.RUnlock() + out := map[netip.Addr]uint32{} + for a, n := range inv.sampling { + out[a] = n + } + return out +} + // SegmentCountries maps network names to the country set for them. func (inv *Inventory) SegmentCountries() map[string]string { inv.mu.RLock() diff --git a/internal/web/static/app.js b/internal/web/static/app.js index 0a61375..aba7f8f 100644 --- a/internal/web/static/app.js +++ b/internal/web/static/app.js @@ -1297,15 +1297,15 @@ views.sources = async (el) => { ['IPtoASN', 'geo.f_iptoasn', 'PDDL 1.0', 'https://iptoasn.com']]; const freeRows = FREE.map(([n, k, lic, url]) => `${n}${esc(t(k))}${lic}`).join(''); const srcs = d.sources || []; - const issueText = (code, s) => t('issue.' + code, {n: nf(s.pending), p: nf(code === 'agent_drops' ? s.drop_pct : code === 'loss' ? (s.transit_pct ?? s.lost_pct) : s.lost_pct, 1) + '%', s: Math.round(Math.abs(s.clock_skew_ns) / 1e9) + ' s'}); + const issueText = (code, s) => t('issue.' + code, {a: s.exporter, n: nf(s.pending), p: nf(code === 'agent_drops' ? s.drop_pct : code === 'loss' ? (s.transit_pct ?? s.lost_pct) : s.lost_pct, 1) + '%', s: Math.round(Math.abs(s.clock_skew_ns) / 1e9) + ' s'}); const rows = srcs.map(s => { const kind = s.status; return `${s.name ? esc(s.name) + ' ' : ''}${esc(s.exporter)}${s.domain ? ' / ' + s.domain : ''} ${esc(s.proto)}${nf(s.rec_per_sec, 1)} - ${s.sampling ? esc(s.sampling) : (s.sampling_state === 'waiting' ? '?' : '1:1')}${s.effective ? ` (${esc(t('src.effective', {v: '1:' + nf(s.effective)}))})` : ''} + ${s.sampling ? esc(s.sampling) : (s.sampling_state === 'waiting' ? '?' : '1:1')}${s.sampling_state === 'manual' ? ` (${esc(t('src.manual'))})` : ''}${s.effective ? ` (${esc(t('src.effective', {v: '1:' + nf(s.effective)}))})` : ''} ${nf(s.lost_pct, 2)}%${s.last_seen ? ago(Date.parse(s.last_seen)) : ''} ${status(kind, t('st.' + kind))} - ${s.issues?.length ? `${s.issues.map(c => `
${esc(issueText(c, s))}
`).join('')}` : ''}`; + ${s.issues?.length ? `${s.issues.map(c => `
${esc(issueText(c, s))}
`).join('')}${['assumed1', 'waiting', 'manual'].includes(s.sampling_state) && s.template_info?.length ? `
${esc(t('src.templates'))} ${s.template_info.map(esc).join(' · ')}
` : ''}` : ''}`; }).join(''); const lis = (d.listeners || []).map(l => `UDP ${esc(l.addr)} · ${esc(l.proto)} · ${nf(l.packets)}`).join(' '); const caps = (d.captures || []).map(c => `${esc(c.iface)} · ${esc(c.method)} · ${nf(c.packets)}${c.error ? ' · ' + esc(c.error) : ''}`).join(' '); @@ -1316,6 +1316,7 @@ views.sources = async (el) => { ${panel('c12', t('src.title'), t('src.sub'), ` ${rows || ``}
${t('col.device')}${t('col.proto')}${t('col.rate')}${t('col.sampling')}${t('col.lost')}${t('col.last')}${t('col.status')}
${t('empty.first')}

${t('src.listeners')} ${lis || '—'} ${caps}

+ ${(d.stale || []).length ? `

${t('src.stale')} ${d.stale.map(x => `${esc(x.name || x.exporter)} / ${x.domain} · ${esc(x.proto)} · ${esc(ago(Date.parse(x.last_seen)))}`).join(' ')}

` : ''} ${snmp ? `

${t('src.snmp')} ${snmp}

` : ''}`)} ${panel('c12', t('geo.title'), t('geo.sub'), `${geoRows}
${t('geo.col_holds')}${t('geo.col_db')}${t('geo.col_built')}${t('geo.col_use')}
diff --git a/internal/web/static/i18n/ar.json b/internal/web/static/i18n/ar.json index 80d2977..808091a 100644 --- a/internal/web/static/i18n/ar.json +++ b/internal/web/static/i18n/ar.json @@ -197,7 +197,7 @@ "st.bad": "لا استقبال", "issue.silent": "لم يصل شيء منذ أكثر من دقيقتين. تحقق من وجهة التصدير على الجهاز ومن سماح الجدران النارية بـ UDP.", "issue.waiting_sampling": "وصلت القوالب لكن معدل أخذ العينات لم يصل بعد. هناك {n} سجلًا محجوزًا وسيُعاد حسابها عند وصوله، ولا تُحسب 1:1. تعيد الأجهزة إرساله عادةً خلال دقائق.", -"issue.assumed_unsampled": "لم يعلن هذا الجهاز معدل أخذ عينات قط، لذا تُحسب سجلاته 1:1. إن كان يأخذ عينات، فعّل تصدير خيارات أخذ العينات أو علّمه بـ unsampled في «الأسماء».", +"issue.assumed_unsampled": "لم يعلن هذا الجهاز معدل أخذ العينات، لذا تُحسب تدفقاته 1:1. إذا كان يأخذ عينات فاجعله يصدّر خيارات أداة العينات، أو اكتب المعدل في الإعدادات ← الأسماء، مثل device {a} الاسم sampling=1000. وإذا كان يصدّر كل الحزم فأضف unsampled إلى ذلك السطر.", "issue.loss": "فُقد {p} من حزم التصدير في الطريق (فجوات في الأرقام التسلسلية). المجاميع معوَّضة، أما تفاصيل كل مضيف فلا. إذا أظهر “netstat -su” على هذا الجهاز تزايد receive buffer errors فارفع net.core.rmem_max، وإلا فافحص مسار الشبكة.", "issue.no_template": "البيانات تصل لكن قالبها لم يصل بعد. حُجزت البيانات وستُفك فور وصول القالب.", "issue.clock_skew": "ساعة الجهاز منحرفة بمقدار {s}. صُحِّحت الأوقات إلى وقت الوصول.", @@ -497,5 +497,8 @@ "ov.dir_total": "الإجمالي", "ov.bw_all": "عرض النطاق (وارد + صادر)، حسب التطبيق", "ov.bw_in": "عرض النطاق الوارد، حسب التطبيق", -"ov.bw_out": "عرض النطاق الصادر، حسب التطبيق" +"ov.bw_out": "عرض النطاق الصادر، حسب التطبيق", +"src.manual": "مضبوط يدويًا", +"src.templates": "القوالب المستلمة (أرقام الحقول؛ s = النطاق):", +"src.stale": "متوقفة (الجهاز يرسل الآن بطريقة أخرى):" } diff --git a/internal/web/static/i18n/bn.json b/internal/web/static/i18n/bn.json index 55a5ce0..55bd2a6 100644 --- a/internal/web/static/i18n/bn.json +++ b/internal/web/static/i18n/bn.json @@ -197,7 +197,7 @@ "st.bad": "ডেটা আসছে না", "issue.silent": "২ মিনিটের বেশি কিছু আসেনি। ডিভাইসে এক্সপোর্টের গন্তব্য দেখুন এবং ফায়ারওয়াল UDP অনুমতি দেয় কি না যাচাই করুন।", "issue.waiting_sampling": "টেমপ্লেট এসেছে, কিন্তু স্যাম্পলিং হার এখনো আসেনি। {n}টি রেকর্ড আটকে রাখা হয়েছে; হার এলে সঠিক মাপে গোনা হবে, ১:১ নয়। ডিভাইস সাধারণত কয়েক মিনিটে আবার পাঠায়।", -"issue.assumed_unsampled": "এই ডিভাইস কখনো স্যাম্পলিং হার জানায়নি, তাই এর রেকর্ড ১:১ গোনা হচ্ছে। যদি এটি স্যাম্পলিং করে, স্যাম্পলার অপশন এক্সপোর্ট চালু করুন বা 'নাম'-এ unsampled চিহ্ন দিন।", +"issue.assumed_unsampled": "এই ডিভাইস তার sampling হার জানায়নি, তাই এর flow 1:1 ধরে গোনা হচ্ছে। এটি sampling করলে sampler options export করান, অথবা সেটিংস → নাম-এ হার লিখুন, যেমন device {a} নাম sampling=1000। এটি প্রতিটি প্যাকেট export করলে সেই লাইনে unsampled যোগ করুন।", "issue.loss": "রপ্তানি প্যাকেটের {p} পথে হারিয়েছে (sequence নম্বরে ফাঁক)। মোট পরিমাণ পূরণ করা হয়েছে, কিন্তু প্রতিটি host-এর বিবরণ ফেরানো যায় না। এই মেশিনে “netstat -su”-তে receive buffer errors বাড়লে net.core.rmem_max বাড়ান; না হলে নেটওয়ার্ক পথ দেখুন।", "issue.no_template": "ডেটা আসছে, কিন্তু টেমপ্লেট এখনো আসেনি। ডেটা রাখা হয়েছে, টেমপ্লেট এলেই পড়া হবে।", "issue.clock_skew": "ডিভাইসের ঘড়ি {s} এদিক-ওদিক। সময় পৌঁছানোর সময় অনুযায়ী ঠিক করা হয়েছে।", @@ -497,5 +497,8 @@ "ov.dir_total": "মোট", "ov.bw_all": "ব্যান্ডউইথ (আগত + বহির্গামী), অ্যাপ্লিকেশন অনুযায়ী", "ov.bw_in": "আগত ব্যান্ডউইথ, অ্যাপ্লিকেশন অনুযায়ী", -"ov.bw_out": "বহির্গামী ব্যান্ডউইথ, অ্যাপ্লিকেশন অনুযায়ী" +"ov.bw_out": "বহির্গামী ব্যান্ডউইথ, অ্যাপ্লিকেশন অনুযায়ী", +"src.manual": "হাতে ঠিক করা", +"src.templates": "পাওয়া টেমপ্লেট (field id; s = scope):", +"src.stale": "বন্ধ (ডিভাইস এখন অন্যভাবে পাঠাচ্ছে):" } diff --git a/internal/web/static/i18n/en.json b/internal/web/static/i18n/en.json index 47839cf..959acaa 100644 --- a/internal/web/static/i18n/en.json +++ b/internal/web/static/i18n/en.json @@ -197,7 +197,7 @@ "st.bad": "Not receiving", "issue.silent": "Nothing received for more than 2 minutes. Check the export destination on the device and that UDP is allowed through firewalls.", "issue.waiting_sampling": "Templates received but no sampling rate yet. {n} records are held and will be scaled when it arrives, not counted as 1:1. Devices usually resend it within a few minutes.", -"issue.assumed_unsampled": "This device never declared a sampling rate, so its records are counted 1:1. If it samples, make it export sampler options, or mark it 'unsampled' in Names.", +"issue.assumed_unsampled": "This device has not declared its sampling rate, so its flows count 1:1. If it samples, have it export its sampler options, or give the rate under Settings → Names, e.g. device {a} name sampling=1000. If it exports every packet, add unsampled to that line.", "issue.loss": "{p} of the export packets were lost on the way here (gaps in their sequence numbers). Totals are compensated; per-host detail cannot be. If “netstat -su” on this machine shows receive buffer errors rising, raise net.core.rmem_max; otherwise check the network path.", "issue.no_template": "Data is arriving but its template has not. It is held and decoded as soon as the template arrives.", "issue.clock_skew": "The device clock is off by {s}. Times were corrected to arrival time.", @@ -497,5 +497,8 @@ "ov.dir_total": "Total", "ov.bw_all": "Bandwidth (in + out), by application", "ov.bw_in": "Inbound bandwidth, by application", -"ov.bw_out": "Outbound bandwidth, by application" +"ov.bw_out": "Outbound bandwidth, by application", +"src.manual": "set by hand", +"src.templates": "Templates received (field ids; s = scope):", +"src.stale": "Stopped (the device now sends otherwise):" } diff --git a/internal/web/static/i18n/es.json b/internal/web/static/i18n/es.json index 32591c9..9afa843 100644 --- a/internal/web/static/i18n/es.json +++ b/internal/web/static/i18n/es.json @@ -197,7 +197,7 @@ "st.bad": "Sin recibir", "issue.silent": "No se ha recibido nada en más de 2 minutos. Revise el destino de exportación del dispositivo y que los cortafuegos permitan UDP.", "issue.waiting_sampling": "Se recibieron plantillas pero aún no la tasa de muestreo. Hay {n} registros en espera; se escalarán cuando llegue y no se contarán como 1:1. Los dispositivos suelen reenviarla en pocos minutos.", -"issue.assumed_unsampled": "Este dispositivo nunca declaró una tasa de muestreo, así que sus registros se cuentan 1:1. Si muestrea, configúrelo para exportar las opciones del muestreador o márquelo como 'unsampled' en Nombres.", +"issue.assumed_unsampled": "Este equipo no ha declarado su tasa de muestreo, así que sus flujos cuentan 1:1. Si muestrea, haga que exporte las opciones del muestreador o indique la tasa en Configuración → Nombres, p. ej. device {a} nombre sampling=1000. Si exporta todos los paquetes, añada unsampled a esa línea.", "issue.loss": "Se perdió un {p} de los paquetes exportados por el camino (huecos en los números de secuencia). Los totales están compensados; el detalle por host no. Si “netstat -su” en esta máquina muestra receive buffer errors en aumento, suba net.core.rmem_max; si no, revise la ruta de red.", "issue.no_template": "Llegan datos pero aún no su plantilla. Se guardan y se decodifican en cuanto llegue la plantilla.", "issue.clock_skew": "El reloj del dispositivo tiene un desfase de {s}. Las horas se corrigieron a la hora de llegada.", @@ -497,5 +497,8 @@ "ov.dir_total": "Total", "ov.bw_all": "Ancho de banda (entrada + salida), por aplicación", "ov.bw_in": "Ancho de banda entrante, por aplicación", -"ov.bw_out": "Ancho de banda saliente, por aplicación" +"ov.bw_out": "Ancho de banda saliente, por aplicación", +"src.manual": "fijado a mano", +"src.templates": "Plantillas recibidas (ids de campo; s = ámbito):", +"src.stale": "Detenidos (el equipo ahora envía de otra forma):" } diff --git a/internal/web/static/i18n/fr.json b/internal/web/static/i18n/fr.json index 4c43b39..2d5d8c1 100644 --- a/internal/web/static/i18n/fr.json +++ b/internal/web/static/i18n/fr.json @@ -197,7 +197,7 @@ "st.bad": "Rien reçu", "issue.silent": "Rien reçu depuis plus de 2 minutes. Vérifiez la destination d'export sur l'équipement et que les pare-feu laissent passer l'UDP.", "issue.waiting_sampling": "Modèles reçus mais pas encore le taux d'échantillonnage. {n} enregistrements sont en attente ; ils seront mis à l'échelle à son arrivée et ne sont pas comptés en 1:1. Les équipements le renvoient généralement en quelques minutes.", -"issue.assumed_unsampled": "Cet équipement n'a jamais déclaré de taux d'échantillonnage : ses enregistrements sont comptés en 1:1. S'il échantillonne, configurez l'export des options de l'échantillonneur ou marquez-le « unsampled » dans Noms.", +"issue.assumed_unsampled": "Cet équipement n'a pas déclaré son taux d'échantillonnage, ses flux comptent donc 1:1. S'il échantillonne, faites-lui exporter les options de l'échantillonneur, ou indiquez le taux dans Paramètres → Noms, par ex. device {a} nom sampling=1000. S'il exporte tous les paquets, ajoutez unsampled à cette ligne.", "issue.loss": "{p} des paquets exportés ont été perdus en route (trous dans les numéros de séquence). Les totaux sont compensés, pas le détail par hôte. Si « netstat -su » sur cette machine montre des receive buffer errors en hausse, augmentez net.core.rmem_max ; sinon, vérifiez le chemin réseau.", "issue.no_template": "Des données arrivent mais pas encore leur modèle. Elles sont conservées et décodées dès l'arrivée du modèle.", "issue.clock_skew": "L'horloge de l'équipement est décalée de {s}. Les heures ont été corrigées sur l'heure de réception.", @@ -497,5 +497,8 @@ "ov.dir_total": "Total", "ov.bw_all": "Bande passante (entrant + sortant), par application", "ov.bw_in": "Bande passante entrante, par application", -"ov.bw_out": "Bande passante sortante, par application" +"ov.bw_out": "Bande passante sortante, par application", +"src.manual": "fixé à la main", +"src.templates": "Modèles reçus (ids de champ ; s = portée) :", +"src.stale": "Arrêtés (l’équipement envoie désormais autrement) :" } diff --git a/internal/web/static/i18n/hi.json b/internal/web/static/i18n/hi.json index 74afa26..6c577dd 100644 --- a/internal/web/static/i18n/hi.json +++ b/internal/web/static/i18n/hi.json @@ -197,7 +197,7 @@ "st.bad": "डेटा नहीं आ रहा", "issue.silent": "2 मिनट से ज़्यादा समय से कुछ नहीं आया। डिवाइस पर एक्सपोर्ट का पता जाँचें और देखें कि फ़ायरवॉल UDP की अनुमति देता है।", "issue.waiting_sampling": "टेम्पलेट मिल गए, पर सैंपलिंग दर अभी नहीं मिली। {n} रिकॉर्ड रोके गए हैं; दर मिलते ही उन्हें सही पैमाने पर गिना जाएगा, 1:1 नहीं। डिवाइस आमतौर पर कुछ मिनटों में इसे फिर भेजते हैं।", -"issue.assumed_unsampled": "इस डिवाइस ने कभी सैंपलिंग दर नहीं बताई, इसलिए इसके रिकॉर्ड 1:1 गिने जा रहे हैं। अगर यह सैंपलिंग करता है, तो सैंपलर विकल्प एक्सपोर्ट करना चालू करें या 'नाम' में इसे unsampled चिह्नित करें।", +"issue.assumed_unsampled": "इस डिवाइस ने अपनी sampling दर नहीं बताई, इसलिए उसके flow 1:1 गिने जा रहे हैं। अगर यह sampling करता है तो इससे sampler options export करवाएँ, या सेटिंग्स → नाम में दर लिखें, जैसे device {a} नाम sampling=1000। अगर यह हर पैकेट export करता है तो उस पंक्ति में unsampled जोड़ें।", "issue.loss": "एक्सपोर्ट पैकेटों में से {p} रास्ते में खो गए (sequence नंबरों में अंतर)। कुल मात्रा की भरपाई कर दी गई है, पर हर host का विवरण वापस नहीं आ सकता। अगर इस मशीन पर “netstat -su” में receive buffer errors बढ़ रहे हैं तो net.core.rmem_max बढ़ाएँ; नहीं तो नेटवर्क पथ जाँचें।", "issue.no_template": "डेटा आ रहा है, पर उसका टेम्पलेट अभी नहीं आया। डेटा रोका गया है और टेम्पलेट आते ही पढ़ा जाएगा।", "issue.clock_skew": "डिवाइस की घड़ी {s} आगे-पीछे है। समय को आने के समय के अनुसार ठीक किया गया।", @@ -497,5 +497,8 @@ "ov.dir_total": "कुल", "ov.bw_all": "बैंडविड्थ (आने + जाने वाला), ऐप्लिकेशन के अनुसार", "ov.bw_in": "आने वाली बैंडविड्थ, ऐप्लिकेशन के अनुसार", -"ov.bw_out": "जाने वाली बैंडविड्थ, ऐप्लिकेशन के अनुसार" +"ov.bw_out": "जाने वाली बैंडविड्थ, ऐप्लिकेशन के अनुसार", +"src.manual": "हाथ से सेट", +"src.templates": "मिले टेम्पलेट (field id; s = scope):", +"src.stale": "बंद (डिवाइस अब दूसरे तरीके से भेज रहा है):" } diff --git a/internal/web/static/i18n/id.json b/internal/web/static/i18n/id.json index edcffb5..86965f6 100644 --- a/internal/web/static/i18n/id.json +++ b/internal/web/static/i18n/id.json @@ -197,7 +197,7 @@ "st.bad": "Tidak menerima", "issue.silent": "Tidak ada data lebih dari 2 menit. Periksa tujuan ekspor di perangkat dan apakah firewall mengizinkan UDP.", "issue.waiting_sampling": "Template sudah diterima, tetapi laju sampling belum. {n} catatan ditahan dan akan diskalakan saat laju itu tiba, bukan dihitung 1:1. Perangkat biasanya mengirim ulang dalam beberapa menit.", -"issue.assumed_unsampled": "Perangkat ini tidak pernah menyatakan laju sampling, jadi catatannya dihitung 1:1. Jika perangkat melakukan sampling, aktifkan ekspor opsi sampler atau tandai sebagai 'unsampled' di Nama.", +"issue.assumed_unsampled": "Perangkat ini belum menyatakan laju sampling-nya, jadi flow-nya dihitung 1:1. Jika ia melakukan sampling, minta ia mengekspor opsi sampler, atau tulis lajunya di Pengaturan → Nama, misalnya device {a} nama sampling=1000. Jika ia mengekspor setiap paket, tambahkan unsampled pada baris itu.", "issue.loss": "{p} paket ekspor hilang di jalan (ada celah nomor urut). Total sudah dikompensasi; rincian per host tidak. Jika “netstat -su” di mesin ini menunjukkan receive buffer errors naik, besarkan net.core.rmem_max; jika tidak, periksa jalur jaringan.", "issue.no_template": "Data masuk, tetapi template-nya belum. Data ditahan dan didekode begitu template tiba.", "issue.clock_skew": "Jam perangkat meleset {s}. Waktu sudah dikoreksi ke waktu kedatangan.", @@ -497,5 +497,8 @@ "ov.dir_total": "Total", "ov.bw_all": "Bandwidth (masuk + keluar), per aplikasi", "ov.bw_in": "Bandwidth masuk, per aplikasi", -"ov.bw_out": "Bandwidth keluar, per aplikasi" +"ov.bw_out": "Bandwidth keluar, per aplikasi", +"src.manual": "diatur manual", +"src.templates": "Template yang diterima (id field; s = scope):", +"src.stale": "Berhenti (perangkat kini mengirim dengan cara lain):" } diff --git a/internal/web/static/i18n/ja.json b/internal/web/static/i18n/ja.json index 87f060a..24c2c24 100644 --- a/internal/web/static/i18n/ja.json +++ b/internal/web/static/i18n/ja.json @@ -197,7 +197,7 @@ "st.bad": "未受信", "issue.silent": "2 分以上何も届いていません。機器のエクスポート先と、ファイアウォールで UDP が許可されているかを確認してください。", "issue.waiting_sampling": "テンプレートは届きましたが、サンプリングレートがまだです。{n} 件のフローを保留中で、レートが届いた時点で換算して取り込みます(1:1 では数えません)。通常は数分以内に再送されます。", -"issue.assumed_unsampled": "この機器はサンプリングレートを一度も通知していないため、1:1 で数えています。サンプリングしている場合は、サンプラーのオプションをエクスポートするよう設定するか、「名前」で unsampled と指定してください。", +"issue.assumed_unsampled": "この機器はサンプリングレートを宣言していないため、フローは 1:1 で数えています。サンプリングしているなら、サンプラーのオプションをエクスポートさせるか、「設定 → 名前」で device {a} 名前 sampling=1000 のように指定してください。全パケットをエクスポートしているなら、その行に unsampled を付けてください。", "issue.loss": "エクスポートパケットの {p} が途中で失われました(シーケンス番号の欠け)。合計は補正済みですが、ホストごとの明細は戻りません。このマシンの “netstat -su” で receive buffer errors が増えていれば net.core.rmem_max を上げてください。増えていなければ経路を確認してください。", "issue.no_template": "データは届いていますが、テンプレートがまだです。データは保留し、テンプレートが届き次第解読します。", "issue.clock_skew": "機器の時計が {s} ずれています。時刻は受信時刻に補正しました。", @@ -497,5 +497,8 @@ "ov.dir_total": "合計", "ov.bw_all": "帯域(受信 + 送信)、アプリ別", "ov.bw_in": "受信帯域、アプリ別", -"ov.bw_out": "送信帯域、アプリ別" +"ov.bw_out": "送信帯域、アプリ別", +"src.manual": "手動で指定", +"src.templates": "受信したテンプレート(フィールド番号、s はスコープ):", +"src.stale": "停止(同じ機器が別の形式で送信中):" } diff --git a/internal/web/static/i18n/ko.json b/internal/web/static/i18n/ko.json index 65f9181..a8c0a40 100644 --- a/internal/web/static/i18n/ko.json +++ b/internal/web/static/i18n/ko.json @@ -197,7 +197,7 @@ "st.bad": "수신 안 됨", "issue.silent": "2분 넘게 아무것도 받지 못했습니다. 장비의 export 대상 설정과 방화벽에서 UDP가 허용되는지 확인하세요.", "issue.waiting_sampling": "템플릿은 받았지만 샘플링 비율이 아직 없습니다. 레코드 {n}건을 보류 중이며, 비율이 도착하면 1:1이 아니라 올바른 배율로 집계합니다. 장비는 보통 몇 분 안에 다시 보냅니다.", -"issue.assumed_unsampled": "이 장비는 샘플링 비율을 한 번도 알리지 않아 레코드를 1:1로 집계하고 있습니다. 샘플링을 한다면 sampler options를 export하도록 설정하거나, 이름에서 unsampled로 표시하세요.", +"issue.assumed_unsampled": "이 장비는 샘플링 비율을 알려 주지 않아 플로를 1:1로 계산합니다. 샘플링을 한다면 샘플러 옵션을 내보내게 하거나, 설정 → 이름에서 device {a} 이름 sampling=1000 처럼 지정하세요. 모든 패킷을 내보낸다면 그 줄에 unsampled를 붙이세요.", "issue.loss": "내보낸 패킷의 {p}가 오는 길에 유실되었습니다(시퀀스 번호 누락). 합계는 보정했지만 호스트별 상세는 되찾을 수 없습니다. 이 서버의 “netstat -su”에서 receive buffer errors가 늘고 있으면 net.core.rmem_max를 올리고, 아니면 네트워크 경로를 확인하세요.", "issue.no_template": "데이터는 오지만 템플릿이 아직 오지 않았습니다. 보류해 두었다가 템플릿이 도착하는 즉시 해석합니다.", "issue.clock_skew": "장비 시계가 {s} 어긋나 있습니다. 시간을 수신 시각으로 보정했습니다.", @@ -497,5 +497,8 @@ "ov.dir_total": "합계", "ov.bw_all": "대역폭(수신 + 송신), 애플리케이션별", "ov.bw_in": "수신 대역폭, 애플리케이션별", -"ov.bw_out": "송신 대역폭, 애플리케이션별" +"ov.bw_out": "송신 대역폭, 애플리케이션별", +"src.manual": "수동 지정", +"src.templates": "받은 템플릿(필드 번호, s는 스코프):", +"src.stale": "중지됨(같은 장비가 지금은 다른 방식으로 보냄):" } diff --git a/internal/web/static/i18n/pt.json b/internal/web/static/i18n/pt.json index 473bb47..4f9950e 100644 --- a/internal/web/static/i18n/pt.json +++ b/internal/web/static/i18n/pt.json @@ -197,7 +197,7 @@ "st.bad": "Sem recebimento", "issue.silent": "Nada recebido há mais de 2 minutos. Confira o destino de exportação no equipamento e se os firewalls liberam UDP.", "issue.waiting_sampling": "Os modelos chegaram, mas a taxa de amostragem ainda não. {n} registros estão retidos e serão escalados quando ela chegar, sem contar como 1:1. Os equipamentos costumam reenviá-la em poucos minutos.", -"issue.assumed_unsampled": "Este equipamento nunca declarou uma taxa de amostragem, então seus registros contam como 1:1. Se ele amostra, configure a exportação das opções do amostrador ou marque-o como 'unsampled' em Nomes.", +"issue.assumed_unsampled": "Este equipamento não declarou sua taxa de amostragem, então seus fluxos contam 1:1. Se ele amostra, faça-o exportar as opções do amostrador ou informe a taxa em Configurações → Nomes, ex. device {a} nome sampling=1000. Se exporta todos os pacotes, adicione unsampled a essa linha.", "issue.loss": "{p} dos pacotes exportados se perderam no caminho (lacunas nos números de sequência). Os totais são compensados; o detalhe por host, não. Se “netstat -su” nesta máquina mostrar receive buffer errors aumentando, aumente net.core.rmem_max; senão, verifique o caminho de rede.", "issue.no_template": "Os dados estão chegando, mas o modelo ainda não. Eles ficam retidos e são decodificados assim que o modelo chegar.", "issue.clock_skew": "O relógio do equipamento está {s} fora. Os horários foram corrigidos para a hora de chegada.", @@ -497,5 +497,8 @@ "ov.dir_total": "Total", "ov.bw_all": "Largura de banda (entrada + saída), por aplicação", "ov.bw_in": "Largura de banda de entrada, por aplicação", -"ov.bw_out": "Largura de banda de saída, por aplicação" +"ov.bw_out": "Largura de banda de saída, por aplicação", +"src.manual": "definido à mão", +"src.templates": "Modelos recebidos (ids de campo; s = escopo):", +"src.stale": "Parados (o equipamento agora envia de outra forma):" } diff --git a/internal/web/static/i18n/ru.json b/internal/web/static/i18n/ru.json index 866b745..3518596 100644 --- a/internal/web/static/i18n/ru.json +++ b/internal/web/static/i18n/ru.json @@ -197,7 +197,7 @@ "st.bad": "Нет приёма", "issue.silent": "Более 2 минут ничего не приходит. Проверьте адрес экспорта на устройстве и пропускают ли межсетевые экраны UDP.", "issue.waiting_sampling": "Шаблоны получены, а частота сэмплирования ещё нет. {n} записей отложено; они будут пересчитаны, когда она придёт, и не считаются как 1:1. Обычно устройства повторяют её в течение нескольких минут.", -"issue.assumed_unsampled": "Это устройство ни разу не сообщило частоту сэмплирования, поэтому его записи считаются 1:1. Если оно сэмплирует, включите экспорт параметров сэмплера или отметьте его как unsampled в «Названиях».", +"issue.assumed_unsampled": "Устройство не сообщило частоту сэмплирования, поэтому его потоки считаются 1:1. Если оно сэмплирует, включите экспорт опций сэмплера или укажите частоту в Настройки → Имена, например device {a} имя sampling=1000. Если оно экспортирует каждый пакет, добавьте в эту строку unsampled.", "issue.loss": "{p} экспортных пакетов потеряно по пути (пропуски в порядковых номерах). Итоги скомпенсированы, детализация по хостам — нет. Если в «netstat -su» на этой машине растут receive buffer errors, увеличьте net.core.rmem_max; иначе проверьте сетевой путь.", "issue.no_template": "Данные приходят, а шаблона ещё нет. Они отложены и будут разобраны, как только придёт шаблон.", "issue.clock_skew": "Часы устройства расходятся на {s}. Время исправлено на время приёма.", @@ -497,5 +497,8 @@ "ov.dir_total": "Всего", "ov.bw_all": "Полоса (вход + выход), по приложениям", "ov.bw_in": "Входящая полоса, по приложениям", -"ov.bw_out": "Исходящая полоса, по приложениям" +"ov.bw_out": "Исходящая полоса, по приложениям", +"src.manual": "задано вручную", +"src.templates": "Полученные шаблоны (номера полей; s — область):", +"src.stale": "Остановлены (устройство теперь шлёт иначе):" } diff --git a/internal/web/static/i18n/ur.json b/internal/web/static/i18n/ur.json index 2be61ea..760ab48 100644 --- a/internal/web/static/i18n/ur.json +++ b/internal/web/static/i18n/ur.json @@ -197,7 +197,7 @@ "st.bad": "وصول نہیں ہو رہا", "issue.silent": "2 منٹ سے زیادہ کچھ وصول نہیں ہوا۔ آلے پر ایکسپورٹ کی منزل چیک کریں اور یقینی بنائیں کہ فائر وال UDP کی اجازت دیتی ہے۔", "issue.waiting_sampling": "ٹیمپلیٹ مل گئے لیکن سیمپلنگ کی شرح ابھی نہیں ملی۔ {n} ریکارڈ روکے گئے ہیں اور شرح ملنے پر درست پیمانے پر گنے جائیں گے، 1:1 نہیں۔ آلات عموماً چند منٹ میں دوبارہ بھیج دیتے ہیں۔", -"issue.assumed_unsampled": "اس آلے نے کبھی سیمپلنگ کی شرح نہیں بتائی، اس لیے ریکارڈ 1:1 گنے جا رہے ہیں۔ اگر یہ سیمپلنگ کرتا ہے تو سیمپلر آپشن ایکسپورٹ کروائیں، یا نام میں اسے unsampled لکھیں۔", +"issue.assumed_unsampled": "اس آلے نے اپنی sampling شرح نہیں بتائی، اس لیے اس کے فلو 1:1 گنے جا رہے ہیں۔ اگر یہ sampling کرتا ہے تو اس سے sampler options export کروائیں، یا ترتیبات ← نام میں شرح لکھیں، مثلاً device {a} نام sampling=1000۔ اگر یہ ہر پیکٹ export کرتا ہے تو اس سطر میں unsampled شامل کریں۔", "issue.loss": "ایکسپورٹ پیکٹوں میں سے {p} راستے میں کھو گئے (sequence نمبروں میں خلا)۔ کل مقدار کی تلافی کر دی گئی ہے، مگر ہر host کی تفصیل واپس نہیں آ سکتی۔ اگر اس مشین پر “netstat -su” میں receive buffer errors بڑھ رہے ہیں تو net.core.rmem_max بڑھائیں، ورنہ نیٹ ورک کا راستہ دیکھیں۔", "issue.no_template": "ڈیٹا آ رہا ہے لیکن اس کا ٹیمپلیٹ نہیں آیا۔ ڈیٹا روکا گیا ہے اور ٹیمپلیٹ آتے ہی ڈیکوڈ ہو گا۔", "issue.clock_skew": "آلے کی گھڑی {s} آگے پیچھے ہے۔ اوقات کو وصولی کے وقت پر درست کیا گیا۔", @@ -497,5 +497,8 @@ "ov.dir_total": "کل", "ov.bw_all": "بینڈوڈتھ (آنے + جانے والی)، ایپلیکیشن کے لحاظ سے", "ov.bw_in": "آنے والی بینڈوڈتھ، ایپلیکیشن کے لحاظ سے", -"ov.bw_out": "جانے والی بینڈوڈتھ، ایپلیکیشن کے لحاظ سے" +"ov.bw_out": "جانے والی بینڈوڈتھ، ایپلیکیشن کے لحاظ سے", +"src.manual": "ہاتھ سے مقرر", +"src.templates": "موصول ٹیمپلیٹ (field id؛ s = scope):", +"src.stale": "بند (آلہ اب دوسرے طریقے سے بھیج رہا ہے):" } diff --git a/internal/web/static/i18n/zh.json b/internal/web/static/i18n/zh.json index 6485747..64de0d8 100644 --- a/internal/web/static/i18n/zh.json +++ b/internal/web/static/i18n/zh.json @@ -197,7 +197,7 @@ "st.bad": "没有收到", "issue.silent": "超过 2 分钟没有收到数据。检查设备上的导出地址,以及防火墙有没有放行 UDP。", "issue.waiting_sampling": "模板收到了,但采样率还没收到。{n} 条流已暂存,收到采样率后自动换算入库,不会按 1:1 算。设备一般几分钟内会重发。", -"issue.assumed_unsampled": "这台设备一直没有声明采样率,它的流按 1:1 计算。如果它开了采样,请让它导出采样器选项,或者在「名称」里把它标成 unsampled。", +"issue.assumed_unsampled": "这台设备一直没有声明采样率,它的流按 1:1 计算。如果它开了采样,请让它导出采样器选项,或者在「设定 → 名称」里手动写上,例如 device {a} 名称 sampling=1000。如果它不采样,在那一行加 unsampled。", "issue.loss": "导出包在路上丢了 {p}(序号有缺口)。总量已经补偿,但每台主机的明细无法找回。如果本机 “netstat -su” 里的 receive buffer errors 在增长,请调大 net.core.rmem_max;否则请检查网络链路。", "issue.no_template": "数据在进来,但模板还没到。数据已暂存,模板一到就解码。", "issue.clock_skew": "设备时钟偏差 {s},时间已按收到的时刻校正。", @@ -497,5 +497,8 @@ "ov.dir_total": "合计", "ov.bw_all": "带宽(入 + 出),按应用", "ov.bw_in": "入站带宽,按应用", -"ov.bw_out": "出站带宽,按应用" +"ov.bw_out": "出站带宽,按应用", +"src.manual": "手动指定", +"src.templates": "收到的模板(字段编号,s 表示作用域):", +"src.stale": "已停用(同一设备现在用别的方式在发):" } diff --git a/inventory.txt.example b/inventory.txt.example index cb013f3..9a08ae3 100644 --- a/inventory.txt.example +++ b/inventory.txt.example @@ -7,7 +7,8 @@ net 192.168.0.0/16 Office country=JP # country=XX (two letters) places a network on the world map: lines are drawn # from that country to the countries it talks to # -# device [unsampled] +# device [unsampled] [sampling=N] +# (sampling=N: the device samples 1:N but does not declare it in its export) device 192.0.2.1 Core router # # iface [speed=]