diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a6b3ec..62224e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,32 @@ The release notes on GitHub are taken from this file: the section whose heading is the version number. +## 1.5.2 + +Fixes +- Client and server were swapped for half of many TCP connections: exported + records (NetFlow, IPFIX, local capture, pcap files) carry the flags of all + their packets together, so a client's SYN followed by ACKs looked like a + server's SYN+ACK and its upload was counted the wrong way round. The flags + now decide only for single packets (sFlow samples, lone SYNs); local + capture and pcap files use the first packet of each connection; other + records go by the ports. Data stored before stays as it is; pcap files are + right once imported again. + +Offline pcap analysis +- One file at a time, each in a database of its own: **Analyse** on a + file's row shows that file on every page; the bar at the top switches to + another. Deleting a file no longer re-imports the others. + +Interfaces +- Interface check explains that local capture has nothing to compare (no + device interfaces or counters), and the interface picker is hidden when + there are no interfaces. + +Docs +- README: the Windows commands for local capture (`traffic66.exe interfaces`, + `traffic66.exe -capture Wi-Fi`). + ## 1.5.1 - The web UI's scripts and styles are no longer kept by the browser across diff --git a/README.md b/README.md index 320e04b..3ba9ce1 100644 --- a/README.md +++ b/README.md @@ -324,8 +324,10 @@ sampling. **Offline pcap analysis** shows packet captures (pcap, pcapng) with the same pages, apart from the live data: `traffic66 a.pcap b.pcapng` starts on 127.0.0.1 and opens the browser (up to 3 files, 3 GB; Ctrl+C deletes the -imported data), or upload up to 3 files of 50 MB on that page. It works on -flows, not packet contents. +imported data), or upload up to 3 files of 50 MB on that page. One file is +analysed at a time, each in a database of its own: **Analyse** on a file's +row shows it on every page, and the bar at the top switches to another. It +works on flows, not packet contents. ![Offline analysis: capture files with their packets, flows and time](docs/images/sandbox.png) @@ -340,9 +342,17 @@ in a browser, q quit; `-lang` picks the language. **Local capture** builds flows from a local interface, best a port connected to a switch's mirror port: `traffic66 interfaces` lists them, -`-capture eth1` (or a Windows name or number) captures. Linux needs root or -`setcap cap_net_raw,cap_net_admin+ep`, macOS root, Windows -[Npcap](https://npcap.com). Captured flows come from the device `127.0.0.1`. +`-capture eth1` captures. On Windows: + +``` +traffic66.exe interfaces # list the network cards: name, number, address +traffic66.exe -capture Wi-Fi # capture on the wireless card (or by number: -capture 2) +``` + +Linux needs root or `setcap cap_net_raw,cap_net_admin+ep`, macOS root, +Windows [Npcap](https://npcap.com). Captured flows come from the device +`127.0.0.1`. Local capture has no device interfaces or counters, so +**Interface check** has nothing to compare for it. ## 10. Options and data diff --git a/docs/README.ar.md b/docs/README.ar.md index 816259a..63aaed9 100644 --- a/docs/README.ar.md +++ b/docs/README.ar.md @@ -343,7 +343,9 @@ NetFlow/IPFIX بقدر تأخر الأجهزة في التصدير (دقيقتا يعرض **تحليل pcap دون اتصال** ملفات التقاط الحزم (pcap وpcapng) بالصفحات نفسها، بمعزل عن البيانات الحية: يبدأ `traffic66 a.pcap b.pcapng` على 127.0.0.1 ويفتح المتصفح (حتى 3 ملفات، 3 GB؛ يحذف Ctrl+C البيانات المستوردة)، أو ارفع حتى 3 -ملفات بحجم 50 MB في تلك الصفحة. يعمل على التدفقات، لا على محتوى الحزم. +ملفات بحجم 50 MB في تلك الصفحة. يُحلَّل ملف واحد في كل مرة، ولكل ملف قاعدة +بيانات خاصة به: يعرض **تحليل** في صف الملف بياناته في كل الصفحات، ويبدّل +الشريط في الأعلى إلى ملف آخر. يعمل على التدفقات، لا على محتوى الحزم. ![التحليل دون اتصال: ملفات الالتقاط مع حزمها وتدفقاتها ووقتها](images/sandbox.png) @@ -357,10 +359,18 @@ NetFlow/IPFIX بقدر تأخر الأجهزة في التصدير (دقيقتا ![الواجهة الطرفية: محادثات أعلى 66](images/tui-topn.png) **الالتقاط المحلي** يبني التدفقات من واجهة محلية، ويُفضَّل منفذ موصول بمنفذ -المرآة في مبدّل: يسردها `traffic66 interfaces`، ويلتقط `-capture eth1` (أو اسم -أو رقم في Windows). يحتاج Linux إلى root أو -`setcap cap_net_raw,cap_net_admin+ep`، وmacOS إلى root، وWindows إلى -[Npcap](https://npcap.com). تأتي التدفقات الملتقطة من الجهاز `127.0.0.1`. +المرآة في مبدّل: يسردها `traffic66 interfaces`، ويلتقط `-capture eth1`. على +Windows: + +``` +traffic66.exe interfaces # list the network cards: name, number, address +traffic66.exe -capture Wi-Fi # capture on the wireless card (or by number: -capture 2) +``` + +يحتاج Linux إلى root أو `setcap cap_net_raw,cap_net_admin+ep`، وmacOS إلى +root، وWindows إلى [Npcap](https://npcap.com). تأتي التدفقات الملتقطة من +الجهاز `127.0.0.1`. لا واجهات ولا عدّادات جهاز للالتقاط المحلي، لذا ليس لدى +**مطابقة الواجهات** ما تقارنه فيه. diff --git a/docs/README.bn.md b/docs/README.bn.md index 1290751..148b2e7 100644 --- a/docs/README.bn.md +++ b/docs/README.bn.md @@ -351,8 +351,10 @@ export করে ততটা (সর্বোচ্চ 2 মিনিট)। 6 **অফলাইন pcap বিশ্লেষণ** প্যাকেট ক্যাপচার (pcap, pcapng) একই পেজে দেখায়, লাইভ ডেটা থেকে আলাদা রেখে: `traffic66 a.pcap b.pcapng` 127.0.0.1-এ চালু হয় এবং browser খোলে (সর্বোচ্চ 3টি ফাইল, 3 GB; Ctrl+C ইমপোর্ট করা ডেটা মুছে দেয়), -অথবা ওই পেজে 50 MB পর্যন্ত 3টি ফাইল আপলোড করুন। এটি flow নিয়ে কাজ করে, -প্যাকেটের বিষয়বস্তু নিয়ে নয়। +অথবা ওই পেজে 50 MB পর্যন্ত 3টি ফাইল আপলোড করুন। একবারে একটি ফাইল বিশ্লেষণ +করা হয়, প্রতিটি নিজস্ব আলাদা ডেটাবেসে: কোনো ফাইলের সারিতে **বিশ্লেষণ** +সেটিকে প্রতিটি পেজে দেখায়, আর ওপরের বার অন্য ফাইলে বদলে দেয়। এটি flow নিয়ে +কাজ করে, প্যাকেটের বিষয়বস্তু নিয়ে নয়। ![অফলাইন বিশ্লেষণ: ক্যাপচার ফাইল, তাদের প্যাকেট, ফ্লো ও সময়](images/sandbox.png) @@ -367,9 +369,17 @@ browser-এ খুলুন, q বেরিয়ে যান; `-lang` ভা **Local capture** একটি local interface থেকে flow তৈরি করে, সবচেয়ে ভালো হয় switch-এর mirror port-এ যুক্ত একটি port: `traffic66 interfaces` সেগুলোর তালিকা -দেয়, `-capture eth1` (অথবা Windows-এর নাম বা নম্বর) capture করে। Linux-এ root -বা `setcap cap_net_raw,cap_net_admin+ep` লাগে, macOS-এ root, Windows-এ -[Npcap](https://npcap.com)। capture করা flow ডিভাইস `127.0.0.1` থেকে আসে। +দেয়, `-capture eth1` capture করে। Windows-এ: + +``` +traffic66.exe interfaces # list the network cards: name, number, address +traffic66.exe -capture Wi-Fi # capture on the wireless card (or by number: -capture 2) +``` + +Linux-এ root বা `setcap cap_net_raw,cap_net_admin+ep` লাগে, macOS-এ root, +Windows-এ [Npcap](https://npcap.com)। capture করা flow ডিভাইস `127.0.0.1` +থেকে আসে। Local capture-এ ডিভাইসের interface বা counter নেই, তাই +**ইন্টারফেস মিলানো**-তে এর জন্য তুলনা করার কিছু নেই। diff --git a/docs/README.es.md b/docs/README.es.md index ca00f54..be1e236 100644 --- a/docs/README.es.md +++ b/docs/README.es.md @@ -360,7 +360,10 @@ los escaneos muy pequeños pueden ocultarse tras el muestreo. las mismas páginas, aparte de los datos en vivo: `traffic66 a.pcap b.pcapng` arranca en 127.0.0.1 y abre el navegador (hasta 3 archivos, 3 GB; Ctrl+C borra los datos importados), o suba en esa página hasta 3 archivos de 50 MB. -Trabaja con flujos, no con el contenido de los paquetes. +Se analiza un archivo a la vez, cada uno en su propia base de datos: +**Analizar** en la fila de un archivo lo muestra en todas las páginas, y la +barra superior cambia a otro. Trabaja con flujos, no con el contenido de los +paquetes. ![Análisis offline: archivos de captura con sus paquetes, flujos y tiempo](images/sandbox.png) @@ -375,10 +378,18 @@ tiempo, w abrir en un navegador, q salir; `-lang` elige el idioma. **Captura local** construye flujos desde una interfaz local, idealmente un puerto conectado al puerto espejo de un switch: `traffic66 interfaces` las -lista y `-capture eth1` (o un nombre o número de Windows) captura. Linux -necesita root o `setcap cap_net_raw,cap_net_admin+ep`, macOS root, Windows -[Npcap](https://npcap.com). Los flujos capturados provienen del equipo -`127.0.0.1`. +lista y `-capture eth1` captura. En Windows: + +``` +traffic66.exe interfaces # list the network cards: name, number, address +traffic66.exe -capture Wi-Fi # capture on the wireless card (or by number: -capture 2) +``` + +Linux necesita root o `setcap cap_net_raw,cap_net_admin+ep`, macOS root, +Windows [Npcap](https://npcap.com). Los flujos capturados provienen del +equipo `127.0.0.1`. La captura local no tiene interfaces ni contadores del +equipo, así que **Verificación de interfaces** no tiene nada que comparar +para ella. diff --git a/docs/README.fr.md b/docs/README.fr.md index f5a25ac..b59bcdd 100644 --- a/docs/README.fr.md +++ b/docs/README.fr.md @@ -369,8 +369,11 @@ l'échantillonnage. pcapng) avec les mêmes pages, à part des données en direct : `traffic66 a.pcap b.pcapng` démarre sur 127.0.0.1 et ouvre le navigateur (jusqu'à 3 fichiers, 3 Go ; Ctrl+C supprime les données importées), ou -importez sur cette page jusqu'à 3 fichiers de 50 Mo. L'analyse porte sur -les flux, pas sur le contenu des paquets. +importez sur cette page jusqu'à 3 fichiers de 50 Mo. Un seul fichier est +analysé à la fois, chacun dans sa propre base de données : **Analyser** sur +la ligne d'un fichier l'affiche sur toutes les pages, et la barre du haut +passe à un autre. L'analyse porte sur les flux, pas sur le contenu des +paquets. ![Analyse hors ligne : fichiers de capture avec leurs paquets, flux et période](images/sandbox.png) @@ -386,10 +389,18 @@ la langue. **Capture locale** construit des flux à partir d'une interface locale, idéalement un port relié au port miroir d'un switch : `traffic66 interfaces` -les liste, `-capture eth1` (ou un nom ou numéro Windows) capture. Linux -demande root ou `setcap cap_net_raw,cap_net_admin+ep`, macOS root, Windows -[Npcap](https://npcap.com). Les flux capturés proviennent de l'équipement -`127.0.0.1`. +les liste, `-capture eth1` capture. Sous Windows : + +``` +traffic66.exe interfaces # list the network cards: name, number, address +traffic66.exe -capture Wi-Fi # capture on the wireless card (or by number: -capture 2) +``` + +Linux demande root ou `setcap cap_net_raw,cap_net_admin+ep`, macOS root, +Windows [Npcap](https://npcap.com). Les flux capturés proviennent de +l'équipement `127.0.0.1`. La capture locale n'a ni interfaces ni compteurs +d'équipement, le **Contrôle des interfaces** n'a donc rien à comparer pour +elle. diff --git a/docs/README.hi.md b/docs/README.hi.md index 4e2f361..231c07e 100644 --- a/docs/README.hi.md +++ b/docs/README.hi.md @@ -352,8 +352,10 @@ NetFlow/IPFIX के साथ उतनी देर तक जितनी द **ऑफ़लाइन pcap विश्लेषण** पैकेट कैप्चर (pcap, pcapng) को उन्हीं पेजों पर दिखाता है, लाइव डेटा से अलग: `traffic66 a.pcap b.pcapng` 127.0.0.1 पर शुरू होकर browser खोलता है (अधिकतम 3 फ़ाइलें, 3 GB; Ctrl+C आयात किया डेटा मिटा देता -है), या उस पेज पर 50 MB तक की अधिकतम 3 फ़ाइलें अपलोड करें। यह flows पर काम -करता है, पैकेट की सामग्री पर नहीं। +है), या उस पेज पर 50 MB तक की अधिकतम 3 फ़ाइलें अपलोड करें। एक समय में एक +फ़ाइल का विश्लेषण होता है, हर फ़ाइल अपने अलग database में: किसी फ़ाइल की +पंक्ति पर **विश्लेषण करें** उसे हर पेज पर दिखाता है, और ऊपर की पट्टी दूसरी +फ़ाइल पर बदल देती है। यह flows पर काम करता है, पैकेट की सामग्री पर नहीं। ![ऑफ़लाइन विश्लेषण: कैप्चर फ़ाइलें, उनके पैकेट, फ़्लो और समय](images/sandbox.png) @@ -368,9 +370,17 @@ browser में खोलें, q बाहर निकलें; `-lang` **Local capture** किसी local interface से flows बनाता है, सबसे अच्छा किसी switch के mirror port से जुड़ा port: `traffic66 interfaces` उनकी सूची देता है, -`-capture eth1` (या Windows का नाम या नंबर) capture करता है। Linux को root या -`setcap cap_net_raw,cap_net_admin+ep` चाहिए, macOS को root, Windows को -[Npcap](https://npcap.com)। Capture किए गए flows डिवाइस `127.0.0.1` से आते हैं। +`-capture eth1` capture करता है। Windows पर: + +``` +traffic66.exe interfaces # list the network cards: name, number, address +traffic66.exe -capture Wi-Fi # capture on the wireless card (or by number: -capture 2) +``` + +Linux को root या `setcap cap_net_raw,cap_net_admin+ep` चाहिए, macOS को root, +Windows को [Npcap](https://npcap.com)। Capture किए गए flows डिवाइस `127.0.0.1` +से आते हैं। Local capture में डिवाइस के interfaces या counters नहीं होते, +इसलिए **इंटरफ़ेस मिलान** के पास इसके लिए तुलना करने को कुछ नहीं है। diff --git a/docs/README.id.md b/docs/README.id.md index 65c11fb..d48c996 100644 --- a/docs/README.id.md +++ b/docs/README.id.md @@ -357,7 +357,10 @@ kecil bisa tersembunyi di balik sampling. halaman yang sama, terpisah dari data langsung: `traffic66 a.pcap b.pcapng` berjalan di 127.0.0.1 dan membuka browser (maksimal 3 file, 3 GB; Ctrl+C menghapus data yang diimpor), atau unggah maksimal 3 file berukuran 50 MB di -halaman itu. Analisis bekerja pada flow, bukan isi paket. +halaman itu. Satu file dianalisis pada satu waktu, masing-masing dalam +database tersendiri: **Analisis** pada baris sebuah file menampilkannya di +semua halaman, dan bilah di bagian atas beralih ke file lain. Analisis +bekerja pada flow, bukan isi paket. ![Analisis offline: file tangkapan beserta paket, flow, dan waktunya](images/sandbox.png) @@ -372,10 +375,18 @@ rentang waktu, w buka di browser, q keluar; `-lang` memilih bahasa. **Capture lokal** membuat flow dari interface lokal, paling baik port yang terhubung ke port mirror sebuah switch: `traffic66 interfaces` menampilkan -daftarnya, `-capture eth1` (atau nama atau nomor di Windows) melakukan -capture. Linux butuh root atau `setcap cap_net_raw,cap_net_admin+ep`, macOS -butuh root, Windows butuh [Npcap](https://npcap.com). Flow hasil capture -berasal dari perangkat `127.0.0.1`. +daftarnya, `-capture eth1` melakukan capture. Di Windows: + +``` +traffic66.exe interfaces # list the network cards: name, number, address +traffic66.exe -capture Wi-Fi # capture on the wireless card (or by number: -capture 2) +``` + +Linux butuh root atau `setcap cap_net_raw,cap_net_admin+ep`, macOS butuh +root, Windows butuh [Npcap](https://npcap.com). Flow hasil capture berasal +dari perangkat `127.0.0.1`. Capture lokal tidak punya interface atau counter +perangkat, jadi **Pencocokan antarmuka** tidak punya apa pun untuk +dibandingkan. diff --git a/docs/README.ja.md b/docs/README.ja.md index dc4d5e9..60b764e 100644 --- a/docs/README.ja.md +++ b/docs/README.ja.md @@ -272,7 +272,7 @@ snmp 192.0.2.9 s3cret 10.99.0.9:161 # other management address ## 9. オフライン pcap、ターミナル UI、ローカルキャプチャ -**オフライン pcap 分析** は、パケットキャプチャ(pcap、pcapng)をライブデータとは分けて、同じページで表示します。`traffic66 a.pcap b.pcapng` は 127.0.0.1 で起動してブラウザーを開きます(最大 3 ファイル、3 GB。Ctrl+C で取り込んだデータを削除)。そのページで 50 MB までのファイルを最大 3 つアップロードすることもできます。扱うのはフローで、パケットの中身ではありません。 +**オフライン pcap 分析** は、パケットキャプチャ(pcap、pcapng)をライブデータとは分けて、同じページで表示します。`traffic66 a.pcap b.pcapng` は 127.0.0.1 で起動してブラウザーを開きます(最大 3 ファイル、3 GB。Ctrl+C で取り込んだデータを削除)。そのページで 50 MB までのファイルを最大 3 つアップロードすることもできます。一度に分析するのは 1 ファイルで、それぞれ専用のデータベースに入ります。ファイルの行の **分析** でそのファイルが全ページに表示され、上部のバーで別のファイルに切り替えられます。扱うのはフローで、パケットの中身ではありません。 ![オフライン分析:キャプチャファイルとパケット数、フロー数、時間](images/sandbox.png) @@ -282,7 +282,14 @@ snmp 192.0.2.9 s3cret 10.99.0.9:161 # other management address ![ターミナル UI:上位 66 の会話](images/tui-topn.png) -**ローカルキャプチャ** はローカルのインターフェースからフローを作ります。スイッチのミラーポートにつないだポートが最適です。`traffic66 interfaces` で一覧を表示し、`-capture eth1`(または Windows の名前か番号)でキャプチャします。Linux では root か `setcap cap_net_raw,cap_net_admin+ep`、macOS では root、Windows では [Npcap](https://npcap.com) が必要です。キャプチャしたフローは機器 `127.0.0.1` からのものとして表示されます。 +**ローカルキャプチャ** はローカルのインターフェースからフローを作ります。スイッチのミラーポートにつないだポートが最適です。`traffic66 interfaces` で一覧を表示し、`-capture eth1` でキャプチャします。Windows では: + +``` +traffic66.exe interfaces # list the network cards: name, number, address +traffic66.exe -capture Wi-Fi # capture on the wireless card (or by number: -capture 2) +``` + +Linux では root か `setcap cap_net_raw,cap_net_admin+ep`、macOS では root、Windows では [Npcap](https://npcap.com) が必要です。キャプチャしたフローは機器 `127.0.0.1` からのものとして表示されます。ローカルキャプチャには機器のインターフェースもカウンターもないため、**インターフェース照合** で比較する対象はありません。 diff --git a/docs/README.ko.md b/docs/README.ko.md index 244fdfb..3bc3223 100644 --- a/docs/README.ko.md +++ b/docs/README.ko.md @@ -272,7 +272,7 @@ snmp 192.0.2.9 s3cret 10.99.0.9:161 # other management address ## 9. 오프라인 pcap, 터미널 UI, 로컬 캡처 -**오프라인 pcap 분석**은 패킷 캡처(pcap, pcapng)를 실시간 데이터와 따로, 같은 페이지로 보여 줍니다. `traffic66 a.pcap b.pcapng`는 127.0.0.1에서 시작해 브라우저를 엽니다(최대 3개 파일, 3 GB. Ctrl+C로 가져온 데이터를 삭제). 또는 그 페이지에서 50 MB까지의 파일을 최대 3개 업로드할 수 있습니다. 다루는 것은 플로이며, 패킷 내용이 아닙니다. +**오프라인 pcap 분석**은 패킷 캡처(pcap, pcapng)를 실시간 데이터와 따로, 같은 페이지로 보여 줍니다. `traffic66 a.pcap b.pcapng`는 127.0.0.1에서 시작해 브라우저를 엽니다(최대 3개 파일, 3 GB. Ctrl+C로 가져온 데이터를 삭제). 또는 그 페이지에서 50 MB까지의 파일을 최대 3개 업로드할 수 있습니다. 한 번에 한 파일씩, 파일마다 별도의 데이터베이스에서 분석합니다. 파일 행의 **분석**을 누르면 모든 페이지에 그 파일이 표시되고, 상단 바에서 다른 파일로 전환합니다. 다루는 것은 플로이며, 패킷 내용이 아닙니다. ![오프라인 분석: 캡처 파일과 패킷, 플로, 시간](images/sandbox.png) @@ -282,7 +282,14 @@ snmp 192.0.2.9 s3cret 10.99.0.9:161 # other management address ![터미널 UI: Top 66 대화](images/tui-topn.png) -**로컬 캡처**는 로컬 인터페이스에서 플로를 만듭니다. 스위치의 미러 포트에 연결한 포트가 가장 좋습니다. `traffic66 interfaces`로 목록을 보고, `-capture eth1`(또는 Windows의 이름이나 번호)로 캡처합니다. Linux는 root 또는 `setcap cap_net_raw,cap_net_admin+ep`, macOS는 root, Windows는 [Npcap](https://npcap.com)이 필요합니다. 캡처한 플로는 장비 `127.0.0.1`에서 온 것으로 표시됩니다. +**로컬 캡처**는 로컬 인터페이스에서 플로를 만듭니다. 스위치의 미러 포트에 연결한 포트가 가장 좋습니다. `traffic66 interfaces`로 목록을 보고, `-capture eth1`로 캡처합니다. Windows에서는: + +``` +traffic66.exe interfaces # list the network cards: name, number, address +traffic66.exe -capture Wi-Fi # capture on the wireless card (or by number: -capture 2) +``` + +Linux는 root 또는 `setcap cap_net_raw,cap_net_admin+ep`, macOS는 root, Windows는 [Npcap](https://npcap.com)이 필요합니다. 캡처한 플로는 장비 `127.0.0.1`에서 온 것으로 표시됩니다. 로컬 캡처에는 장비 인터페이스나 카운터가 없으므로 **인터페이스 대조**에서 비교할 것이 없습니다. diff --git a/docs/README.pt.md b/docs/README.pt.md index 8097a2b..8aa5bf1 100644 --- a/docs/README.pt.md +++ b/docs/README.pt.md @@ -357,8 +357,11 @@ muito pequenas podem se esconder atrás da amostragem. **Análise offline de pcap** mostra capturas de pacotes (pcap, pcapng) com as mesmas páginas, separadas dos dados ao vivo: `traffic66 a.pcap b.pcapng` inicia em 127.0.0.1 e abre o navegador (até 3 arquivos, 3 GB; Ctrl+C apaga -os dados importados), ou envie até 3 arquivos de 50 MB nessa página. Ela -trabalha com fluxos, não com o conteúdo dos pacotes. +os dados importados), ou envie até 3 arquivos de 50 MB nessa página. Um +arquivo é analisado por vez, cada um em seu próprio banco de dados: +**Analisar** na linha de um arquivo o mostra em todas as páginas, e a barra +no topo troca para outro. Ela trabalha com fluxos, não com o conteúdo dos +pacotes. ![Análise offline: arquivos de captura com pacotes, fluxos e período](images/sandbox.png) @@ -373,10 +376,18 @@ tempo, w abrir no navegador, q sair; `-lang` escolhe o idioma. **Captura local** gera fluxos a partir de uma interface local, de preferência uma porta ligada à porta espelho de um switch: -`traffic66 interfaces` as lista, `-capture eth1` (ou um nome ou número do -Windows) captura. O Linux precisa de root ou -`setcap cap_net_raw,cap_net_admin+ep`, o macOS de root, o Windows do -[Npcap](https://npcap.com). Os fluxos capturados vêm do equipamento `127.0.0.1`. +`traffic66 interfaces` as lista, `-capture eth1` captura. No Windows: + +``` +traffic66.exe interfaces # list the network cards: name, number, address +traffic66.exe -capture Wi-Fi # capture on the wireless card (or by number: -capture 2) +``` + +O Linux precisa de root ou `setcap cap_net_raw,cap_net_admin+ep`, o macOS de +root, o Windows do [Npcap](https://npcap.com). Os fluxos capturados vêm do +equipamento `127.0.0.1`. A captura local não tem interfaces nem contadores +do equipamento, então a **Conferência de interfaces** não tem nada a +comparar para ela. diff --git a/docs/README.ru.md b/docs/README.ru.md index 8d611da..195c9cb 100644 --- a/docs/README.ru.md +++ b/docs/README.ru.md @@ -359,7 +359,9 @@ Geolocation by DB-IP", [db-ip.com](https://db-ip.com)); **Настройки** страницах, отдельно от живых данных: `traffic66 a.pcap b.pcapng` запускается на 127.0.0.1 и открывает браузер (до 3 файлов, 3 ГБ; Ctrl+C удаляет импортированные данные), или загрузите на этой странице до 3 файлов по -50 МБ. Анализируются потоки, а не содержимое пакетов. +50 МБ. Файлы анализируются по одному, у каждого своя база данных: кнопка +**Анализ** в строке файла показывает его на всех страницах, а панель вверху +переключает на другой. Анализируются потоки, а не содержимое пакетов. ![Офлайн-анализ: файлы захвата с пакетами, потоками и временем](images/sandbox.png) @@ -374,10 +376,17 @@ t период, w открыть в браузере, q выход; `-lang` вы **Локальный захват** строит потоки с локального интерфейса, лучше всего с порта, подключённого к зеркальному порту коммутатора: `traffic66 interfaces` -выводит их список, `-capture eth1` (или имя либо номер в Windows) -захватывает. В Linux нужны root или `setcap cap_net_raw,cap_net_admin+ep`, -в macOS — root, в Windows — [Npcap](https://npcap.com). Захваченные потоки -приходят от устройства `127.0.0.1`. +выводит их список, `-capture eth1` захватывает. В Windows: + +``` +traffic66.exe interfaces # list the network cards: name, number, address +traffic66.exe -capture Wi-Fi # capture on the wireless card (or by number: -capture 2) +``` + +В Linux нужны root или `setcap cap_net_raw,cap_net_admin+ep`, в macOS — root, +в Windows — [Npcap](https://npcap.com). Захваченные потоки приходят от +устройства `127.0.0.1`. У локального захвата нет интерфейсов и счётчиков +устройства, поэтому в **Сверке интерфейсов** для него нечего сравнивать. diff --git a/docs/README.ur.md b/docs/README.ur.md index b9c3c62..dbf71bd 100644 --- a/docs/README.ur.md +++ b/docs/README.ur.md @@ -354,8 +354,10 @@ Threat lists متن کی فائلیں ہیں، ہر لائن میں ایک addre **آف لائن pcap تجزیہ** پیکٹ کیپچر (pcap، pcapng) کو انہی صفحات پر دکھاتا ہے، لائیو ڈیٹا سے الگ: `traffic66 a.pcap b.pcapng` 127.0.0.1 پر شروع ہوتا ہے اور browser کھولتا ہے (زیادہ سے زیادہ 3 فائلیں، 3 GB؛ Ctrl+C امپورٹ شدہ ڈیٹا حذف کر دیتا -ہے)، یا اسی صفحے پر 50 MB تک کی 3 فائلیں اپ لوڈ کریں۔ یہ flows پر کام کرتا ہے، -پیکٹ کے مواد پر نہیں۔ +ہے)، یا اسی صفحے پر 50 MB تک کی 3 فائلیں اپ لوڈ کریں۔ ایک وقت میں ایک فائل کا +تجزیہ ہوتا ہے، ہر فائل اپنے الگ ڈیٹابیس میں: کسی فائل کی قطار پر +**تجزیہ کریں** اسے ہر صفحے پر دکھاتا ہے، اور اوپر کی پٹی دوسری فائل پر بدل دیتی +ہے۔ یہ flows پر کام کرتا ہے، پیکٹ کے مواد پر نہیں۔ ![آف لائن تجزیہ: کیپچر فائلیں، ان کے پیکٹ، فلو اور وقت](images/sandbox.png) @@ -370,9 +372,17 @@ browser میں کھولیں، q باہر نکلیں؛ `-lang` زبان چنتا **Local capture** ایک local interface سے flows بناتا ہے، بہترین ہے کہ وہ کسی switch کے mirror port سے جڑا port ہو: `traffic66 interfaces` ان کی فہرست دیتا -ہے، `-capture eth1` (یا Windows کا نام یا نمبر) capture کرتا ہے۔ Linux کو root یا -`setcap cap_net_raw,cap_net_admin+ep` چاہیے، macOS کو root، Windows کو -[Npcap](https://npcap.com)۔ capture شدہ flows ڈیوائس `127.0.0.1` سے آتے ہیں۔ +ہے، `-capture eth1` capture کرتا ہے۔ Windows پر: + +``` +traffic66.exe interfaces # list the network cards: name, number, address +traffic66.exe -capture Wi-Fi # capture on the wireless card (or by number: -capture 2) +``` + +Linux کو root یا `setcap cap_net_raw,cap_net_admin+ep` چاہیے، macOS کو root، +Windows کو [Npcap](https://npcap.com)۔ capture شدہ flows ڈیوائس `127.0.0.1` سے +آتے ہیں۔ Local capture میں ڈیوائس کے interfaces یا counters نہیں ہوتے، اس لیے +**انٹرفیس جانچ** کے پاس اس کے لیے موازنہ کرنے کو کچھ نہیں ہے۔ diff --git a/docs/README.zh.md b/docs/README.zh.md index 45e78a0..4502d89 100644 --- a/docs/README.zh.md +++ b/docs/README.zh.md @@ -272,7 +272,7 @@ snmp 192.0.2.9 s3cret 10.99.0.9:161 # other management address ## 9. 离线 pcap、终端界面、本地抓包 -**离线 pcap 分析** 用相同的页面查看抓包文件(pcap、pcapng),与实时数据分开:`traffic66 a.pcap b.pcapng` 在 127.0.0.1 上启动并打开浏览器(最多 3 个文件,3 GB;按 Ctrl+C 删除导入的数据),也可以在该页面上传最多 3 个 50 MB 的文件。它分析的是流,而不是包内容。 +**离线 pcap 分析** 用相同的页面查看抓包文件(pcap、pcapng),与实时数据分开:`traffic66 a.pcap b.pcapng` 在 127.0.0.1 上启动并打开浏览器(最多 3 个文件,3 GB;按 Ctrl+C 删除导入的数据),也可以在该页面上传最多 3 个 50 MB 的文件。一次分析一个文件,每个文件有自己独立的数据库:在文件所在行点 **分析**,所有页面都显示该文件,顶部的栏可切换到另一个文件。它分析的是流,而不是包内容。 ![离线分析:抓包文件及其包数、流数和时间](images/sandbox.png) @@ -282,7 +282,14 @@ snmp 192.0.2.9 s3cret 10.99.0.9:161 # other management address ![终端界面:Top 66 会话](images/tui-topn.png) -**本地抓包** 从本机网卡生成流,最好是接到交换机镜像端口的网口:`traffic66 interfaces` 列出网卡,`-capture eth1`(或 Windows 的名称或编号)开始抓包。Linux 需要 root 或 `setcap cap_net_raw,cap_net_admin+ep`,macOS 需要 root,Windows 需要 [Npcap](https://npcap.com)。抓到的流显示为来自设备 `127.0.0.1`。 +**本地抓包** 从本机网卡生成流,最好是接到交换机镜像端口的网口:`traffic66 interfaces` 列出网卡,`-capture eth1` 开始抓包。在 Windows 上: + +``` +traffic66.exe interfaces # list the network cards: name, number, address +traffic66.exe -capture Wi-Fi # capture on the wireless card (or by number: -capture 2) +``` + +Linux 需要 root 或 `setcap cap_net_raw,cap_net_admin+ep`,macOS 需要 root,Windows 需要 [Npcap](https://npcap.com)。抓到的流显示为来自设备 `127.0.0.1`。本地抓包没有设备接口和计数器,因此 **接口对账** 对它没有可对比的内容。 diff --git a/docs/images/sandbox.png b/docs/images/sandbox.png index e48ed47..0682f9f 100644 Binary files a/docs/images/sandbox.png and b/docs/images/sandbox.png differ diff --git a/internal/api/sandbox.go b/internal/api/sandbox.go index bf58ebc..468c150 100644 --- a/internal/api/sandbox.go +++ b/internal/api/sandbox.go @@ -78,3 +78,16 @@ func (s *Server) deleteSandbox(w http.ResponseWriter, r *http.Request) { } writeJSON(w, http.StatusOK, s.SB.Info()) } + +// putSandboxActive makes one capture file the one the pages show. +func (s *Server) putSandboxActive(w http.ResponseWriter, r *http.Request) { + if s.SB == nil { + fail(w, errors.New("offline analysis is not available")) + return + } + if err := s.SB.Select(r.URL.Query().Get("name")); err != nil { + fail(w, err) + return + } + writeJSON(w, http.StatusOK, s.SB.Info()) +} diff --git a/internal/api/server.go b/internal/api/server.go index 6a1d826..421d483 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -112,6 +112,7 @@ func (s *Server) Handler() http.Handler { api("GET /api/sandbox", s.getSandbox) api("POST /api/sandbox/files", s.putSandboxFile) api("DELETE /api/sandbox/files", s.deleteSandbox) + api("POST /api/sandbox/active", s.putSandboxActive) api("POST /api/logo", s.putLogo) api("DELETE /api/logo", s.deleteLogo) api("GET /api/geo", s.getGeo) diff --git a/internal/capture/capture.go b/internal/capture/capture.go index 600e067..d4eeb62 100644 --- a/internal/capture/capture.go +++ b/internal/capture/capture.go @@ -38,6 +38,7 @@ type entry struct { first, last time.Time pkts, bytes uint64 flags uint8 + opener int8 // kept across active-timeout exports } // Capture is one running interface capture. @@ -111,7 +112,7 @@ func (c *Capture) readLoop(ctx context.Context) { c.Dropped.Add(1) return } - e = &entry{first: now} + e = &entry{first: now, opener: opener(r.TCPFlags)} c.table[k] = e } e.last = now @@ -145,6 +146,15 @@ func (c *Capture) expireLoop(ctx context.Context) { func (c *Capture) flush(now time.Time, all bool) { var out []flow.Record c.mu.Lock() + // first learn who opened each connection from either direction, before + // any entry is removed below + for k, e := range c.table { + if e.opener == 0 && e.pkts > 0 { + if re := c.table[key{k.dst, k.src, k.dport, k.sport, k.proto, k.vlan, k.encap}]; re != nil { + e.opener = -re.opener + } + } + } for k, e := range c.table { idle := now.Sub(e.last) >= idleTimeout active := now.Sub(e.first) >= activeTimeout @@ -157,15 +167,29 @@ func (c *Capture) flush(now time.Time, all bool) { } continue } + op := e.opener out = append(out, flow.Record{Start: e.first, End: e.last, Src: k.src, Dst: k.dst, SrcPort: k.sport, DstPort: k.dport, - Proto: k.proto, VLAN: k.vlan, Encap: k.encap, TCPFlags: e.flags, Bytes: e.bytes, Packets: e.pkts, L2: true, + Proto: k.proto, VLAN: k.vlan, Encap: k.encap, TCPFlags: e.flags, Bytes: e.bytes, Packets: e.pkts, L2: true, Opener: op, Sampling: 1, Mult: 1, SamplingKnown: true, Exporter: Exporter, Domain: c.domain, Source: flow.SrcCapture, Direction: 255}) if idle || all { delete(c.table, k) } else { - *e = entry{first: now, last: now} + *e = entry{first: now, last: now, opener: op} } } c.mu.Unlock() c.sink.Submit(out) } + +// opener reads the first packet of a TCP connection seen in one direction: +// a SYN alone opens it (1), a SYN+ACK answers (-1); anything else says +// nothing (0). +func opener(flags uint8) int8 { + switch flags & 0x12 { + case 0x02: + return 1 + case 0x12: + return -1 + } + return 0 +} diff --git a/internal/enrich/role_test.go b/internal/enrich/role_test.go new file mode 100644 index 0000000..1aa9282 --- /dev/null +++ b/internal/enrich/role_test.go @@ -0,0 +1,31 @@ +package enrich + +import ( + "net/netip" + "testing" + + "github.com/githubflyideas/traffic66/internal/flow" +) + +// Exported TCP records carry the flags of all their packets: both +// directions of a normal connection read SYN+ACK. Both must agree on who +// the client is. +func TestRoleMergedFlags(t *testing.T) { + pc, srv := netip.MustParseAddr("10.0.0.5"), netip.MustParseAddr("20.27.177.113") + up := flow.Record{Src: pc, Dst: srv, SrcPort: 52114, DstPort: 443, Proto: 6, TCPFlags: 0x1b, Packets: 40} + down := flow.Record{Src: srv, Dst: pc, SrcPort: 443, DstPort: 52114, Proto: 6, TCPFlags: 0x1b, Packets: 60} + if !Role(&up) || Role(&down) { + t.Fatalf("up=%v down=%v, want the PC as client both ways", Role(&up), Role(&down)) + } + // both ports unknown: the opener seen in the first packet decides + a := flow.Record{Src: pc, Dst: srv, SrcPort: 40000, DstPort: 50000, Proto: 6, TCPFlags: 0x1b, Packets: 9, Opener: 1} + b := flow.Record{Src: srv, Dst: pc, SrcPort: 50000, DstPort: 40000, Proto: 6, TCPFlags: 0x1b, Packets: 9, Opener: -1} + if !Role(&a) || Role(&b) { + t.Fatalf("opener ignored: a=%v b=%v", Role(&a), Role(&b)) + } + // a single sampled packet still reads its flags + syn := flow.Record{Src: pc, Dst: srv, SrcPort: 50000, DstPort: 40000, Proto: 6, TCPFlags: 0x02, Packets: 1} + if !Role(&syn) { + t.Fatal("a lone SYN opens the connection") + } +} diff --git a/internal/enrich/services.go b/internal/enrich/services.go index eb7ce92..3d3ebc6 100644 --- a/internal/enrich/services.go +++ b/internal/enrich/services.go @@ -60,10 +60,17 @@ func known(proto uint8, port uint16) bool { func Role(r *flow.Record) bool { switch r.Proto { case 6: - if r.TCPFlags&0x12 == 0x02 { + if r.Opener != 0 { + return r.Opener > 0 // the first packet of the connection was seen + } + // The flags of a record are those of all its packets together: a + // client's SYN followed by ACKs reads as SYN+ACK just like the + // server's answer. They tell the roles only for a single packet + // (an sFlow sample, a lone SYN of a scan). + if r.Packets == 1 && r.TCPFlags&0x12 == 0x02 { return true // SYN without ACK: source opened the connection } - if r.TCPFlags&0x12 == 0x12 { + if r.Packets == 1 && r.TCPFlags&0x12 == 0x12 { return false // SYN+ACK: source is answering } case 17, 132: diff --git a/internal/flow/flow.go b/internal/flow/flow.go index 1c981f0..ef59e44 100644 --- a/internal/flow/flow.go +++ b/internal/flow/flow.go @@ -85,6 +85,12 @@ type Record struct { Encap Encap // Direction as reported by the exporter: 0 ingress, 1 egress, 255 unknown. Direction uint8 + // Opener says who opened a TCP connection, when the packets showed it + // (local capture and pcap files see the first packet of each + // direction): 1 the source sent the SYN, -1 the source answered it with + // SYN+ACK, 0 unknown. Exported records carry only the merged flags of + // all packets, which cannot tell. + Opener int8 SrcAS, DstAS uint32 diff --git a/internal/sandbox/flows.go b/internal/sandbox/flows.go index 4e3fbf9..7fa3ca5 100644 --- a/internal/sandbox/flows.go +++ b/internal/sandbox/flows.go @@ -34,6 +34,7 @@ type fentry struct { first, last time.Time pkts, bytes uint64 flags uint8 + opener int8 // see flow.Record.Opener; kept across active-timeout exports } const ( @@ -89,6 +90,12 @@ func (b *builder) add(p *pcapfile.Packet) { b.sweep(p.Time, true) } e = &fentry{first: p.Time} + switch r.TCPFlags & 0x12 { + case 0x02: + e.opener = 1 + case 0x12: + e.opener = -1 + } b.table[k] = e } if p.Time.After(e.last) { @@ -111,6 +118,15 @@ func (b *builder) add(p *pcapfile.Packet) { func (b *builder) sweep(now time.Time, all bool) { b.swept = now var out []flow.Record + // first learn who opened each connection from either direction, before + // any entry is removed below + for k, e := range b.table { + if e.opener == 0 && e.pkts > 0 { + if re := b.table[fkey{k.dst, k.src, k.dport, k.sport, k.proto, k.vlan, k.encap, k.l2}]; re != nil { + e.opener = -re.opener + } + } + } for k, e := range b.table { idle := now.Sub(e.last) >= idleTimeout active := now.Sub(e.first) >= activeTimeout @@ -118,14 +134,14 @@ func (b *builder) sweep(now time.Time, all bool) { continue } if e.pkts > 0 { - out = append(out, flow.Record{Start: e.first, End: e.last, Src: k.src, Dst: k.dst, SrcPort: k.sport, DstPort: k.dport, + out = append(out, flow.Record{Start: e.first, End: e.last, Src: k.src, Dst: k.dst, SrcPort: k.sport, DstPort: k.dport, Opener: e.opener, Proto: k.proto, VLAN: k.vlan, Encap: k.encap, TCPFlags: e.flags, Bytes: e.bytes, Packets: e.pkts, L2: k.l2, Sampling: 1, Mult: 1, SamplingKnown: true, Exporter: b.exporter, Domain: b.domain, Source: flow.SrcCapture, Direction: 255}) } if idle || all { delete(b.table, k) } else { - *e = fentry{first: now, last: now} + *e = fentry{first: now, last: now, opener: e.opener} } } if len(out) > 0 { diff --git a/internal/sandbox/sandbox.go b/internal/sandbox/sandbox.go index 3121c8f..8f830ef 100644 --- a/internal/sandbox/sandbox.go +++ b/internal/sandbox/sandbox.go @@ -58,7 +58,9 @@ type File struct { Path string `json:"path,omitempty"` // a file opened in place (never deleted); else it is in Dir/files } -// Sandbox holds the uploaded files and their database. +// Sandbox holds the uploaded files. Each file has a database of its own, so +// one capture's flows and findings never mix with another's; the pages show +// one file at a time, the active one. type Sandbox struct { Dir string Inv *enrich.Inventory // the live inventory: names of networks and hosts @@ -72,14 +74,15 @@ type Sandbox struct { work sync.Mutex closed bool - mu sync.Mutex - files []*File - st *store.Store - det *detect.Detector - inv *enrich.Inventory // live inventory plus a device per file - busy bool - gen int // bumped by every rebuild; stale imports stop - wake chan struct{} + mu sync.Mutex + files []*File + active string // the file the pages show + st *store.Store // the active file's database, nil when not open + det *detect.Detector // its detection rules + inv *enrich.Inventory // live inventory plus a device per file + busy bool + gen int // bumped by every rebuild; stale imports stop + wake chan struct{} } // ErrLimit is returned when a file would exceed the limits. @@ -100,13 +103,6 @@ func NewWith(dir string, inv *enrich.Inventory, asn *enrich.ASNDB, thr *enrich.T sb.load() go sb.worker() if len(sb.files) > 0 { - sb.mu.Lock() - if sb.st == nil { // database missing: rebuild from the files - for _, f := range sb.files { - f.Status = "waiting" - } - } - sb.mu.Unlock() sb.kick() } return sb @@ -121,9 +117,23 @@ func (sb *Sandbox) filePath(f *File) string { } return filepath.Join(sb.pcapDir(), f.Name) } -func (sb *Sandbox) dbDir() string { return filepath.Join(sb.Dir, "db") } -func (sb *Sandbox) indexPath() string { - return filepath.Join(sb.Dir, "files.json") +func (sb *Sandbox) dbRoot() string { return filepath.Join(sb.Dir, "db") } +func (sb *Sandbox) dbDir(f *File) string { return filepath.Join(sb.dbRoot(), f.Exporter) } +func (sb *Sandbox) indexPath() string { return filepath.Join(sb.Dir, "files.json") } +func (sb *Sandbox) activePath() string { return filepath.Join(sb.Dir, "active") } +func (sb *Sandbox) hasDB(f *File) bool { + _, err := os.Stat(filepath.Join(sb.dbDir(f), "traffic66.duckdb")) + return err == nil +} + +// file returns the file of that name; sb.mu held. +func (sb *Sandbox) file(name string) *File { + for _, f := range sb.files { + if f.Name == name { + return f + } + } + return nil } func (sb *Sandbox) load() { @@ -140,16 +150,49 @@ func (sb *Sandbox) load() { sb.files = append(sb.files, f) } } - if _, err := os.Stat(filepath.Join(sb.dbDir(), "traffic66.duckdb")); err == nil { - if err := sb.open(); err != nil { + // before 1.5.2 all files shared one database: import them again + if _, err := os.Stat(filepath.Join(sb.dbRoot(), "traffic66.duckdb")); err == nil { + os.RemoveAll(sb.dbRoot()) + } + for _, f := range sb.files { + if f.Status != "error" && (f.Status != "done" || !sb.hasDB(f)) { + f.Status = "waiting" + } + } + if b, err := os.ReadFile(sb.activePath()); err == nil { + sb.active = strings.TrimSpace(string(b)) + } + sb.pickActive() + if f := sb.file(sb.active); f != nil && f.Status == "done" { + if err := sb.openActive(); err != nil { log.Printf("sandbox: %v", err) } } +} + +// pickActive keeps the active file if it is still there, else takes the +// first imported one; sb.mu held. +func (sb *Sandbox) pickActive() { + if f := sb.file(sb.active); f != nil && f.Status != "error" { + return + } + sb.active = "" for _, f := range sb.files { - if f.Status != "done" && f.Status != "error" { - f.Status = "waiting" + if f.Status == "done" { + sb.active = f.Name + break } } + sb.saveActive() +} + +func (sb *Sandbox) saveActive() { + if sb.active == "" { + os.Remove(sb.activePath()) + return + } + os.MkdirAll(sb.Dir, 0o755) + os.WriteFile(sb.activePath(), []byte(sb.active), 0o644) } func (sb *Sandbox) save() { @@ -161,13 +204,22 @@ func (sb *Sandbox) save() { } } -// open opens (or creates) the database; sb.mu held or not yet shared. -func (sb *Sandbox) open() error { +// openDB opens (or creates) one file's database. +func (sb *Sandbox) openDB(f *File) (*store.Store, error) { threads := 2 // beside live collection if sb.Mem > 0.1 { threads = 0 // on its own: all but one core } - st, err := store.Open(store.Options{Dir: sb.dbDir(), MemoryFraction: sb.Mem, Threads: threads}) + return store.Open(store.Options{Dir: sb.dbDir(f), MemoryFraction: sb.Mem, Threads: threads}) +} + +// openActive opens the active file's database for the pages; sb.mu held. +func (sb *Sandbox) openActive() error { + f := sb.file(sb.active) + if f == nil { + return errors.New("no capture file chosen") + } + st, err := sb.openDB(f) if err != nil { return err } @@ -177,6 +229,34 @@ func (sb *Sandbox) open() error { return nil } +// closeActive closes the pages' database; sb.mu held. +func (sb *Sandbox) closeActive() { + if sb.st != nil { + sb.st.Close() + } + sb.st, sb.det = nil, nil +} + +// Select makes the file of that name the one the pages show. +func (sb *Sandbox) Select(name string) error { + sb.mu.Lock() + defer sb.mu.Unlock() + f := sb.file(name) + if f == nil { + return errors.New("no such file") + } + if f.Status != "done" { + return errors.New("the file is not imported yet") + } + if sb.active == name && sb.st != nil { + return nil + } + sb.closeActive() + sb.active = name + sb.saveActive() + return sb.openActive() +} + // refreshInventory names each file's exporter after the file. func (sb *Sandbox) refreshInventory() { inv := enrich.NewInventory() @@ -197,7 +277,7 @@ func (sb *Sandbox) refreshInventory() { } } -// Store returns the sandbox database, or nil when there is none. +// Store returns the active file's database, or nil when there is none. func (sb *Sandbox) Store() (*store.Store, *detect.Detector, *enrich.Inventory) { sb.mu.Lock() defer sb.mu.Unlock() @@ -206,10 +286,11 @@ func (sb *Sandbox) Store() (*store.Store, *detect.Detector, *enrich.Inventory) { // Info describes the sandbox for the UI. type Info struct { - Files []File `json:"files"` - Busy bool `json:"busy"` - First time.Time `json:"first"` - Last time.Time `json:"last"` + Files []File `json:"files"` + Busy bool `json:"busy"` + Active string `json:"active"` // the file the pages show + First time.Time `json:"first"` // its time span + Last time.Time `json:"last"` Limits Ready bool `json:"ready"` // has data to look at } @@ -217,19 +298,13 @@ type Info struct { func (sb *Sandbox) Info() Info { sb.mu.Lock() defer sb.mu.Unlock() - in := Info{Files: []File{}, Busy: sb.busy, Limits: sb.Lim} + in := Info{Files: []File{}, Busy: sb.busy, Limits: sb.Lim, Active: sb.active} for _, f := range sb.files { in.Files = append(in.Files, *f) - if f.Flows == 0 { - continue - } - if in.First.IsZero() || f.First.Before(in.First) { - in.First = f.First - } - if f.Last.After(in.Last) { - in.Last = f.Last + if f.Name == sb.active && f.Flows > 0 { + in.First, in.Last = f.First, f.Last + in.Ready = sb.st != nil } - in.Ready = sb.st != nil } return in } @@ -408,15 +483,11 @@ func slicesDelete(fs []*File, f *File) []*File { return out } -// Delete removes one file (name) or everything (name ""). The database is -// rebuilt from the remaining files. +// Delete removes one file (name) or everything (name ""), with its +// database. The other files keep theirs. func (sb *Sandbox) Delete(name string) error { sb.mu.Lock() - found := name == "" - for _, f := range sb.files { - found = found || f.Name == name - } - if !found { + if name != "" && sb.file(name) == nil { sb.mu.Unlock() return errors.New("no such file") } @@ -433,31 +504,32 @@ func (sb *Sandbox) Delete(name string) error { keep = append(keep, f) continue } + if f.Name == sb.active { + sb.closeActive() + } if f.Path == "" { os.Remove(filepath.Join(sb.pcapDir(), f.Name)) } + os.RemoveAll(sb.dbDir(f)) continue } + if f.Status == "importing" { // stopped by the delete: start it again + f.Status = "waiting" + } keep = append(keep, f) } sb.files = keep - st := sb.st - sb.st, sb.det = nil, nil - for _, f := range sb.files { - if f.Status != "uploading" { - f.Status, f.Error, f.Packets, f.Skipped, f.Flows, f.First, f.Last = "waiting", "", 0, 0, 0, time.Time{}, time.Time{} + sb.pickActive() + if sb.st == nil && sb.active != "" { + if err := sb.openActive(); err != nil { + log.Printf("sandbox: %v", err) } } sb.save() sb.refreshInventory() + empty := len(keep) == 0 sb.mu.Unlock() - if st != nil { - st.Close() - } - if err := os.RemoveAll(sb.dbDir()); err != nil { - return err - } - if len(keep) == 0 { + if empty { os.RemoveAll(sb.Dir) } sb.kick() @@ -504,17 +576,25 @@ func (sb *Sandbox) importNext() bool { sb.mu.Unlock() return false } - if sb.st == nil { - if err := sb.open(); err != nil { - f.Status, f.Error = "error", err.Error() - sb.save() - sb.mu.Unlock() - return true - } + // a fresh database of its own + if f.Name == sb.active { + sb.closeActive() + } + os.RemoveAll(sb.dbDir(f)) + st, err := sb.openDB(f) + if err != nil { + f.Status, f.Error = "error", err.Error() + sb.save() + sb.mu.Unlock() + return true } f.Status = "importing" sb.busy = true - gen, st, det, inv := sb.gen, sb.st, sb.det, sb.inv + if sb.inv == nil { + sb.refreshInventory() + } + gen, inv := sb.gen, sb.inv + det := detect.New(st, inv, detect.Config{}) sb.mu.Unlock() res, err := sb.importFile(gen, f, st, inv) @@ -531,10 +611,11 @@ func (sb *Sandbox) importNext() bool { } } } + st.Close() sb.mu.Lock() defer sb.mu.Unlock() sb.busy = false - if gen != sb.gen { // deleted meanwhile + if gen != sb.gen { // deleted or stopped meanwhile return true } f.Packets, f.Skipped, f.Flows, f.First, f.Last = res.packets, res.skipped, res.flows, res.first, res.last @@ -546,6 +627,15 @@ func (sb *Sandbox) importNext() bool { default: f.Status = "done" } + if f.Status == "done" && (sb.active == "" || sb.active == f.Name) { + sb.active = f.Name + sb.saveActive() + if sb.st == nil { + if err := sb.openActive(); err != nil { + log.Printf("sandbox: %v", err) + } + } + } sb.save() log.Printf("sandbox: %s: %d packets, %d flows, %s – %s (%v)", f.Name, res.packets, res.flows, res.first.Format(time.RFC3339), res.last.Format(time.RFC3339), err) @@ -628,10 +718,6 @@ func (sb *Sandbox) Close() { sb.work.Lock() // wait for a running import to stop defer sb.work.Unlock() sb.mu.Lock() - st := sb.st - sb.st = nil + sb.closeActive() sb.mu.Unlock() - if st != nil { - st.Close() - } } diff --git a/internal/sandbox/sandbox_test.go b/internal/sandbox/sandbox_test.go index 4f9e1d2..aea5da3 100644 --- a/internal/sandbox/sandbox_test.go +++ b/internal/sandbox/sandbox_test.go @@ -153,3 +153,60 @@ func TestAddPath(t *testing.T) { t.Errorf("the original file was deleted: %v", err) } } + +// Two captures are analysed one at a time: each has its own flows and +// findings, the first imported is shown first, and switching shows the +// other one alone. +func TestOneFileAtATime(t *testing.T) { + dir := filepath.Join(t.TempDir(), "sandbox") + sb := New(dir, enrich.NewInventory(), enrich.NewASNDB(), enrich.NewThreats()) + var a, b bytes.Buffer + Sample(&a, time.Date(2026, 9, 20, 11, 0, 0, 0, time.UTC)) + Sample(&b, time.Date(2026, 9, 21, 11, 0, 0, 0, time.UTC)) + sb.Add("a.pcap", bytes.NewReader(a.Bytes()), false) + sb.Add("b.pcap", bytes.NewReader(b.Bytes()), false) + in := wait(t, sb) + if in.Active != "a.pcap" || !in.Ready || in.First.Day() != 20 { + t.Fatalf("active %q ready %v first %v", in.Active, in.Ready, in.First) + } + count := func() (n int, days string) { + st, _, _ := sb.Store() + st.DB.QueryRow(`SELECT count(*), string_agg(DISTINCT strftime(ts, '%d'), ',') FROM hot`).Scan(&n, &days) + return + } + if _, d := count(); d != "20" { + t.Errorf("a.pcap shows days %q", d) + } + if err := sb.Select("b.pcap"); err != nil { + t.Fatal(err) + } + if in := sb.Info(); in.Active != "b.pcap" || in.First.Day() != 21 { + t.Errorf("after switch: %q %v", in.Active, in.First) + } + if _, d := count(); d != "21" { + t.Errorf("b.pcap shows days %q", d) + } + // deleting the active file shows the other one; its data stays + if err := sb.Delete("b.pcap"); err != nil { + t.Fatal(err) + } + if in := sb.Info(); in.Active != "a.pcap" || !in.Ready { + t.Errorf("after delete: %q ready %v", in.Active, in.Ready) + } + if n, d := count(); n == 0 || d != "20" { + t.Errorf("a.pcap after delete: %d rows, days %q", n, d) + } + // a restart keeps the file and its database + sb.Close() + sb = New(dir, enrich.NewInventory(), enrich.NewASNDB(), enrich.NewThreats()) + defer sb.Close() + if in := wait(t, sb); in.Active != "a.pcap" || !in.Ready { + t.Errorf("after restart: %q ready %v", in.Active, in.Ready) + } + // a file added to a sandbox that has none open yet imports too + sb.Delete("") + sb.Add("b.pcap", bytes.NewReader(b.Bytes()), false) + if in := wait(t, sb); in.Active != "b.pcap" || !in.Ready { + t.Errorf("fresh: %q ready %v", in.Active, in.Ready) + } +} diff --git a/internal/web/static/app.css b/internal/web/static/app.css index ff3ae12..01e6458 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -365,3 +365,4 @@ label.btn.disabled{opacity:.5;cursor:not-allowed!important} .iflist .ifrow.peer>button:first-child{font-size:12px;color:var(--ink-3)} .iflist .ifrow.peer>button:first-child .muted{font-size:10.5px!important} .iflist .ifrow.peer>button:first-child .st{font-size:11px;opacity:.75} +#sbbar select{border:1px solid var(--line-2);border-radius:7px;padding:4px 8px;background:var(--surface);color:var(--ink);font-weight:600;max-width:260px} diff --git a/internal/web/static/app.js b/internal/web/static/app.js index 9cd1779..90c2f8e 100644 --- a/internal/web/static/app.js +++ b/internal/web/static/app.js @@ -214,6 +214,7 @@ async function renderIfsel() { w.hidden = !show; if (!show) return; const d = await loadIfaces(), list = d.ifaces || []; + if (!list.length) { w.hidden = true; return; } // e.g. local capture only: nothing to choose if (state.ifv === undefined) state.ifv = d.default || 'all'; const key = f => f.exporter + '/' + f.ifindex; const opt = (v, l, tt) => ``; @@ -1150,18 +1151,17 @@ views.sandbox = async (el) => { ${f.packets ? nf(f.packets) : '—'}${f.flows ? nf(f.flows) : '—'} ${f.flows ? esc(df.format(new Date(f.first))) + ' – ' + esc(df.format(new Date(f.last))) : ''} ${f.status === 'error' ? status('bad', f.error || t('sb.st_error')) : f.status === 'done' ? status('ok', t('sb.st_done')) : `${t('sb.st_' + f.status)}…`} - `).join(''); + `).join(''); el.innerHTML = `
${panel('c12', t('sb.title'), t('sb.sub'), `

${esc(t('sb.explain'))}

${rows || ``}
${t('sb.col_file')}${t('sb.col_size')}${t('sb.col_packets')}${t('sb.col_flows')}${t('sb.col_span')}${t('sb.col_status')}
${t('sb.empty')}
-
- +
${fs.length ? `` : ''}

${esc(sbInfo.max_total >= sbInfo.max_file_size * sbInfo.max_files ? t('sb.limits', {n: sbInfo.max_files, mb}) : sbInfo.max_total <= sbInfo.max_file_size ? t('sb.limits_total', {n: sbInfo.max_files, gb: fmtBytes(sbInfo.max_total)}) : t('sb.limits', {n: sbInfo.max_files, mb}) + ' ' + t('sb.limits_total', {n: sbInfo.max_files, gb: fmtBytes(sbInfo.max_total)}))} ${esc(t('sb.formats'))}

`)}
`; // the last upload problem stays shown after the page refreshes const msg = (ok, text) => { sbMsg = ok === false ? text : ''; const m = $('#sbMsg'); m.style.color = ok === null ? 'var(--ink-3)' : ok ? 'var(--good)' : 'var(--crit)'; m.textContent = text; }; if (sbMsg) msg(false, sbMsg); - $('#sbGo').onclick = () => { state.ds = 'sb'; go('overview'); }; + el.querySelectorAll('[data-sbgo]').forEach(b => b.onclick = async () => { if (await sbSelect(b.dataset.sbgo)) { state.ds = 'sb'; go('overview'); } }); el.querySelectorAll('[data-sbdel]').forEach(b => b.onclick = async () => { if (!confirm(t('sb.confirm', {f: b.dataset.sbdel}))) return; await fetch('/api/sandbox/files?name=' + encodeURIComponent(b.dataset.sbdel), {method: 'DELETE'}); @@ -1293,7 +1293,13 @@ views.ifaces = async (el) => { const d = await api('ifaces'); (d.ifaces || []).forEach(f => ifaceNames.set(f.exporter + '/' + f.ifindex, ifName(f) + ' · ' + (f.device || f.exporter))); const list = (d.ifaces || []).slice().sort((a, b) => (!!a.peer - !!b.peer) || (!!b.has_counters - !!a.has_counters) || ((devKind(b) === 'warn') - (devKind(a) === 'warn'))); - if (!list.length) { el.innerHTML = `
${t('empty.nodata')}
`; return; } + if (!list.length) { + // local capture has no device interfaces or counters to compare with + const src = await api('sources').catch(() => ({})); + const capOnly = (src.captures || []).length && !(src.sources || []).length; + el.innerHTML = `
${esc(t(capOnly ? 'if.capture_only' : 'empty.nodata'))}
`; + return; + } if (!state.ifc || !list.some(f => f.exporter === state.ifc.exporter && f.ifindex === state.ifc.ifindex)) { const d = list.find(f => f.default) || list.find(f => !f.peer) || list[0]; state.ifc = {exporter: d.exporter, ifindex: d.ifindex}; } const key = state.ifc.exporter + '/' + state.ifc.ifindex, sel = list.find(f => f.exporter === state.ifc.exporter && f.ifindex === state.ifc.ifindex); // one device's interface: count everything it saw, also traffic another device reported too @@ -1597,11 +1603,23 @@ function renderSB() { const r = sbRange(), span = r.to - r.from; const df = new Intl.DateTimeFormat(LANG, {dateStyle: 'medium', timeStyle: 'short'}), tf = new Intl.DateTimeFormat(LANG, {timeStyle: 'short'}); const sameDay = new Date(r.from).toDateString() === new Date(r.to).toDateString(); - const names = sbInfo.files.filter(f => f.status === 'done').map(f => f.name); - $('#sbbar').innerHTML = `${t('sb.banner')}${names.map(esc).join(' · ')} + // one file at a time; the others are a click away + const done = sbInfo.files.filter(f => f.status === 'done'); + const pick = done.length > 1 ? `` : `${esc(sbInfo.active)}`; + $('#sbbar').innerHTML = `${t('sb.banner')}${pick} ${esc(df.format(r.from))} – ${esc(sameDay ? tf.format(r.to) : df.format(r.to))} (${esc(fmtDur(span))}) ${offlineMode ? '' : ``}`; if ($('#sbBack')) $('#sbBack').onclick = () => { state.ds = ''; render(); }; + if ($('#sbPick')) $('#sbPick').onchange = async e => { if (await sbSelect(e.target.value)) render(); }; +} +// sbSelect makes one capture file the one every page shows. +async function sbSelect(name) { + if (name === sbInfo.active && sbInfo.ready) return true; + const res = await fetch('/api/sandbox/active?name=' + encodeURIComponent(name), {method: 'POST'}); + const j = await res.json().catch(() => ({})); + if (!res.ok) { toast(j.error || res.statusText); return false; } + sbInfo = j; + return true; } function fmtDur(ms) { const m = Math.round(ms / 6e4); diff --git a/internal/web/static/i18n/ar.json b/internal/web/static/i18n/ar.json index 7317999..70cb4cd 100644 --- a/internal/web/static/i18n/ar.json +++ b/internal/web/static/i18n/ar.json @@ -378,7 +378,7 @@ "nav.sandbox": "تحليل pcap دون اتصال", "sb.title": "ملفات الالتقاط", "sb.sub": "حلّل ملفات pcap بمعزل عن البيانات الحية", -"sb.explain": "ارفع ملفات التقاط من Wireshark أو tcpdump. تُحفظ في قاعدة بيانات منفصلة فلا تتأثر البيانات الحية وأرقامها واكتشافاتها. يعرض زر «تحليل» الملفات في كل الصفحات (نظرة عامة، أعلى 66، الاكتشافات، مسارات الحركة، الخريطة، سجلات التدفق)، ويحذف زر «حذف» الملفات وبياناتها.", +"sb.explain": "ارفع ملفات الالتقاط من Wireshark أو tcpdump. يذهب كل ملف إلى قاعدة بيانات خاصة به، فلا تتأثر البيانات الحية وأرقامها واكتشافاتها. يعرض «تحليل» ذلك الملف وحده في كل الصفحات (النظرة العامة، Top 66، الاكتشافات، مسارات التدفق، الخريطة، سجلات التدفق)؛ بدّل الملف من الشريط في الأعلى. «حذف» يزيل الملف وبياناته.", "sb.col_file": "الملف", "sb.col_size": "الحجم", "sb.col_packets": "الحزم", @@ -514,5 +514,6 @@ "if.peer_note": "واجهة الطرف الآخر: بلا عينات، فقط الحركة عبر الواجهة المأخوذ منها العينات في هذا الجهاز", "src.sampled_if": "العينات من: {v}", "src.dir_yes": "مع flowDirection (61)", -"src.dir_no": "بدون flowDirection (61): الواجهة مستنتجة من الحركة" +"src.dir_no": "بدون flowDirection (61): الواجهة مستنتجة من الحركة", +"if.capture_only": "تقارن مطابقة الواجهات التدفقات بعدادات الواجهات في أجهزة sFlow وNetFlow وIPFIX. الالتقاط المحلي على بطاقة شبكة هذا الجهاز لا يحمل واجهات أو عدادات أجهزة، لذا لا يوجد ما يُقارن هنا." } diff --git a/internal/web/static/i18n/bn.json b/internal/web/static/i18n/bn.json index a150a01..3e94cfa 100644 --- a/internal/web/static/i18n/bn.json +++ b/internal/web/static/i18n/bn.json @@ -378,7 +378,7 @@ "nav.sandbox": "অফলাইন pcap বিশ্লেষণ", "sb.title": "ক্যাপচার ফাইল", "sb.sub": "লাইভ ডেটা থেকে আলাদা করে pcap ফাইল বিশ্লেষণ", -"sb.explain": "Wireshark বা tcpdump-এর ক্যাপচার ফাইল আপলোড করুন। এগুলো আলাদা ডেটাবেসে যায়: লাইভ ডেটা, তার হিসাব ও ফলাফল বদলায় না। “বিশ্লেষণ” সব পেজে (সংক্ষেপ, Top 66, ফলাফল, ফ্লো পথ, মানচিত্র, ফ্লো রেকর্ড) ফাইলগুলো দেখায়; “মুছুন” ফাইল ও তার ডেটা মুছে দেয়।", +"sb.explain": "Wireshark বা tcpdump-এর ক্যাপচার ফাইল আপলোড করুন। প্রতিটি ফাইল নিজস্ব ডেটাবেসে যায়: লাইভ ডেটা, তার সংখ্যা ও শনাক্তকরণে কোনো প্রভাব পড়ে না। কোনো ফাইলের ‘বিশ্লেষণ’ চাপলে সব পেজ (সংক্ষিপ্তসার, Top 66, শনাক্তকরণ, ফ্লো পথ, মানচিত্র, ফ্লো রেকর্ড) শুধু সেই ফাইল দেখায়; উপরের বারে ফাইল বদলান। ‘মুছুন’ ফাইল ও তার ডেটা মুছে দেয়।", "sb.col_file": "ফাইল", "sb.col_size": "আকার", "sb.col_packets": "প্যাকেট", @@ -514,5 +514,6 @@ "if.peer_note": "বিপরীত ইন্টারফেস: স্যাম্পল নয়, শুধু এই ডিভাইসের স্যাম্পল করা ইন্টারফেস দিয়ে যাওয়া ট্রাফিক", "src.sampled_if": "স্যাম্পল ইন্টারফেস: {v}", "src.dir_yes": "flowDirection (61) সহ", -"src.dir_no": "flowDirection (61) ছাড়া: স্যাম্পল ইন্টারফেস ট্রাফিক থেকে অনুমান" +"src.dir_no": "flowDirection (61) ছাড়া: স্যাম্পল ইন্টারফেস ট্রাফিক থেকে অনুমান", +"if.capture_only": "ইন্টারফেস মিলান sFlow, NetFlow ও IPFIX ডিভাইসের ফ্লোকে ইন্টারফেস কাউন্টারের সঙ্গে মেলায়। এই মেশিনের নেটওয়ার্ক কার্ডে লোকাল ক্যাপচারে ডিভাইসের ইন্টারফেস বা কাউন্টার থাকে না, তাই এখানে মেলানোর কিছু নেই।" } diff --git a/internal/web/static/i18n/en.json b/internal/web/static/i18n/en.json index 5886d61..dcee5b2 100644 --- a/internal/web/static/i18n/en.json +++ b/internal/web/static/i18n/en.json @@ -378,7 +378,7 @@ "nav.sandbox": "Offline pcap analysis", "sb.title": "Capture files", "sb.sub": "Analyse pcap files apart from the live data", -"sb.explain": "Upload packet captures from Wireshark or tcpdump. They go into a separate database: the live data, its numbers and findings are not touched. Analyse shows the files on every page (overview, Top 66, findings, flow paths, map, flow records); Delete removes the files and their data.", +"sb.explain": "Upload packet captures from Wireshark or tcpdump. Each goes into a database of its own: the live data, its numbers and findings are not touched. Analyse shows that one file on every page (overview, Top 66, findings, flow paths, map, flow records); switch files in the bar at the top. Delete removes a file and its data.", "sb.col_file": "File", "sb.col_size": "Size", "sb.col_packets": "Packets", @@ -514,5 +514,6 @@ "if.peer_note": "Peer interface: not sampled, only the traffic through the sampled interface of this device", "src.sampled_if": "Sampled on: {v}", "src.dir_yes": "with flowDirection (61)", -"src.dir_no": "without flowDirection (61): sampled interface inferred from traffic" +"src.dir_no": "without flowDirection (61): sampled interface inferred from traffic", +"if.capture_only": "Interface check compares flows with the interface counters of sFlow, NetFlow and IPFIX devices. Local capture on this machine's network card has no device interfaces or counters, so there is nothing to compare here." } diff --git a/internal/web/static/i18n/es.json b/internal/web/static/i18n/es.json index 383ecec..c265b6a 100644 --- a/internal/web/static/i18n/es.json +++ b/internal/web/static/i18n/es.json @@ -378,7 +378,7 @@ "nav.sandbox": "Análisis offline de pcap", "sb.title": "Archivos de captura", "sb.sub": "Analizar archivos pcap aparte de los datos en vivo", -"sb.explain": "Suba capturas de Wireshark o tcpdump. Van a una base de datos aparte: los datos en vivo, sus cifras y hallazgos no se tocan. Analizar muestra los archivos en todas las páginas (resumen, Top 66, hallazgos, rutas, mapa, registros); Eliminar borra los archivos y sus datos.", +"sb.explain": "Sube capturas de Wireshark o tcpdump. Cada una va a su propia base de datos: los datos en vivo, sus cifras y hallazgos no se tocan. Analizar muestra ese archivo en todas las páginas (resumen, Top 66, hallazgos, rutas, mapa, registros de flujo); cambia de archivo en la barra superior. Eliminar borra un archivo y sus datos.", "sb.col_file": "Archivo", "sb.col_size": "Tamaño", "sb.col_packets": "Paquetes", @@ -514,5 +514,6 @@ "if.peer_note": "Interfaz del otro extremo: sin muestreo, solo el tráfico por la interfaz muestreada de este equipo", "src.sampled_if": "Muestreo en: {v}", "src.dir_yes": "con flowDirection (61)", -"src.dir_no": "sin flowDirection (61): interfaz muestreada deducida del tráfico" +"src.dir_no": "sin flowDirection (61): interfaz muestreada deducida del tráfico", +"if.capture_only": "La verificación de interfaces compara los flujos con los contadores de interfaz de equipos sFlow, NetFlow e IPFIX. La captura local en la tarjeta de red de este equipo no tiene interfaces ni contadores de dispositivo, así que aquí no hay nada que comparar." } diff --git a/internal/web/static/i18n/fr.json b/internal/web/static/i18n/fr.json index 153825e..abf3f22 100644 --- a/internal/web/static/i18n/fr.json +++ b/internal/web/static/i18n/fr.json @@ -378,7 +378,7 @@ "nav.sandbox": "Analyse hors ligne de pcap", "sb.title": "Fichiers de capture", "sb.sub": "Analyser des fichiers pcap à part des données en direct", -"sb.explain": "Importez des captures Wireshark ou tcpdump. Elles vont dans une base séparée : les données en direct, leurs chiffres et détections ne sont pas touchés. Analyser affiche les fichiers sur toutes les pages (vue d'ensemble, Top 66, détections, chemins, carte, enregistrements) ; Supprimer efface les fichiers et leurs données.", +"sb.explain": "Téléversez des captures Wireshark ou tcpdump. Chacune va dans sa propre base : les données en direct, leurs chiffres et détections ne sont pas touchés. Analyser affiche ce seul fichier sur toutes les pages (vue d’ensemble, Top 66, détections, chemins, carte, enregistrements de flux) ; changez de fichier dans la barre du haut. Supprimer efface un fichier et ses données.", "sb.col_file": "Fichier", "sb.col_size": "Taille", "sb.col_packets": "Paquets", @@ -514,5 +514,6 @@ "if.peer_note": "Interface d’en face : non échantillonnée, seulement le trafic passé par l’interface échantillonnée de cet équipement", "src.sampled_if": "Échantillonné sur : {v}", "src.dir_yes": "avec flowDirection (61)", -"src.dir_no": "sans flowDirection (61) : interface échantillonnée déduite du trafic" +"src.dir_no": "sans flowDirection (61) : interface échantillonnée déduite du trafic", +"if.capture_only": "Le contrôle des interfaces compare les flux aux compteurs d’interface des équipements sFlow, NetFlow et IPFIX. La capture locale sur la carte réseau de cette machine n’a ni interfaces ni compteurs d’équipement : il n’y a rien à comparer ici." } diff --git a/internal/web/static/i18n/hi.json b/internal/web/static/i18n/hi.json index 35c8467..0696fa9 100644 --- a/internal/web/static/i18n/hi.json +++ b/internal/web/static/i18n/hi.json @@ -378,7 +378,7 @@ "nav.sandbox": "ऑफ़लाइन pcap विश्लेषण", "sb.title": "कैप्चर फ़ाइलें", "sb.sub": "लाइव डेटा से अलग pcap फ़ाइलों का विश्लेषण", -"sb.explain": "Wireshark या tcpdump की कैप्चर फ़ाइलें अपलोड करें। वे अलग डेटाबेस में जाती हैं: लाइव डेटा, उसके आँकड़े और निष्कर्ष नहीं बदलते। “विश्लेषण करें” सभी पेजों (अवलोकन, Top 66, निष्कर्ष, फ़्लो पथ, मानचित्र, फ़्लो रिकॉर्ड) पर फ़ाइलें दिखाता है; “हटाएँ” फ़ाइलें और उनका डेटा मिटा देता है।", +"sb.explain": "Wireshark या tcpdump की कैप्चर फ़ाइलें अपलोड करें। हर फ़ाइल अपने अलग डेटाबेस में जाती है: लाइव डेटा, उसके आँकड़े और पहचानें प्रभावित नहीं होतीं। किसी फ़ाइल का ‘विश्लेषण करें’ दबाने पर हर पेज (अवलोकन, Top 66, पहचानें, फ़्लो पथ, नक्शा, फ़्लो रिकॉर्ड) सिर्फ़ वही फ़ाइल दिखाता है; ऊपर की पट्टी में फ़ाइल बदलें। ‘हटाएँ’ फ़ाइल और उसका डेटा हटाता है।", "sb.col_file": "फ़ाइल", "sb.col_size": "आकार", "sb.col_packets": "पैकेट", @@ -514,5 +514,6 @@ "if.peer_note": "सामने वाला इंटरफ़ेस: सैंपल नहीं, केवल इस डिवाइस के सैंपल किए इंटरफ़ेस से गुज़रा ट्रैफ़िक", "src.sampled_if": "सैंपल इंटरफ़ेस: {v}", "src.dir_yes": "flowDirection (61) सहित", -"src.dir_no": "flowDirection (61) के बिना: सैंपल इंटरफ़ेस ट्रैफ़िक से अनुमानित" +"src.dir_no": "flowDirection (61) के बिना: सैंपल इंटरफ़ेस ट्रैफ़िक से अनुमानित", +"if.capture_only": "इंटरफ़ेस मिलान sFlow, NetFlow और IPFIX डिवाइसों के फ़्लो की तुलना इंटरफ़ेस काउंटरों से करता है। इस मशीन के नेटवर्क कार्ड पर लोकल कैप्चर में डिवाइस इंटरफ़ेस या काउंटर नहीं होते, इसलिए यहाँ तुलना के लिए कुछ नहीं है।" } diff --git a/internal/web/static/i18n/id.json b/internal/web/static/i18n/id.json index da6a74c..62d816b 100644 --- a/internal/web/static/i18n/id.json +++ b/internal/web/static/i18n/id.json @@ -378,7 +378,7 @@ "nav.sandbox": "Analisis pcap offline", "sb.title": "File tangkapan", "sb.sub": "Analisis file pcap terpisah dari data langsung", -"sb.explain": "Unggah tangkapan paket dari Wireshark atau tcpdump. File masuk ke basis data terpisah: data langsung, angkanya, dan temuannya tidak tersentuh. Analisis menampilkan file di semua halaman (ringkasan, Top 66, temuan, jalur, peta, catatan flow); Hapus menghapus file dan datanya.", +"sb.explain": "Unggah file tangkapan dari Wireshark atau tcpdump. Masing-masing masuk ke basis data sendiri: data langsung, angka dan temuannya tidak tersentuh. Analisis menampilkan satu file itu di semua halaman (ringkasan, Top 66, temuan, jalur aliran, peta, catatan aliran); ganti file di bilah atas. Hapus menghapus file beserta datanya.", "sb.col_file": "File", "sb.col_size": "Ukuran", "sb.col_packets": "Paket", @@ -514,5 +514,6 @@ "if.peer_note": "Antarmuka lawan: tidak disampel, hanya trafik lewat antarmuka yang disampel di perangkat ini", "src.sampled_if": "Disampel di: {v}", "src.dir_yes": "dengan flowDirection (61)", -"src.dir_no": "tanpa flowDirection (61): antarmuka yang disampel ditebak dari trafik" +"src.dir_no": "tanpa flowDirection (61): antarmuka yang disampel ditebak dari trafik", +"if.capture_only": "Pencocokan antarmuka membandingkan aliran dengan penghitung antarmuka perangkat sFlow, NetFlow, dan IPFIX. Tangkapan lokal di kartu jaringan mesin ini tidak punya antarmuka atau penghitung perangkat, jadi tidak ada yang dibandingkan di sini." } diff --git a/internal/web/static/i18n/ja.json b/internal/web/static/i18n/ja.json index 8485b91..5db1f1c 100644 --- a/internal/web/static/i18n/ja.json +++ b/internal/web/static/i18n/ja.json @@ -378,7 +378,7 @@ "nav.sandbox": "オフライン pcap 分析", "sb.title": "キャプチャファイル", "sb.sub": "pcap ファイルをライブデータと分けて分析", -"sb.explain": "Wireshark や tcpdump のキャプチャファイルをアップロードします。別のデータベースに入るため、ライブデータや集計、検出結果には影響しません。「分析」を押すと全ページ(概要、Top 66、検出、フローパス、地図、フローレコード)にファイルの内容が表示されます。「削除」でファイルとデータを消します。", +"sb.explain": "Wireshark や tcpdump のキャプチャファイルをアップロードします。ファイルごとに専用のデータベースに入り、ライブデータや統計、検知には影響しません。ファイルの「分析」を押すと、すべてのページ(概要、上位 66、検知、フロー経路、地図、フローレコード)がそのファイルだけを表示します。上部のバーでファイルを切り替えられます。「削除」でファイルとそのデータを削除します。", "sb.col_file": "ファイル", "sb.col_size": "サイズ", "sb.col_packets": "パケット", @@ -514,5 +514,6 @@ "if.peer_note": "対向インターフェース:未サンプリング。このデバイスのサンプリング対象を通った分のみ", "src.sampled_if": "サンプリング対象:{v}", "src.dir_yes": "方向フィールド(61)あり", -"src.dir_no": "方向フィールド(61)なし:サンプリング対象はトラフィックから推定" +"src.dir_no": "方向フィールド(61)なし:サンプリング対象はトラフィックから推定", +"if.capture_only": "インターフェース照合は sFlow・NetFlow・IPFIX 機器のフローとインターフェースカウンターを比べます。このマシンの NIC でのローカルキャプチャには機器のインターフェースもカウンターもないため、ここで比べるものはありません。" } diff --git a/internal/web/static/i18n/ko.json b/internal/web/static/i18n/ko.json index 90763f5..d79a0f7 100644 --- a/internal/web/static/i18n/ko.json +++ b/internal/web/static/i18n/ko.json @@ -378,7 +378,7 @@ "nav.sandbox": "오프라인 pcap 분석", "sb.title": "캡처 파일", "sb.sub": "실시간 데이터와 따로 pcap 파일 분석", -"sb.explain": "Wireshark나 tcpdump 캡처 파일을 올립니다. 별도 데이터베이스에 들어가므로 실시간 데이터, 집계, 탐지 결과에는 영향이 없습니다. '분석'을 누르면 모든 페이지(개요, Top 66, 탐지, 플로 경로, 지도, 플로 레코드)에 파일 내용이 나오고, '삭제'는 파일과 데이터를 지웁니다.", +"sb.explain": "Wireshark나 tcpdump의 캡처 파일을 올립니다. 파일마다 별도 데이터베이스에 들어가며 실시간 데이터, 통계, 탐지에는 영향이 없습니다. 파일의 '분석'을 누르면 모든 페이지(개요, Top 66, 탐지, 흐름 경로, 지도, 플로 레코드)가 그 파일만 보여 줍니다. 위쪽 막대에서 파일을 바꿀 수 있습니다. '삭제'는 파일과 데이터를 지웁니다.", "sb.col_file": "파일", "sb.col_size": "크기", "sb.col_packets": "패킷", @@ -514,5 +514,6 @@ "if.peer_note": "상대 인터페이스: 샘플링 안 됨, 이 장비의 샘플링 인터페이스를 거친 트래픽만", "src.sampled_if": "샘플링 인터페이스: {v}", "src.dir_yes": "방향 필드(61) 있음", -"src.dir_no": "방향 필드(61) 없음: 샘플링 인터페이스를 트래픽으로 추정" +"src.dir_no": "방향 필드(61) 없음: 샘플링 인터페이스를 트래픽으로 추정", +"if.capture_only": "인터페이스 대조는 sFlow, NetFlow, IPFIX 장비의 플로와 인터페이스 카운터를 비교합니다. 이 컴퓨터 네트워크 카드의 로컬 캡처에는 장비 인터페이스와 카운터가 없어 여기서 비교할 것이 없습니다." } diff --git a/internal/web/static/i18n/pt.json b/internal/web/static/i18n/pt.json index 81bfb89..2ef9731 100644 --- a/internal/web/static/i18n/pt.json +++ b/internal/web/static/i18n/pt.json @@ -378,7 +378,7 @@ "nav.sandbox": "Análise offline de pcap", "sb.title": "Arquivos de captura", "sb.sub": "Analisar arquivos pcap separados dos dados ao vivo", -"sb.explain": "Envie capturas do Wireshark ou tcpdump. Elas vão para um banco separado: os dados ao vivo, seus números e detecções não são afetados. Analisar mostra os arquivos em todas as páginas (visão geral, Top 66, detecções, caminhos, mapa, registros); Excluir apaga os arquivos e seus dados.", +"sb.explain": "Envie capturas do Wireshark ou tcpdump. Cada uma vai para um banco de dados próprio: os dados ao vivo, seus números e detecções não são afetados. Analisar mostra esse arquivo em todas as páginas (visão geral, Top 66, detecções, caminhos, mapa, registros de fluxo); troque de arquivo na barra superior. Excluir apaga um arquivo e seus dados.", "sb.col_file": "Arquivo", "sb.col_size": "Tamanho", "sb.col_packets": "Pacotes", @@ -514,5 +514,6 @@ "if.peer_note": "Interface do outro lado: sem amostragem, só o tráfego pela interface amostrada deste equipamento", "src.sampled_if": "Amostragem em: {v}", "src.dir_yes": "com flowDirection (61)", -"src.dir_no": "sem flowDirection (61): interface amostrada deduzida do tráfego" +"src.dir_no": "sem flowDirection (61): interface amostrada deduzida do tráfego", +"if.capture_only": "A conferência de interfaces compara os fluxos com os contadores de interface de equipamentos sFlow, NetFlow e IPFIX. A captura local na placa de rede desta máquina não tem interfaces nem contadores de equipamento, então não há o que comparar aqui." } diff --git a/internal/web/static/i18n/ru.json b/internal/web/static/i18n/ru.json index 78cad1f..bca26c4 100644 --- a/internal/web/static/i18n/ru.json +++ b/internal/web/static/i18n/ru.json @@ -378,7 +378,7 @@ "nav.sandbox": "Офлайн-анализ pcap", "sb.title": "Файлы захвата", "sb.sub": "Анализ pcap-файлов отдельно от живых данных", -"sb.explain": "Загрузите захваты Wireshark или tcpdump. Они попадают в отдельную базу: живые данные, их статистика и находки не затрагиваются. «Анализ» показывает файлы на всех страницах (обзор, Top 66, находки, пути, карта, записи потоков); «Удалить» стирает файлы и их данные.", +"sb.explain": "Загрузите захваты Wireshark или tcpdump. Каждый попадает в собственную базу: живые данные, их цифры и обнаружения не затрагиваются. «Анализ» показывает этот файл на всех страницах (обзор, Топ 66, обнаружения, пути, карта, записи потоков); переключайте файлы в полосе сверху. «Удалить» удаляет файл и его данные.", "sb.col_file": "Файл", "sb.col_size": "Размер", "sb.col_packets": "Пакеты", @@ -514,5 +514,6 @@ "if.peer_note": "Встречный интерфейс: без выборки, только трафик через интерфейс с выборкой этого устройства", "src.sampled_if": "Выборка на: {v}", "src.dir_yes": "с flowDirection (61)", -"src.dir_no": "без flowDirection (61): интерфейс с выборкой определён по трафику" +"src.dir_no": "без flowDirection (61): интерфейс с выборкой определён по трафику", +"if.capture_only": "Сверка интерфейсов сравнивает потоки со счётчиками интерфейсов устройств sFlow, NetFlow и IPFIX. У локального захвата с сетевой карты этой машины нет интерфейсов и счётчиков устройства, поэтому сравнивать здесь нечего." } diff --git a/internal/web/static/i18n/ur.json b/internal/web/static/i18n/ur.json index 530f1f0..8426102 100644 --- a/internal/web/static/i18n/ur.json +++ b/internal/web/static/i18n/ur.json @@ -378,7 +378,7 @@ "nav.sandbox": "آف لائن pcap تجزیہ", "sb.title": "کیپچر فائلیں", "sb.sub": "لائیو ڈیٹا سے الگ pcap فائلوں کا تجزیہ", -"sb.explain": "Wireshark یا tcpdump کی کیپچر فائلیں اپ لوڈ کریں۔ یہ الگ ڈیٹابیس میں جاتی ہیں: لائیو ڈیٹا، اس کے اعداد اور نتائج متاثر نہیں ہوتے۔ «تجزیہ کریں» تمام صفحات (جائزہ، Top 66، نتائج، فلو راستے، نقشہ، فلو ریکارڈز) پر فائلیں دکھاتا ہے؛ «حذف کریں» فائلیں اور ان کا ڈیٹا مٹا دیتا ہے۔", +"sb.explain": "Wireshark یا tcpdump کی کیپچر فائلیں اپ لوڈ کریں۔ ہر فائل اپنے الگ ڈیٹا بیس میں جاتی ہے: لائیو ڈیٹا، اس کے اعداد اور نشاندہیاں متاثر نہیں ہوتیں۔ کسی فائل کا ”تجزیہ کریں“ دبانے پر ہر صفحہ (جائزہ، Top 66، نشاندہیاں، فلو راستے، نقشہ، فلو ریکارڈ) صرف وہی فائل دکھاتا ہے؛ اوپر کی پٹی میں فائل بدلیں۔ ”حذف کریں“ فائل اور اس کا ڈیٹا ہٹا دیتا ہے۔", "sb.col_file": "فائل", "sb.col_size": "سائز", "sb.col_packets": "پیکٹ", @@ -514,5 +514,6 @@ "if.peer_note": "مخالف سرے کا انٹرفیس: سیمپل نہیں، صرف اس ڈیوائس کے سیمپل والے انٹرفیس سے گزری ٹریفک", "src.sampled_if": "سیمپل انٹرفیس: {v}", "src.dir_yes": "flowDirection (61) کے ساتھ", -"src.dir_no": "flowDirection (61) کے بغیر: سیمپل انٹرفیس ٹریفک سے اخذ" +"src.dir_no": "flowDirection (61) کے بغیر: سیمپل انٹرفیس ٹریفک سے اخذ", +"if.capture_only": "انٹرفیس ملان sFlow، NetFlow اور IPFIX آلات کے فلو کا انٹرفیس کاؤنٹرز سے موازنہ کرتا ہے۔ اس مشین کے نیٹ ورک کارڈ پر لوکل کیپچر میں آلے کے انٹرفیس یا کاؤنٹر نہیں ہوتے، اس لیے یہاں موازنے کو کچھ نہیں۔" } diff --git a/internal/web/static/i18n/zh.json b/internal/web/static/i18n/zh.json index f9e0b8c..e16bc0a 100644 --- a/internal/web/static/i18n/zh.json +++ b/internal/web/static/i18n/zh.json @@ -378,7 +378,7 @@ "nav.sandbox": "离线 pcap 分析", "sb.title": "抓包文件", "sb.sub": "在实时数据之外分析 pcap 文件", -"sb.explain": "上传 Wireshark 或 tcpdump 的抓包文件。它们进入一个单独的数据库,不影响实时数据、统计和发现。点“分析”后,所有页面(概览、Top 66、发现、流向、地图、流记录)都显示这些文件的内容;“删除”会删除文件及其数据。", +"sb.explain": "上传 Wireshark 或 tcpdump 的抓包文件。每个文件进入各自单独的数据库,不影响实时数据、统计和发现。点某个文件的“分析”,所有页面(概览、Top 66、发现、流向、地图、流记录)都只显示这一个文件;在顶部的条上可以切换文件。“删除”会删除文件及其数据。", "sb.col_file": "文件", "sb.col_size": "大小", "sb.col_packets": "包数", @@ -514,5 +514,6 @@ "if.peer_note": "对端接口:未采样,只含经过本设备采样接口的流量", "src.sampled_if": "采样接口:{v}", "src.dir_yes": "带方向字段(61)", -"src.dir_no": "不带方向字段(61),采样接口按流量推断" +"src.dir_no": "不带方向字段(61),采样接口按流量推断", +"if.capture_only": "接口对账用来对比 sFlow、NetFlow、IPFIX 设备的流量和接口计数器。本机网卡抓包没有设备接口和计数器,所以这里没有可对比的内容。" }