mirror of
https://github.com/GNS3/gns3-gui.git
synced 2026-08-24 20:27:35 +03:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c776d4ef37 | ||
|
|
38ecc9c4ed | ||
|
|
30e34073c8 | ||
|
|
a403512fa7 | ||
|
|
8a97b300ca | ||
|
|
957c4f5de0 | ||
|
|
5182bc3841 | ||
|
|
04b90c6aab | ||
|
|
424ef9d60e | ||
|
|
a4984a396a | ||
|
|
bfc1ed525c | ||
|
|
6c5f78c6cd | ||
|
|
0e46bbaf81 | ||
|
|
de1b8e0c59 | ||
|
|
5c2640d3ff | ||
|
|
0136c8e85a |
@@ -1,5 +1,14 @@
|
||||
# Change Log
|
||||
|
||||
## 2.2.61 30/07/2026
|
||||
|
||||
* No changes
|
||||
|
||||
## 2.2.60 15/07/2026
|
||||
|
||||
* feat(capture): implement live packet capture with internal tail
|
||||
* Add support for Ptyxis terminal. Fixes #3834
|
||||
|
||||
## 2.2.59 08/05/2026
|
||||
|
||||
* Remove psutil version check
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-rrequirements.txt
|
||||
|
||||
pytest==8.4.2; python_version == '3.9' # version 8.4.2 is the last one supporting Python 3.9
|
||||
pytest==9.0.2; python_version >= '3.10'
|
||||
pytest==9.0.3; python_version >= '3.10'
|
||||
pytest-timeout==2.4.0
|
||||
|
||||
@@ -50,7 +50,7 @@ class CrashReport:
|
||||
Report crash to a third party service
|
||||
"""
|
||||
|
||||
DSN = "https://dd662ce99d7e4a04714a89939ec523c9@o19455.ingest.us.sentry.io/38506"
|
||||
DSN = "https://52512702e506197edfb413c1d7e13397@o19455.ingest.us.sentry.io/38506"
|
||||
_instance = None
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -78,9 +78,14 @@ class VMware(Module):
|
||||
vmrun_path = shutil.which("vmrun")
|
||||
if vmrun_path is None:
|
||||
# look for vmrun.exe using the VMware Workstation directory listed in the registry
|
||||
vmrun_path = VMware._findVmrunRegistry(r"SOFTWARE\Wow6432Node\VMware, Inc.\VMware Workstation")
|
||||
# (native 64-bit key first; VMware Workstation 26H1+ dropped the 32-bit build)
|
||||
vmrun_path = VMware._findVmrunRegistry(r"SOFTWARE\VMware, Inc.\VMware Workstation")
|
||||
if vmrun_path is None:
|
||||
vmrun_path = VMware._findVmrunRegistry(r"SOFTWARE\Wow6432Node\VMware, Inc.\VMware Workstation")
|
||||
if vmrun_path is None:
|
||||
# look for vmrun.exe using the VIX directory listed in the registry
|
||||
vmrun_path = VMware._findVmrunRegistry(r"SOFTWARE\VMware, Inc.\VMware VIX")
|
||||
if vmrun_path is None:
|
||||
vmrun_path = VMware._findVmrunRegistry(r"SOFTWARE\Wow6432Node\VMware, Inc.\VMware VIX")
|
||||
elif sys.platform.startswith("darwin"):
|
||||
vmware_fusion_vmrun_path = None
|
||||
|
||||
@@ -25,6 +25,7 @@ from .local_config import LocalConfig
|
||||
from .settings import PACKET_CAPTURE_SETTINGS
|
||||
from .dialogs.capture_dialog import CaptureDialog
|
||||
from .topology import Topology
|
||||
from .pcap_to_wireshark import PCAPToWireshark
|
||||
|
||||
import logging
|
||||
log = logging.getLogger(__name__)
|
||||
@@ -38,6 +39,7 @@ class PacketCapture:
|
||||
def __init__(self):
|
||||
self._tail_process = {}
|
||||
self._capture_reader_process = {}
|
||||
self._capture_stream_thread = {}
|
||||
# Auto start the capture program for this link
|
||||
self._autostart = {}
|
||||
|
||||
@@ -56,6 +58,11 @@ class PacketCapture:
|
||||
self._tail_process = {}
|
||||
self._capture_reader_process = {}
|
||||
|
||||
for thread in list(self._capture_stream_thread.values()):
|
||||
thread.stop()
|
||||
thread.wait()
|
||||
self._capture_stream_thread = {}
|
||||
|
||||
def topology(self):
|
||||
from .topology import Topology
|
||||
return Topology.instance()
|
||||
@@ -92,7 +99,7 @@ class PacketCapture:
|
||||
link = self.topology().getLink(link_id)
|
||||
if link:
|
||||
if link.capturing():
|
||||
if self._autostart.get(link) and link not in self._tail_process:
|
||||
if self._autostart.get(link) and (link not in self._tail_process and link not in self._capture_stream_thread):
|
||||
log.debug("Starting packet capture reader for link {}".format(link.link_id()))
|
||||
self.startPacketCaptureReader(link)
|
||||
else:
|
||||
@@ -128,6 +135,13 @@ class PacketCapture:
|
||||
pass
|
||||
del self._tail_process[link]
|
||||
|
||||
if link in self._capture_stream_thread:
|
||||
log.debug("Stopping packet capture stream thread for link {}".format(link.link_id()))
|
||||
self._capture_stream_thread[link].stop()
|
||||
self._capture_stream_thread[link].wait()
|
||||
self._capture_stream_thread[link].error_signal.disconnect()
|
||||
del self._capture_stream_thread[link]
|
||||
|
||||
def startPacketCaptureAnalyzer(self, link):
|
||||
"""
|
||||
Starts the packet capture analyzer
|
||||
@@ -207,6 +221,12 @@ class PacketCapture:
|
||||
except (PermissionError, OSError):
|
||||
pass
|
||||
del self._capture_reader_process[link]
|
||||
if link in self._capture_stream_thread:
|
||||
log.debug("Stopping packet capture stream thread for link {}".format(link.link_id()))
|
||||
self._capture_stream_thread[link].stop()
|
||||
self._capture_stream_thread[link].wait()
|
||||
self._capture_stream_thread[link].error_signal.disconnect()
|
||||
del self._capture_stream_thread[link]
|
||||
|
||||
# PCAP capture file path
|
||||
command = command.replace("%c", '"' + capture_file_path + '"')
|
||||
@@ -227,6 +247,15 @@ class PacketCapture:
|
||||
if "|" in command:
|
||||
# live traffic capture (using tail)
|
||||
command1, command2 = command.split("|", 1)
|
||||
|
||||
if '<internal_tail>' in command1:
|
||||
# Start the background capture streaming thread if using the internal tail implementation
|
||||
log.debug("Starting background capture streaming thread for link {}".format(link.link_id()))
|
||||
self._capture_stream_thread[link] = PCAPToWireshark(capture_file_path, command2)
|
||||
self._capture_stream_thread[link].error_signal.connect(lambda msg: QtWidgets.QMessageBox.critical(self.parent(), "Packet capture", msg))
|
||||
self._capture_stream_thread[link].start()
|
||||
return
|
||||
|
||||
info = None
|
||||
if sys.platform.startswith("win"):
|
||||
# hide tail window on Windows
|
||||
|
||||
79
gns3/pcap_to_wireshark.py
Normal file
79
gns3/pcap_to_wireshark.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Copyright (C) 2026 GNS3 Technologies Inc.
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import os
|
||||
import time
|
||||
import shlex
|
||||
import subprocess
|
||||
|
||||
from .qt import QtCore
|
||||
|
||||
class PCAPToWireshark(QtCore.QThread):
|
||||
error_signal = QtCore.pyqtSignal(str)
|
||||
|
||||
def __init__(self, pcap_path, wireshark_cmd):
|
||||
|
||||
super().__init__()
|
||||
self._pcap_path = pcap_path
|
||||
self._wireshark_cmd = wireshark_cmd
|
||||
self._running = True
|
||||
self._wireshark_proc = None
|
||||
|
||||
def run(self):
|
||||
|
||||
if not os.path.exists(self._pcap_path):
|
||||
self.error_signal.emit(f"Error: {self._pcap_path} not found.")
|
||||
return
|
||||
|
||||
try:
|
||||
wireshark_cmd = shlex.split(self._wireshark_cmd)
|
||||
except ValueError as e:
|
||||
self.error_signal.emit(f"Invalid Wireshark command {self._wireshark_cmd}: {str(e)}")
|
||||
return
|
||||
|
||||
try:
|
||||
self._wireshark_proc = subprocess.Popen(
|
||||
wireshark_cmd,
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
except FileNotFoundError:
|
||||
self.error_signal.emit("Error: Wireshark not found in $PATH")
|
||||
return
|
||||
|
||||
with open(self._pcap_path, 'rb') as f:
|
||||
while self._running and self._wireshark_proc.poll() is None:
|
||||
chunk = f.read(4096)
|
||||
if chunk:
|
||||
try:
|
||||
self._wireshark_proc.stdin.write(chunk)
|
||||
self._wireshark_proc.stdin.flush()
|
||||
except BrokenPipeError:
|
||||
break # Wireshark has been closed
|
||||
else:
|
||||
time.sleep(0.1)
|
||||
|
||||
def stop(self):
|
||||
self._running = False
|
||||
if self._wireshark_proc:
|
||||
try:
|
||||
self._wireshark_proc.stdin.close()
|
||||
self._wireshark_proc.kill()
|
||||
except Exception as e:
|
||||
print(e)
|
||||
pass
|
||||
@@ -600,7 +600,15 @@ class Project(QtCore.QObject):
|
||||
Delete the project from all servers
|
||||
"""
|
||||
self.project_about_to_close_signal.emit()
|
||||
Controller.instance().delete("/projects/{project_id}".format(project_id=self._id), self._projectClosedCallback, progressText="Delete the project")
|
||||
Controller.instance().delete("/projects/{project_id}".format(project_id=self._id), self._projectDestroyedCallback, progressText="Delete the project")
|
||||
|
||||
def _projectDestroyedCallback(self, result, error=False, server=None, **kwargs):
|
||||
self._projectClosedCallback(result, error=error, server=server, **kwargs)
|
||||
if not error or ("status" in result and result["status"] == 404):
|
||||
# Refresh the controller's project list so a deleted project disappears
|
||||
# immediately from menus such as "recent projects" instead of only
|
||||
# after restarting the GUI.
|
||||
Controller.instance().refreshProjectList()
|
||||
|
||||
def _projectClosedCallback(self, result, error=False, server=None, **kwargs):
|
||||
|
||||
|
||||
@@ -154,6 +154,7 @@ elif sys.platform.startswith("darwin"):
|
||||
else:
|
||||
PRECONFIGURED_TELNET_CONSOLE_COMMANDS = {'Xterm': 'xterm -T "{name}" -e "telnet {host} {port}"',
|
||||
'Putty': 'putty -telnet {host} {port} -title "{name}" -sl 2500 -fg SALMON1 -bg BLACK',
|
||||
'Ptyxis': 'ptyxis --tab --title="{name}" -- telnet {host} {port}',
|
||||
'Gnome Terminal': 'gnome-terminal --tab -t "{name}" -- telnet {host} {port}',
|
||||
'Xfce4 Terminal': 'xfce4-terminal --tab -T "{name}" -e "telnet {host} {port}"',
|
||||
'ROXTerm': 'roxterm -n "{name}" --tab -e "telnet {host} {port}"',
|
||||
@@ -172,6 +173,8 @@ else:
|
||||
if distro_name == "Debian" or distro_name == "Ubuntu" or distro_name == "Linux Mint":
|
||||
if shutil.which("mate-terminal"):
|
||||
DEFAULT_TELNET_CONSOLE_COMMAND = PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Mate Terminal"]
|
||||
elif shutil.which("ptyxis"):
|
||||
DEFAULT_TELNET_CONSOLE_COMMAND = PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Ptyxis"]
|
||||
else:
|
||||
DEFAULT_TELNET_CONSOLE_COMMAND = PRECONFIGURED_TELNET_CONSOLE_COMMANDS["Gnome Terminal"]
|
||||
|
||||
@@ -245,30 +248,40 @@ else:
|
||||
# Pre-configured packet capture reader commands on various OSes
|
||||
WIRESHARK_NORMAL_CAPTURE = "Wireshark Traditional Capture"
|
||||
WIRESHARK_LIVE_TRAFFIC_CAPTURE = "Wireshark Live Traffic Capture"
|
||||
WIRESHARK_LIVE_TRAFFIC_CAPTURE_INTERNAL = "Wireshark Live Traffic Capture (with internal tail)"
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
PRECONFIGURED_PACKET_CAPTURE_READER_COMMANDS = {WIRESHARK_NORMAL_CAPTURE: r'{}\Wireshark\wireshark.exe {{pcap_file}} --capture-comment "{{project}} {{link_description}}"'.format(program_files),
|
||||
WIRESHARK_LIVE_TRAFFIC_CAPTURE: r'tail.exe -f -c +0b {{pcap_file}} | "{}\Wireshark\wireshark.exe" --capture-comment "{{project}} {{link_description}}" -o "gui.window_title:{{link_description}}" -k -i -'.format(program_files)}
|
||||
WIRESHARK_LIVE_TRAFFIC_CAPTURE: r'tail.exe -f -c +0b {{pcap_file}} | "{}\Wireshark\wireshark.exe" --capture-comment "{{project}} {{link_description}}" -o "gui.window_title:{{link_description}}" -k -i -'.format(program_files),
|
||||
WIRESHARK_LIVE_TRAFFIC_CAPTURE_INTERNAL: r'<internal_tail> {{pcap_file}} | "{}\Wireshark\wireshark.exe" --capture-comment "{{project}} {{link_description}}" -o "gui.window_title:{{link_description}}" -k -i -'.format(program_files)}
|
||||
|
||||
elif sys.platform.startswith("darwin"):
|
||||
# Mac OS X
|
||||
PRECONFIGURED_PACKET_CAPTURE_READER_COMMANDS = {WIRESHARK_NORMAL_CAPTURE: '/usr/bin/open -a /Applications/Wireshark.app {pcap_file} --capture-comment {project} {link_description}"',
|
||||
WIRESHARK_LIVE_TRAFFIC_CAPTURE: 'tail -f -c +0 {pcap_file} | /Applications/Wireshark.app/Contents/MacOS/Wireshark --capture-comment "{project} {link_description}" -o "gui.window_title:{link_description}" -k -i -'}
|
||||
WIRESHARK_LIVE_TRAFFIC_CAPTURE: 'tail -f -c +0 {pcap_file} | /Applications/Wireshark.app/Contents/MacOS/Wireshark --capture-comment "{project} {link_description}" -o "gui.window_title:{link_description}" -k -i -',
|
||||
WIRESHARK_LIVE_TRAFFIC_CAPTURE_INTERNAL: '<internal_tail> {pcap_file} | /Applications/Wireshark.app/Contents/MacOS/Wireshark --capture-comment "{project} {link_description}" -o "gui.window_title:{link_description}" -k -i -'}
|
||||
|
||||
elif sys.platform.startswith("freebsd"):
|
||||
# FreeBSD
|
||||
PRECONFIGURED_PACKET_CAPTURE_READER_COMMANDS = {WIRESHARK_NORMAL_CAPTURE: 'wireshark {pcap_file} --capture-comment "{project} {link_description}"',
|
||||
WIRESHARK_LIVE_TRAFFIC_CAPTURE: 'gtail -f -c +0b {pcap_file} | wireshark --capture-comment "{project} {link_description}" -o "gui.window_title:{link_description}" -k -i -'}
|
||||
WIRESHARK_LIVE_TRAFFIC_CAPTURE: 'gtail -f -c +0b {pcap_file} | wireshark --capture-comment "{project} {link_description}" -o "gui.window_title:{link_description}" -k -i -',
|
||||
WIRESHARK_LIVE_TRAFFIC_CAPTURE_INTERNAL: '<internal_tail> {pcap_file} | wireshark --capture-comment "{project} {link_description}" -o "gui.window_title:{link_description}" -k -i -'}
|
||||
|
||||
elif sys.platform.startswith("openbsd"):
|
||||
# OpenBSD
|
||||
PRECONFIGURED_PACKET_CAPTURE_READER_COMMANDS = {WIRESHARK_NORMAL_CAPTURE: 'wireshark {pcap_file} --capture-comment "{project} {link_description}"',
|
||||
WIRESHARK_LIVE_TRAFFIC_CAPTURE: 'tail -f -c +0 {pcap_file} | wireshark --capture-comment "{project} {link_description}" -o "gui.window_title:{link_description}" -k -i -'}
|
||||
WIRESHARK_LIVE_TRAFFIC_CAPTURE: 'tail -f -c +0 {pcap_file} | wireshark --capture-comment "{project} {link_description}" -o "gui.window_title:{link_description}" -k -i -',
|
||||
WIRESHARK_LIVE_TRAFFIC_CAPTURE_INTERNAL: '<internal_tail> {pcap_file} | wireshark --capture-comment "{project} {link_description}" -o "gui.window_title:{link_description}" -k -i -'}
|
||||
else:
|
||||
PRECONFIGURED_PACKET_CAPTURE_READER_COMMANDS = {WIRESHARK_NORMAL_CAPTURE: 'wireshark {pcap_file} --capture-comment "{project} {link_description}"',
|
||||
WIRESHARK_LIVE_TRAFFIC_CAPTURE: 'tail -f -c +0b {pcap_file} | wireshark --capture-comment "{project} {link_description}" -o "gui.window_title:{link_description}" -k -i -'}
|
||||
WIRESHARK_LIVE_TRAFFIC_CAPTURE: 'tail -f -c +0b {pcap_file} | wireshark --capture-comment "{project} {link_description}" -o "gui.window_title:{link_description}" -k -i -',
|
||||
WIRESHARK_LIVE_TRAFFIC_CAPTURE_INTERNAL: '<internal_tail> {pcap_file} | wireshark --capture-comment "{project} {link_description}" -o "gui.window_title:{link_description}" -k -i -'}
|
||||
|
||||
DEFAULT_PACKET_CAPTURE_READER_COMMAND = PRECONFIGURED_PACKET_CAPTURE_READER_COMMANDS[WIRESHARK_LIVE_TRAFFIC_CAPTURE]
|
||||
if sys.platform.startswith("linux"):
|
||||
# only use the internal live traffic capture version on Linux by default
|
||||
DEFAULT_PACKET_CAPTURE_READER_COMMAND = PRECONFIGURED_PACKET_CAPTURE_READER_COMMANDS[WIRESHARK_LIVE_TRAFFIC_CAPTURE_INTERNAL]
|
||||
else:
|
||||
DEFAULT_PACKET_CAPTURE_READER_COMMAND = PRECONFIGURED_PACKET_CAPTURE_READER_COMMANDS[WIRESHARK_LIVE_TRAFFIC_CAPTURE]
|
||||
|
||||
DEFAULT_PACKET_CAPTURE_ANALYZER_COMMAND = ""
|
||||
if sys.platform.startswith("win"):
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
# or negative for a release candidate or beta (after the base version
|
||||
# number has been incremented)
|
||||
|
||||
__version__ = "2.2.60.dev1"
|
||||
__version_info__ = (2, 2, 60, 99)
|
||||
__version__ = "2.2.61"
|
||||
__version_info__ = (2, 2, 61, 0)
|
||||
|
||||
if "dev" in __version__:
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user