Both is_safe_path rejection and PermissionError were returning 403
without a detail message, making them indistinguishable in logs.
Add specific detail strings to each:
- is_safe_path: 'Path is outside the project directory'
- PermissionError (write): 'Permission denied writing to ...'
- PermissionError (delete): 'Permission denied deleting ...'
The inner try-except caught OSError/UnicodeEncodeError with 'pass',
silently swallowing all write failures and returning HTTP 204 as if
the file was written successfully.
Remove the nested try-except and let errors propagate properly:
- OSError → 500 with error detail
- PermissionError → 403 (already handled)
- FileNotFoundError → 404 (already handled)
- Stream file GET/POST through controller without buffering in memory
- Add recursive and subdirectory filtering to node file listing
- Replace file extension with magic-based file type detection
- Add DELETE endpoint for node and project files
- Include directories in listing response
- Add params and stream support to http_query
- Fix lambda closures, streamer exception scope, and delete error codes
Only description strings changed to tell AI the content is Markdown
format (.md). The actual handler still reads/writes README.txt for
Web UI compatibility.
Updated the actual MCP tool definitions in __init__.py with detailed
filter parameter descriptions. The previous update to links.py was incorrect
because MCP tools are defined via @mcp.tool() decorators in __init__.py.
Changes:
- Updated update_link tool docstring with comprehensive filter information
- Updated create_link tool filters parameter description
- Added all 5 filter types with proper array format requirements
- Added parameter ranges and usage examples
Filters now properly documented:
- frequency_drop: [N] (N: -1 to 32767)
- packet_loss: [rate] (rate: 0 to 100)
- delay: [ms, jitter] (milliseconds)
- corrupt: [rate] (rate: 0 to 100)
- bpf: [expression] (BPF syntax)
This will prevent TypeError: 'int' object is not iterable errors and
help users understand the correct filter format.
Updated the filters parameter description in create_link and update_link
MCP tools to specify the required array format and provide examples.
Changes:
- Updated create_link filters description with array format requirements
- Updated update_link filters description with array format and example
- Added specific filter types: frequency_drop, packet_loss, delay, corrupt, bpf
This helps users understand the correct format:
- frequency_drop: [N] (drop every Nth packet)
- packet_loss: [rate] (packet loss percentage)
- delay: [ms, jitter] (latency and jitter in milliseconds)
- corrupt: [rate] (packet corruption percentage)
- bpf: [expression] (Berkeley Packet Filter)
Prevents TypeError: 'int' object is not iterable errors.
Extended the kwargs parameter handling fix to nodes and links MCP tools,
which had the same nested kwargs structure issue as templates.
Changes:
- Modified update_node_handler to extract params from nested kwargs
- Modified update_link_handler to extract params from nested kwargs
This ensures that node and link updates through MCP tools work correctly,
allowing proper modification of node and link properties.
Fixed an issue where MCP template tools (update_template, create_template)
were not correctly handling nested kwargs parameter structure from MCP clients.
The problem occurred when MCP clients passed parameters in the format:
{'template_id': 'xxx', 'kwargs': {'adapters': 3}}
The original code was passing the entire kwargs dictionary as a parameter,
instead of extracting the actual update parameters from within it.
Changes:
- Modified update_template_handler to extract params from nested kwargs
- Modified create_template_handler to handle the same issue
This fix ensures that template updates through MCP tools now work correctly,
allowing proper modification of template properties like adapters count.
Replace the global variable + lock + polling mechanism with asyncio.Event
pattern for MCP server ready state tracking.
Benefits:
- Eliminates race conditions (Event.set() is thread-safe)
- Event-driven notification instead of polling (no 50x sleep overhead)
- Fixes test contamination from global state
- Reduces code from 54 lines to 30 lines
- Uses standard asyncio primitive for this pattern
Instead of allowing connections to proceed when server initialization
times out, return HTTP 503 Service Unavailable to prevent MCP
protocol initialization errors.
This prevents the original "Received request before initialization
was complete" errors when GNS3 server startup takes longer than
the 5-second timeout.
Add server ready state tracking for MCP service to prevent
"Received request before initialization was complete" errors
when clients connect before GNS3 server completes startup.
Changes:
- Add MCP server ready state management with wait/notify mechanism
- Modify auth wrapper to wait for server initialization before accepting connections
- Set MCP server ready flag after GNS3 startup completes
This ensures MCP protocol initialization handshake only occurs
after GNS3 server is fully initialized, preventing race
conditions during server startup.
When GNS3 server is configured to listen on 0.0.0.0 (all interfaces),
_server_url() was hardcoding 127.0.0.1, making the WebSocket console
URL unreachable from remote MCP clients.
Use the UDP connect trick (connect to 8.8.8.8:80 without sending data)
to discover the default route interface IP, which is the address remote
clients can actually reach.
The MCP library's TransportSecurityMiddleware only supports exact host
matches or "host:*" port wildcards. It does NOT support a standalone "*"
wildcard to mean "allow all hosts" — setting allowed_hosts=["*"] would
reject every connection because no Host header equals "*".
Worse, when transport_security=None was passed to FastMCP while its default
host is "127.0.0.1", FastMCP would auto-enable protection with strict
localhost-only rules, overriding GNS3's intent to allow all hosts.
Root cause analysis:
- FastMCP auto-enables DNS rebinding protection when host is localhost
and no explicit TransportSecuritySettings is provided
- GNS3 was passing transport_security=None (indirectly via FastMCP's default)
when protection was disabled, triggering the auto-enable
- The TransportSecuritySettings "allowed_hosts" list does NOT support "*"
as a catch-all wildcard
This fix:
1. Always pass an explicit TransportSecuritySettings to FastMCP
- Disabled: TransportSecuritySettings(enable_dns_rebinding_protection=False)
- Enabled: TransportSecuritySettings(enable_dns_rebinding_protection=True, ...)
2. Restore mcp_allowed_hosts and mcp_allowed_origins config fields
3. Set mcp_enable_dns_rebinding_protection default to False (allow all hosts)
Behaviour:
- Default (no config change): all hosts can connect to MCP server
- With mcp_enable_dns_rebinding_protection=true: only configured hosts
- Aligns with GNS3 server's 0.0.0.0 binding policy
Add MCP transport security configuration to gns3_server.conf with permissive
defaults that align with GNS3's design philosophy and VM distribution requirements.
## Changes
### 1. Configuration Schema (gns3server/schemas/config.py)
- Added MCP transport security fields to ServerSettings class:
- mcp_enable_dns_rebinding_protection (bool, default: True)
- mcp_allowed_hosts (list[str], default: ["*"])
- mcp_allowed_origins (list[str], default: ["*"])
- Added field validators to handle comma-separated string input
### 2. MCP Server Initialization (gns3server/api/routes/mcp/__init__.py)
- Import TransportSecuritySettings from mcp.server.transport_security
- Added _create_mcp_server() function to read configuration
- Updated FastMCP instantiation to use configured security settings
### 3. Configuration Sample (gns3server/config_samples/gns3_server.conf)
- Added MCP transport security settings section
- Documented default behavior and security options
- Provided examples for different use cases
## Design Philosophy
**Default: Allow All Hosts** (matches GNS3's 0.0.0.0 binding):
- VM distribution works out-of-the-box
- Users can access from any network location
- Security-conscious users can restrict when needed
**Security: Optional Restriction**:
Users can configure specific hosts for enhanced security:
``ini
mcp_allowed_hosts = 127.0.0.1:*,localhost:*,192.168.1.3:*
mcp_allowed_origins = http://127.0.0.1:*,http://localhost:*,http://192.168.1.3:*
```
## Benefits
- Flexible: Users can configure based on security requirements
- User-friendly: Default matches GNS3's 0.0.0.0 binding philosophy
- Maintainable: No code changes needed for different deployment scenarios
- Secure: DNS rebinding protection remains enabled with configurable hosts
## Related
- Issue #2771
- FastMCP DNS rebinding protection design
- Existing skills configuration in ServerSettings
- Replace Args: docstring blocks with Annotated[str, Field(description=...)]
so parameter descriptions appear in inputSchema.properties.*.description
- mcp.server.fastmcp does not parse Args: blocks from docstrings;
only Annotated with pydantic Field injects descriptions into the
structured JSON Schema visible to AI clients via tools/list
- Remove redundant Args: blocks from docstrings (info moved to Field)
- Restore full 4-step websocat workflow in get_node_console_info docstring
with connection, command sending, response receiving, and timeout
- SSE endpoint supports both Authorization: Bearer header and ?token= query param
- Claude Code can use headers (no URL exposure)
- Claude Desktop (EventSource) can use ?token= URL param
- Use FastMCP (Anthropic MCP SDK) for tool registration and SSE transport
- Mount SSE app under /v3/mcp/transport with JWT token authentication
- Token passed via ?token=<jwt> query parameter on SSE connection
- Token validated against GNS3 auth_service and stored in contextvars
- Tool handlers create Gns3Connector with JWT token to call GNS3 REST API
- 7 project tools: list_projects, get_project, create_project, delete_project,
open_project, close_project, get_project_stats
- Unauthenticated SSE connections return 401
- Add MCPTool/MCPToolRegistry system for centralized tool registration
- Add 7 project-related MCP tools: list_projects, get_project, create_project,
delete_project, open_project, close_project, get_project_stats
- Tools use Gns3Connector (custom_gns3fy) to call GNS3 REST API via HTTP loopback,
keeping the MCP layer decoupled from controller internals
- Handlers run in thread pool via asyncio.to_thread() to avoid blocking
the event loop on synchronous requests calls
- Unified POST /v3/mcp/execute endpoint with JWT authentication
When a Docker node is deleted, the compute node's DELETE endpoint only calls
node.delete() which removes the working directory but does not remove the node
object from the project's self._nodes collection. This causes ghost nodes to
remain in memory.
When project variables are updated, the code iterates through ALL nodes in
memory and calls update() on them. For ghost nodes with VNC configuration, this
triggers VNC startup attempts, resulting in 60-second timeouts waiting for X11
socket files that don't exist.
The fix adds await node.project.remove_node(node) to ensure the node object is
removed from the project's node collection when deleted, matching the behavior
of other node types that use manager.delete_node() which already calls
project.remove_node().
This resolves the issue where updating project variables after deleting a VNC
Docker container would timeout with: 'x11 socket file "/tmp/.X11-unix/X100"
does not exist'
Fixes issue #2755
Replace per-project check_user_has_privilege calls with a single
batch method that performs 3 fixed DB queries regardless of project
count. Reduces GET /projects response time for 10000 projects from
~12s to ~290ms (40x improvement).
Fixed a bug where projects created by a user that are also in a resource pool
the user has access to would appear twice in the GET /projects response.
Changes:
- Add seen_project_ids set to track already added projects
- Check for duplicates before adding projects in Step 2 (user projects)
- Check for duplicates before adding projects in Step 3 (resource pool projects)
This ensures each project appears only once regardless of whether it's user-created
or shared via resource pool.
Add new privilege definitions:
- LLMConfig.Audit - View LLM model configurations
- LLMConfig.Modify - Update LLM model configurations
- LLMConfig.Allocate - Create/delete LLM model configurations
Add LLMConfig.Audit and LLMConfig.Modify to default User role so that
regular users can manage their own AI profiles without needing the
User.Manager role.
User-scoped LLM config endpoints now use LLMConfig.* permissions.
Group-scoped LLM config endpoints retain Group.* permissions.
Remove resource pools from the ACE endpoints list to prevent accidental
access through the 'all endpoints' option. Resource pools must be
explicitly configured for team sharing to maintain clear security
boundaries and prevent unintended exposure of shared projects.
This change aligns the UI behavior with the actual permission checking
logic where 'path: /' does not grant resource pool access.
Add a new repository method get_aces_for_path() that:
- Queries ACEs for a specific path at database level (more efficient)
- Preloads related user, group, and role objects to prevent 500 errors
- Keeps original get_aces() method unchanged to avoid performance impact
This improves both performance and code clarity for resource pool
deletion safety checks.
Add safety check to prevent deletion of resource pools that are being
used by ACE configurations. If an attempt is made to delete a resource
pool that has ACE rules referencing it, the API returns a 400 error
with detailed information showing which users/groups are using the
pool and their roles.
The error message only shows the resource pool name for a clean,
user-friendly experience without exposing internal path details.
Implement the correct three-step permission check logic:
- Step 1: ACE check - basic access permission (get projects user has ACE for)
- Step 2: Filter ace_projects by created_by - user's own projects (project sharing only through resource pools)
- Step 3: Resource pool projects (projects shared through resource pools)
This fixes the design flaw where:
- ACE check could bypass user isolation with broad ACE configurations
- seen_project_ids mechanism prevented proper layered checking
- Project sharing was confused with direct ACE configuration
The new logic ensures:
- User isolation works even with broad ACE (path='/', propagate=True)
- Project sharing is only available through resource pools (clear design)
- Proper layered checking without seen blocking mechanism
Implement a three-layer permission system:
- Layer 1: ACE strategy check (explicitly authorized/shared projects)
- Layer 2: Ownership check (user's own projects based on created_by)
- Layer 3: Resource pools (team shared projects)
This approach:
- Resolves the conflict between ACE and user isolation
- Enables project sharing via ACE (other users can grant access)
- Maintains default user isolation via ownership
- Prevents duplicate projects in results
- Preserves resource pool functionality
This commit adds a new `show_filters_icon` property to the Link class, allowing users to control whether filter icons are displayed in the Web UI at the individual link level.
**Changes:**
- Added `_show_filters_icon` attribute to Link class (default: True)
- Added `show_filters_icon` property getter
- Added `update_show_filters_icon()` method for updating the property
- Updated `asdict()` to include the new field in both topology and regular dumps
- Added `show_filters_icon` field to LinkBase schema
- Updated API routes to handle the new field in create and update operations
**API Impact:**
- POST /v3/projects/{project_id}/links - accepts `show_filters_icon` in request body
- PUT /v3/projects/{project_id}/links/{link_id} - can update `show_filters_icon`
- GET /v3/projects/{project_id}/links/{link_id} - returns `show_filters_icon` field
**Future Applications:**
This feature provides granular control for future AI fault injection modules to manage link-level protocol failures while maintaining clean UI presentation.
The Web Wireshark WebSocket endpoint created a WebWiresharkManager but
never called close(), leaving the DockerHTTPClient's ClientSession with
UnixConnector unclosed when users closed the browser tab.
Also switch asyncio.wait in WebSocket proxy from ALL_COMPLETED to
FIRST_COMPLETED to avoid blocking cleanup when one direction disconnects.
## Summary
Add a complete fault injection system for GNS3 Copilot, migrate all
skills from local Python files to an external Git repository with
hot reload support, and restructure Copilot API under /copilot/.
## Key Changes
### Fault Injection
- New troubleshooting_injection mode with InjectionSkillsTool
- 368 fault scenarios across 39 protocol categories
- Context-based filtering (LLM must pass topology protocols)
### External Skills Repository
- SkillsManager: Git clone/pull, version tracking, smart updates
- SkillsLoader: YAML skills + Markdown prompts from external repo
- Hot reload via POST /copilot/reload/skills
- Configurable via gns3_server.conf
### Architecture
- API unified under /copilot/ prefix
- SkillsManager moved from Controller to agent module
- Lazy initialization with startup background preload
- Per-command Git timeout, smart update checks
- Forbidden commands hot-reloadable from external repo
- 32 INFO logs downgraded to DEBUG
Enhance the node files API to include comprehensive file metadata:
- File size in bytes
- File creation time (ISO 8601 format)
- File modification time (ISO 8601 format)
- File extension
Create new NodeFile schema to support these additional fields
while keeping the existing ProjectFile schema for backward compatibility.
This provides users with better information to manage and identify
files in the Web UI.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implement issue #2719 - Add API endpoint to list project files
- Add GET /v3/projects/{project_id}/nodes/{node_id}/files endpoint
- Add list_node_files() method to Project class
- Add security checks to prevent path traversal
- Filter out .ghost temporary files
- Return file paths with MD5 checksums
- Require Node.Audit privilege
This allows users to discover dynamically created files
such as QEMU disk images created via the disk image API.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When accessing /static/web-ui without trailing slash, the request would
fail with "RuntimeError: File at path ... is not a file" because:
1. The route /static/web-ui/{file_path:path} doesn't match paths without
trailing slash (Starlette's path regex requires the /)
2. The request falls through to StaticFiles mount, which tries to serve
the directory as a file
This fix:
- Sets html=True on StaticFiles mount to automatically redirect directory
URLs to trailing slash versions
- Adds os.path.isdir() check to handle empty file_path gracefully
Fixes#2680
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add `get_container_ip` method to WebWiresharkManager for retrieving container IP addresses in wireshark network
- Method uses Docker API as primary approach with container command execution as fallback
- Refactor web_wireshark_websocket endpoint to use new method instead of direct Docker queries
- Improve error handling and logging for container IP retrieval failures
* fix(deps): upgrade pytest to 9.0.3 to fix CVE-2025-71176
CVE-2025-71176: pytest 9.0.2 and earlier versions have a local
security vulnerability due to predictable temporary directory naming.
- pytest: 8.4.2 → 9.0.3
- Python 3.10+ is now required
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(deps): upgrade python-multipart to 0.0.26 to fix CVE-2026-40347
CVE-2026-40347: python-multipart < 0.0.26 has a denial of
service vulnerability when parsing multipart data.
- python-multipart: 0.0.22 → 0.0.26
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(deps): upgrade swagger-ui to 4.1.3 to fix CVE-2018-25031
CVE-2018-25031: swagger-ui < 4.1.3 has a spoofing vulnerability
where remote attackers can display remote OpenAPI definitions
via crafted URLs.
- swagger-ui: 3.30.0 → 4.1.3
- Updated swagger-ui-bundle.js and swagger-ui.css
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(api): change FastAPI version from "v3" to "3.0.0" for Swagger UI 4.1.3 compatibility
Swagger UI 4.x enforces stricter version format validation.
The version "v3" is not accepted by the new validator.
Changed from:
- version="v3"
To:
- version="3.0.0"
This affects both controller and compute API definitions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(deps): upgrade swagger-ui to 5.32.4
Fixes "Unable to render this definition" error when loading OpenAPI 3.0
docs. Swagger UI 3.x had incomplete OpenAPI 3.0 support.
- swagger-ui: 3.19.1 → 5.32.4 (latest)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>