diff --git a/docs/development-setup.md b/docs/development-setup.md index b6ae3a42c..17de34965 100644 --- a/docs/development-setup.md +++ b/docs/development-setup.md @@ -103,7 +103,7 @@ source venv/bin/activate # For China mainland users, use mirror: # pip install -e . -i https://mirrors.aliyun.com/pypi/simple/ -pip install -e . && gns3-wireshark-setup +pip install -e . && wireshark pip install -e .[ai-copilot] pip install -e .[dev] ``` diff --git a/docs/features/web-wireshark-business-process.md b/docs/features/web-wireshark-business-process.md index 338eec3ee..6ef611345 100644 --- a/docs/features/web-wireshark-business-process.md +++ b/docs/features/web-wireshark-business-process.md @@ -27,10 +27,10 @@ Before using Web Wireshark, install the GNS3 server and set up the Docker image: ```bash # Development install -pip install -e . && gns3-wireshark-setup +pip install -e . && wireshark # Production install -pip install gns3-server && gns3-wireshark-setup +pip install gns3-server && wireshark ``` This command will: @@ -38,7 +38,7 @@ This command will: 2. Pull the `gns3/web-wireshark:latest` image from Docker Hub 3. If pull fails, build the image locally using the included Dockerfile -The `gns3-wireshark-setup` command shows the raw output from `docker pull` or `docker build`, allowing you to see the full progress. +The `wireshark` command shows the raw output from `docker pull` or `docker build`, allowing you to see the full progress. --- @@ -552,6 +552,43 @@ Configured via `WebWiresharkSettings` in `gns3server/schemas/config.py`: --- +## Docker API Compatibility + +### Version Negotiation Mechanism + +The Web Wireshark feature implements automatic Docker API version negotiation to ensure compatibility across different Docker versions: + +1. **Primary**: Try API version 1.44 first + - Supports Docker 29.3+ (API 1.54+), which requires minimum API 1.44 + +2. **Fallback**: If server rejects 1.44 with 400 error, downgrade to API 1.40 + - Supports Docker 20.10 (API 1.41) + +### Tested Configurations + +| Docker Version | API Version | API 1.40 | API 1.44 | Solution | +|----------------|-------------|----------|----------|----------| +| 20.10 | 1.41 | ✓ | ✗ | Fallback to 1.40 | +| 29.3+ | 1.54+ | ✗ | ✓ | Use 1.44 | + +### Container IP Retrieval + +The `get_container_ip()` method implements a dual-strategy approach for retrieving container IP addresses: + +1. **Primary Method**: Query Docker Container API + - Uses `docker inspect` via HTTP API + - Safe access to `NetworkSettings.Networks` field using `.get()` + - Handles missing fields gracefully (fixes KeyError bug) + +2. **Fallback Method**: Execute `hostname -I` command inside container + - Works when Docker API response lacks network information + - Compatible with containers without `ip` command + - Returns first IP address from `hostname -I` output + +This dual approach ensures compatibility across different Docker API versions that may have varying response formats. + +--- + ## Known Limitations 1. **JWT Token Visibility**: Token passed via command-line arguments (visible in `/proc//cmdline`) diff --git a/gns3server/agent/web_wireshark/docker_client.py b/gns3server/agent/web_wireshark/docker_client.py index c40f7c5fe..ca9e86360 100644 --- a/gns3server/agent/web_wireshark/docker_client.py +++ b/gns3server/agent/web_wireshark/docker_client.py @@ -32,7 +32,7 @@ logger = logging.getLogger(__name__) # Docker API configuration DOCKER_SOCKET = "/var/run/docker.sock" -DOCKER_MINIMUM_API_VERSION = "1.44" +DOCKER_MINIMUM_API_VERSION = "1.40" DOCKER_PREFERRED_API_VERSION = "1.44" @@ -46,7 +46,9 @@ class DockerHTTPClient: self._connector = None self._session = None self._connected = False - self._api_version = DOCKER_MINIMUM_API_VERSION + # Start with preferred API version (1.44) to support newer Docker + # Will fallback to minimum version (1.40) if server doesn't support it + self._api_version = DOCKER_PREFERRED_API_VERSION async def _get_connector(self): """Get or create Unix socket connector.""" @@ -142,6 +144,14 @@ class DockerHTTPClient: raise RuntimeError(f"Docker API timeout after {self.REQUEST_TIMEOUT}s for {endpoint}") except aiohttp.ClientError as e: raise RuntimeError(f"Docker connection error: {e}") from e + except RuntimeError as e: + # Retry with lower API version if Docker daemon doesn't support current version + error_msg = str(e) + if ("400" in error_msg or "not found" in error_msg.lower()) and self._api_version == DOCKER_PREFERRED_API_VERSION: + logger.warning(f"Docker daemon doesn't support API version {self._api_version}, falling back to {DOCKER_MINIMUM_API_VERSION}") + self._api_version = DOCKER_MINIMUM_API_VERSION + return await self._request(method, endpoint, check_connection=False, **kwargs) + raise async def create_network(self, name: str, driver: str = "bridge", subnet: str = None): """Create Docker network.""" diff --git a/gns3server/agent/web_wireshark/manager.py b/gns3server/agent/web_wireshark/manager.py index 696226675..8fedb8f22 100644 --- a/gns3server/agent/web_wireshark/manager.py +++ b/gns3server/agent/web_wireshark/manager.py @@ -526,6 +526,50 @@ class WebWiresharkManager: return None + async def get_container_ip(self, container_name: str, container_id: str = None) -> Optional[str]: + """Get the container IP address in the wireshark network. + + Args: + container_name: Container name (e.g., "gns3-wireshark-project_id") + container_id: Container ID (optional, only used for fallback method) + + Returns: + Container IP address (e.g., 172.31.0.2) or None if not found + """ + # Method 1: Get from Docker Container API (fastest) + try: + container = await self.docker.get_container(container_name) + if container: + networks = container.get("NetworkSettings", {}).get("Networks", {}) + # Find gns3-wireshark network + for network_name, network_config in networks.items(): + if "wireshark" in network_name.lower(): + container_ip = network_config.get("IPAddress") + if container_ip: + logger.info(f"Got container IP from Docker API: {container_ip}") + return container_ip + except Exception as e: + logger.debug(f"Cannot get container IP from Docker API: {e}") + + # Fallback: Execute command inside container to get IP (slower) + if container_id: + try: + # Use 'hostname -I' to get all IP addresses and take the first one + # This works on most Linux systems and is more portable than 'ip' command + returncode, stdout, stderr = await self._exec_in_container( + container_id, + "hostname -I 2>/dev/null | awk '{print $1}'" + ) + if returncode == 0 and stdout.strip(): + container_ip = stdout.strip() + logger.info(f"Got container IP from container command: {container_ip}") + return container_ip + except Exception as e: + logger.debug(f"Cannot get container IP from container command: {e}") + + logger.warning(f"Failed to get container IP for {container_name}") + return None + async def _fix_localhost_url(self, url: str, container_id: str) -> str: """Fix URL with localhost/127.0.0.1 for container access. diff --git a/gns3server/api/routes/controller/links.py b/gns3server/api/routes/controller/links.py index 5f812fa00..2a6eec200 100644 --- a/gns3server/api/routes/controller/links.py +++ b/gns3server/api/routes/controller/links.py @@ -37,6 +37,7 @@ from gns3server.utils.http_client import HTTPClient from gns3server.utils.port_allocator import link_id_to_port from gns3server.utils.websocket_to_websocket import websocket_proxy from gns3server import schemas +from gns3server.agent.web_wireshark.manager import WebWiresharkManager from .dependencies.database import get_repository from .dependencies.rbac import has_privilege, has_privilege_on_websocket @@ -385,22 +386,8 @@ async def web_wireshark_websocket( xpra_port = link_id_to_port(link_id) # Get container IP - from gns3server.compute.docker import Docker - - docker_manager = Docker.instance() - container_info = await docker_manager.query( - "GET", - f"containers/{container_name}/json" - ) - - networks = container_info["NetworkSettings"]["Networks"] - container_ip = None - - # Find gns3-wireshark network - for network_name, network_config in networks.items(): - if "wireshark" in network_name.lower(): - container_ip = network_config["IPAddress"] - break + manager = WebWiresharkManager() + container_ip = await manager.get_container_ip(container_name) if not container_ip: log.error(f"Container {container_name} not found in wireshark network") diff --git a/pyproject.toml b/pyproject.toml index 63a48aead..3135699d7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,7 +51,7 @@ ai-copilot = {file = ['ai-requirements.txt']} gns3server = "gns3server.main:main" gns3vmnet = "gns3server.utils.vmnet:main" gns3server-uninstall-ai-copilot = "gns3server.utils.uninstall_ai_copilot:main" -gns3-wireshark-setup = "gns3server.agent.web_wireshark.setup_wireshark_image:main" +wireshark = "gns3server.agent.web_wireshark.setup_wireshark_image:main" [tool.setuptools] packages = ["gns3server"]