Merge remote-tracking branch 'origin/3.0' into gh-pages

This commit is contained in:
github-actions committed 2026-05-13 09:20:40 +00:00
commit 5bdd307ea9
471 files changed
+75257 -4705

No files matched your search

+443
View File
@@ -0,0 +1,443 @@
Creative Commons Attribution-ShareAlike 4.0 International
Copyright (c) 2026 GNS3 Web UI Project
=======================================================================
Creative Commons Attribution-ShareAlike 4.0 International
Copyright (C) 2025 Creative Commons Corporation
Creative Commons Corporation ("Creative Commons") is not a law firm and
does not provide legal services or legal advice. Distribution of
Creative Commons public licenses does not create a lawyer-client or
other relationship. Creative Commons makes its licenses and related
information available on an "as-is" basis. Creative Commons gives no
warranties regarding its licenses, any material licensed under their
terms and conditions, or any related information. Creative Commons
disclaims all liability for damages resulting from their use to the
fullest extent possible.
Using Creative Commons Public Licenses
Creative Commons public licenses provide a standard set of terms and
conditions that creators and other rights holders may use to share
original works of authorship and other material subject to copyright
and certain other rights specified in the public license below. The
following considerations are for informational purposes only, are not
exhaustive, and do not form part of our licenses.
Considerations for licensors: Our public licenses are
intended for use by those authorized to give the public
permission to use material in ways otherwise restricted by
copyright and certain other rights. Our licenses are
irrevocable. Licensors should read and understand the terms
and conditions of the license they choose before applying it.
Licensors should also secure all rights necessary before
applying our licenses so that the public can reuse the
material as expected. Licensors should clearly mark any
material not subject to the license. This includes other CC-
licensed material, or material used under an exception or
limitation to copyright. More considerations for licensors:
wiki.creativecommons.org/Considerations_for_licensors
Considerations for the public: Our public licenses are
intended for use by those authorized to give the public
permission to use material in ways otherwise restricted by
copyright and certain other rights. Our licenses are
irrevocable. Licensors should read and understand the terms
and conditions of the license they choose before applying it.
Licensors should also secure all rights necessary before
applying our licenses so that the public can reuse the
material as expected. Licensors should clearly mark any
material not subject to the license. This includes other CC-
licensed material, or material used under an exception or
limitation to copyright. More considerations for licensors:
wiki.creativecommons.org/Considerations_for_licensors
Creative Commons Attribution-ShareAlike 4.0 International Public
License
By exercising the Licensed Rights (defined below), You accept and agree
to be bound by the terms and conditions of this Creative Commons
Attribution-ShareAlike 4.0 International Public License ("Public
License"). To the extent this Public License may be interpreted as a
contract, You are granted the Licensed Rights in consideration of Your
acceptance of these terms and conditions, and the Licensor grants You
such rights in consideration of benefits the Licensor receives from
making the Licensed Material available under these terms and conditions.
Section 1 – Definitions.
a. Adapted Material means material subject to Copyright and Similar
Rights that is derived from or based upon the Licensed Material
and in which the Licensed Material is translated, altered,
arranged, transformed, or otherwise modified in a manner requiring
permission under the Copyright and Similar Rights held by the
Licensor. For purposes of this Public License, where the Licensed
Material is a musical work, performance, or sound recording,
Adapted Material is always produced where the Licensed Material is
synched in timed relation with a moving image.
b. Adapter's License means the license You apply to Your Copyright
and Similar Rights in Your contributions to Adapted Material in
accordance with the terms and conditions of this Public License.
c. Copyright and Similar Rights means copyright and/or similar rights
closely related to copyright including, without limitation,
publication, distribution, public performance, public display, and
adaptation rights, and rights in database extensions and
collections.
d. Licensed Material means the artistic or literary work, database,
or other material to which the Licensor applied this Public
License.
e. Licensed Rights means rights granted to You subject to the terms
and conditions of this Public License, which are limited to all
Copyright and Similar Rights that apply to Your use of the
Licensed Material and that the Licensor has authority to license.
f. Licensor means the individual(s) or entity(ies) granting rights
under this Public License.
g. Share means to provide material to the public by any means or
process that requires permission under the Licensed Rights, such
as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the
public may access the material from a place and at a time
individually chosen by them.
h. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and
of the Council of 11 March 1996 on the legal protection of
databases, as amended and/or succeeded, as well as other
essentially equivalent rights anywhere in the world.
i. You means the individual or entity exercising the Licensed Rights
under this Public License. Your has a corresponding meaning.
Section 2 – Scope.
a. License grant.
1. Subject to the terms and conditions of this Public License,
the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to
exercise the Licensed Rights in the Licensed Material to:
a. reproduce and Share the Licensed Material, in whole or
in part; and
b. produce, reproduce, and Share Adapted Material.
2. Exceptions and Limitations. For the avoidance of doubt,
where Exceptions and Limitations apply to Your use, this
Public License does not apply, and You do not need to
comply with its terms and conditions.
3. Term. The term of this Public License is specified in
Section 6(a).
4. Media and formats; technical modifications allowed. The
Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created,
and to make technical modifications necessary to do so. The
Licensor waives and/or agrees not to assert any right or
authority to forbid You from making technical modifications
necessary to exercise the Licensed Rights, including
technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License,
simply making modifications authorized by this Section 2(a)
(4) never produces Adapted Material.
5. Downstream recipients.
a. Offer from the Licensor – Licensed Material. Every
recipient of the Licensed Material automatically
receives an offer from the Licensor to exercise the
Licensed Rights under the terms and conditions of this
Public License.
b. Additional offer from the Licensor – Adapted Material.
Every recipient of Adapted Material from You
automatically receives an offer from the Licensor to
exercise the Licensed Rights in the Adapted Material
under the conditions of the Adapter's License You
apply.
6. No endorsement. Nothing in this Public License constitutes or
may be construed as permission to assert or imply that You
are, or that Your use of the Licensed Material is, connected
with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as
provided in Section 3(a)(1)(A)(i).
b. Other rights. Moral rights, such as the right of integrity, are
not licensed under this Public License, nor are publicity,
privacy, and/or other similar personality rights; however, to the
extent possible, the Licensor waives and/or agrees not to assert
any such rights held by the Licensor to the limited extent
necessary to allow You to exercise the Licensed Rights, but not
otherwise.
c. Patent and trademark rights are not licensed under this Public
License.
d. To the extent possible, the Licensor waives any right to collect
royalties from You for the exercise of the Licensed Rights,
whether directly or through a collecting society under any
voluntary or waivable statutory or compulsory licensing scheme.
In all other cases the Licensor expressly reserves any right to
collect such royalties.
Section 3 – License Conditions.
Your exercise of the Licensed Rights is expressly made subject to
the following conditions.
a. Attribution.
1. If You Share the Licensed Material (including in modified
form), You must:
a. retain the following if it is supplied by the
Licensor with the Licensed Material:
i. identification of the creator(s) of the
Licensed Material and any others designated to
receive attribution, in any reasonable manner
requested by the Licensor (including by
pseudonym if designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of
warranties;
v. a URI or hyperlink to the Licensed Material to
the extent reasonably practicable;
b. indicate if You modified the Licensed Material and
retain an indication of any previous modifications; and
c. indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI
or hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any
reasonable manner based on the medium, means, and context in
which You Share the Licensed Material. For example, it may
be reasonable to satisfy the conditions by providing a URI
or hyperlink to a resource that includes the required
information.
3. If requested by the Licensor, You must remove any of the
information required by Section 3(a)(1)(A) to the extent
reasonably practicable.
4. If You Share Adapted Material You produce, the Adapter's
License You apply must not prevent recipients of the
Adapted Material from complying with this Public License.
b. ShareAlike.
In addition to the conditions in Section 3(a), if You Share
Adapted Material You produce, the following conditions also apply.
1. The Adapter's License You apply must be a Creative Commons
license with the same License Elements, this version or
later, or a BY-SA Compatible License.
2. You must include the text of, or the URI or hyperlink to,
the Adapter's License You apply. You may satisfy this
condition in any reasonable manner based on the medium,
means, and context in which You Share Adapted Material.
3. You may not offer or impose any additional or different terms
or conditions on, or apply any Effective Technological
Measures to, Adapted Material that restrict exercise of the
rights granted under the Adapter's License You apply.
Section 4 – Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that
apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right
to extract, reuse, reproduce, and Share all or a substantial
portion of the contents of the database;
b. if You include all or a substantial portion of the database
contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database
Rights (but not its individual contents) is Adapted Material; and
c. You must comply with the conditions in Section 3(a) if You Share
all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not
replace Your obligations under this Public License where the Licensed
Rights include other Copyright and Similar Rights.
Section 5 – Disclaimer of Warranties and Limitation of Liability.
a. Unless otherwise separately undertaken by the Licensor, to the
extent possible, the Licensor offers the Licensed Material as-is
and as-available, and makes no representations or warranties of
any kind concerning the Licensed Material, whether express,
implied, statutory, or other. This includes, without limitation,
warranties of title, merchantability, fitness for a particular
purpose, non-infringement, absence of latent or other defects,
accuracy, or the presence or absence of errors, whether or not
known or discoverable. Where disclaimers of warranties are not
allowed in full or in part, this disclaimer may not apply to You.
b. To the extent possible, in no event will the Licensor be liable
to You on any legal theory (including, without limitation,
negligence) or otherwise for any direct, special, indirect,
incidental, consequential, punitive, exemplary, or other losses,
costs, expenses, or damages that arise out of this Public License
or use of the Licensed Material, even if the Licensor has been
advised of the possibility of such losses, costs, expenses, or
damages. Where a limitation of liability is not allowed in full
or in part, this limitation may not apply to You.
c. The disclaimer of warranties and limitation of liability provided
above shall be interpreted in a manner that, to the extent
possible, most closely approximates an absolute disclaimer and
waiver of all liability.
Section 6 – Term and Termination.
a. This Public License applies for the term of the Copyright and
Similar Rights licensed here. However, if You fail to comply
with this Public License, then Your rights under this Public
License terminate automatically.
b. Where Your right to use the Licensed Material has terminated
under Section 6(a), it reinstates:
1. automatically as of the date the violation is cured,
provided it is cured within 30 days of Your discovery of
the violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any
right the Licensor may have to seek remedies for Your violations
of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the
Licensed Material under separate terms or conditions or stop
distributing the Licensed Material at any time; however, doing so
will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public
License.
Section 7 – Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different
terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the
Licensed Material not stated herein are separate from and
independent of the terms and conditions of this Public License.
Section 8 – Interpretation.
a. For the avoidance of doubt, this Public License does not, and
shall not be interpreted to, reduce, limit, restrict, or impose
conditions on any use of the Licensed Material that could lawfully
be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License
is deemed unenforceable, it shall be automatically reformed to
the minimum extent necessary to make it enforceable. If the
provision cannot be reformed, it shall be severed from this
Public License without affecting the enforceability of the
remaining terms and conditions.
c. No term or condition of this Public License will be waived and
no failure to comply consented to unless expressly agreed to by
the Licensor.
d. Nothing in this Public License constitutes or may be interpreted
as a limitation upon, or waiver of, any privileges and immunities
that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority.
=======================================================================
Creative Commons is not a party to its public licenses. Notwithstanding,
Creative Commons may elect to apply one of its public licenses to
material it publishes and in those instances will be considered the
"Licensor." The text of the Creative Commons public licenses is
dedicated to the public domain under the CC0 Public Domain Dedication.
The text of the Creative Commons public licenses is dedicated to the
public domain under the CC0 Public Domain Dedication. Except for the
limited purpose of indicating that material is shared under a Creative
Commons public license or as otherwise permitted by the Creative Commons
policies published at creativecommons.org/policies, Creative Commons
does not authorize the use of the trademark "Creative Commons" or any
other trademark or logo of Creative Commons without its prior written
consent including, without limitation, in connection with any
unauthorized modifications to any of its public licenses or any other
arrangements, understandings, or agreements concerning use of licensed
material. For the avoidance of doubt, this paragraph does not form part
of the public licenses.
Creative Commons may be contacted at creativecommons.org.
=======================================================================
For the GNS3 Web UI documentation:
- Documentation License: CC BY-SA 4.0
- Project License: GPLv3 (see root LICENSE file)
- License URL: https://creativecommons.org/licenses/by-sa/4.0/
All documentation files in this directory are licensed under the
Creative Commons Attribution-ShareAlike 4.0 International License
(CC BY-SA 4.0).
=======================================================================
IMPORTANT: ShareAlike Requirements for Software Derivatives
Under the CC BY-SA 4.0 ShareAlike condition, if you create derivative
works based on this documentation, including software that is considered
a derivative of the documentation, you must:
1. **License your derivative work under CC BY-SA 4.0 or a compatible
license** (such as GPLv3, which is compatible with CC BY-SA 4.0)
2. **Attribute the original documentation** to the GNS3 Web UI Project
3. **Indicate if you modified the documentation** and retain
indications of previous modifications
4. **Include the license notice** and link to the CC BY-SA 4.0 license
What constitutes a derivative work:
- Translations of the documentation
- Adaptations, modifications, or enhancements to the documentation
- Software that incorporates substantial portions of the documentation
- Works based on the documentation's structure, organization, or content
Compatible licenses include:
- CC BY-SA 4.0 or later
- GPLv3 (GNU General Public License version 3)
- AGPLv3 (GNU Affero General Public License version 3)
- Other licenses approved as compatible by Creative Commons
For more information on CC BY-SA compatibility, see:
https://creativecommons.org/compatiblelicenses
If you are unsure whether your use constitutes a derivative work or
which license to apply, please consult the full CC BY-SA 4.0 license
text above or seek legal advice.
+120
View File
@@ -0,0 +1,120 @@
<!--
SPDX-License-Identifier: CC-BY-SA-4.0
See LICENSE file for licensing information.
-->
> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.
# GNS3 Server Documentation
---
## License
This documentation is licensed under the **Creative Commons Attribution-ShareAlike 4.0 International License (CC BY-SA 4.0)**.
⚠️ **Important - ShareAlike Requirement**: If you create derivative works based on this documentation (including software that incorporates substantial portions of the documentation), your work must also be licensed under **CC BY-SA 4.0 or a compatible license** (such as GPLv3).
- 📄 **Full License Text**: See [docs/LICENSE](./LICENSE)
- 🔗 **License URL**: https://creativecommons.org/licenses/by-sa/4.0/
- 📖 **Compatibility**: https://creativecommons.org/compatiblelicenses
**Dual License Structure**:
- 📚 **Documentation**: CC BY-SA 4.0 (this directory)
- 💻 **Software Code**: GPLv3 (see root [LICENSE](../LICENSE))
---
Technical documentation for the GNS3 server project, covering features, AI Copilot, development setup, and known issues.
## Directory Structure
```
docs/
├── README.md # This file
├── development-setup.md # Ubuntu 24.04 development environment setup
├── openapi.json # OpenAPI specification
├── features/ # Feature documentation
│ ├── compute-controller-setup.md # Controller + Compute architecture & configuration
│ ├── statistics-api.md # Aggregated statistics API for monitoring
│ ├── vnc-websocket-console.md # Browser-based VNC console via WebSocket
│ └── web-wireshark-business-process.md # Web Wireshark (Docker + xpra packet capture)
├── gns3-copilot/ # AI Copilot feature documentation
│ ├── netmiko_devices.md # Netmiko supported devices (366 types)
│ ├── template-based-configuration-roadmap.md # Future: template-based config with HITL
│ └── implemented/ # Implemented features
│ ├── chat-api.md # Chat API (SSE, session management)
│ ├── llm-model-configs.md # LLM model configuration system
│ ├── command-security.md # Command security and filtering
│ ├── context-window-management.md # Context window optimization
│ ├── node-control-tools.md # Node start/stop/suspend tools
│ └── multi-vendor-device-support.md # Multi-vendor device support
└── bugs/ # Known issues & bug reports
└── telnet-server-connection-race-condition.md
```
---
## Features
### Controller + Compute Setup (`features/compute-controller-setup.md`)
Architecture and minimum configuration for setting up GNS3 Controller with remote Compute nodes. Covers compute node config, controller registration, and multi-compute deployment.
### Statistics API (`features/statistics-api.md`)
Aggregated server statistics API (`GET /v3/statistics`) for monitoring dashboards. Collects compute resources, project/node/link counts, and Web Wireshark container status in a single request.
### VNC WebSocket Console (`features/vnc-websocket-console.md`)
Browser-based VNC console access via WebSocket. The Controller acts as WebSocket-to-WebSocket relay, and Compute bridges WebSocket to TCP for QEMU/Docker VMs. Supports noVNC clients.
### API Error Responses (`features/api-error-responses.md`)
Unified error response format across all GNS3 API endpoints. Documents HTTP status codes, error types, and client-side error handling patterns.
### Web Wireshark (`features/web-wireshark-business-process.md`)
Web-based packet capture analysis using Docker + xpra HTML5 client. Zero-install Wireshark experience directly in the browser, integrated with GNS3 topologies.
---
## GNS3 AI Copilot (`gns3-copilot/`)
### Implemented Features
| Feature | Description | Status |
|---------|-------------|--------|
| [Chat API](gns3-copilot/implemented/chat-api.md) | SSE streaming, session management, token statistics | Implemented |
| [LLM Model Configs](gns3-copilot/implemented/llm-model-configs.md) | Multi-level model config (system/group/user) | Implemented |
| [Command Security](gns3-copilot/implemented/command-security.md) | Command filtering, dangerous operation detection, HITL | Implemented |
| [Context Window Management](gns3-copilot/implemented/context-window-management.md) | Token optimization, content filtering, compression | Implemented |
| [Node Control Tools](gns3-copilot/implemented/node-control-tools.md) | Start/stop/suspend with batch ops and progress tracking | Implemented |
| [Multi-Vendor Support](gns3-copilot/implemented/multi-vendor-device-support.md) | Cisco, Huawei, Ruijie, VPCS with custom Netmiko drivers | Implemented |
### Reference
- [Netmiko Supported Devices](gns3-copilot/netmiko_devices.md) — 366 device types (154 SSH, 55 Telnet, 3 custom GNS3 drivers)
### Roadmap
- [Template-Based Configuration with HITL](gns3-copilot/template-based-configuration-roadmap.md) — Jinja2 templates with human-in-the-loop confirmations for device configuration and node creation
---
## Known Issues (`bugs/`)
- [Telnet Server Connection Race Condition](bugs/telnet-server-connection-race-condition.md) — `getpeername()` error when client disconnects during connection setup (High severity, Open)
---
## Development Setup (`development-setup.md`)
Quick-start guide for Ubuntu 24.04: install via PPA, set up dependencies, and run gns3-server from source.
---
## Related Documentation
- [GNS3 Server API Documentation](https://api.gns3.com/) — Interactive API docs (also available locally via `redoc.html`)
- [GNS3 Web UI Documentation](https://docs.gns3.com/)
- [LangGraph Documentation](https://langchain-ai.github.io/langgraph/)
---
_Last updated: 2026-04-20_
@@ -0,0 +1,316 @@
<!--
SPDX-License-Identifier: CC-BY-SA-4.0
See LICENSE file for licensing information.
-->
> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.
# Telnet Server Connection Race Condition Bug
## Bug Report
**Date**: 2026-03-14
**Severity**: High
**Status**: Open
**Component**: Telnet Server (`gns3server/utils/asyncio/telnet_server.py`)
## Error Logs
```
2026-03-14 15:21:31 ERROR asyncio:1879 Unhandled exception in client_connected_cb
transport: <_SelectorSocketTransport fd=67 read=polling write=<idle, bufsize=0>>
Traceback (most recent call last):
File "/home/yueguobin/myCode/GNS3/gns3-server/gns3server/utils/asyncio/telnet_server.py", line 215, in run
await self._process(network_reader, network_writer, connection)
File "/home/yueguobin/myCode/GNS3/gns3-server/gns3server/utils/asyncio/telnet_server.py", line 305, in _process
client_info = connection_key.get_extra_info("socket").getpeername()
File "/usr/lib64/python3.13/asyncio/trsock.py", line 77, in getpeername
return self._sock.getpeername()
~~~~~~~~~~~~~~~~~~~~~~^^
OSError: [Errno 107] Transport endpoint is not connected
```
## Root Cause Analysis
### Architecture
The GNS3 Telnet server architecture supports multiple concurrent client connections to a single node console:
```
┌──────────────────────────────────────────────────────────┐
│ Node (VPCS/Docker/Router/Switch, etc.) │
│ - Independent process │
│ - Normal operation, processing business logic │
└────────────────────┬─────────────────────────────────────┘
│ stdout (device output)
│
┌────────────▼─────────────────────────────────────┐
│ AsyncioTelnetServer (Telnet Proxy Server) │
│ - Reads output from Node │
│ - Broadcasts to all connected clients │
│ - ← ← Bug occurs at this layer ← ← │
└────────────┬─────────────────────────────────────┘
│
┌────────────┼────────────┬──────────────────┐
│ │ │ │
Web Console Auto Script Client 1 Client 2
(long-lived) (quick disco) (normal) (normal)
```
### The Race Condition
The bug occurs in the broadcast logic when a client disconnects while the server is iterating through connections:
**Timeline**:
```
t1: Clients A, B, C connect to the same Telnet port
t2: Device has output (e.g., log message)
t3: Client A receives data and script immediately disconnects (FIN sent)
t4: Server hasn't read EOF yet (event loop hasn't checked this connection)
t5: Server iterates through connections to broadcast data
t6: When iterating to Client A, getpeername() is called → OSError!
```
### Code Location
**File**: `gns3server/utils/asyncio/telnet_server.py`
**Line**: 305
**Problematic Code**:
```python
# Line 304-305
for connection_key in list(self._connections.keys()):
client_info = connection_key.get_extra_info("socket").getpeername() # ← OSError here
connection = self._connections[connection_key]
try:
connection.writer.write(data)
await asyncio.wait_for(connection.writer.drain(), timeout=10)
except:
log.debug(f"Timeout while sending data to client: {client_info}, closing and removing from connection table.")
connection.close()
del self._connections[connection_key]
```
### The Core Issue
The `getpeername()` call is **outside** the try-except block, so any OSError from it is not caught.
Additionally, the top-level exception handler at line 216 only catches `ConnectionError`:
```python
# Line 212-227
try:
await self._write_intro(network_writer, echo=self._echo, binary=self._binary, naws=self._naws)
await connection.connected()
await self._process(network_reader, network_writer, connection)
except ConnectionError: # ← Only catches ConnectionError
async with self._lock:
network_writer.close()
if self._reader_process == network_reader:
self._reader_process = None
if self._current_read is not None:
self._current_read.cancel()
await connection.disconnected()
del self._connections[network_writer]
```
**Python Exception Hierarchy**:
```
BaseException
└─ Exception
├─ ConnectionError ← Only this is caught
│ ├─ ConnectionResetError
│ ├─ BrokenPipeError
│ └─ ...
└─ OSError ← Actually thrown! (not a subclass of ConnectionError)
└─ [Errno 107] Transport endpoint is not connected
```
Since `OSError` is **not** a subclass of `ConnectionError`, it propagates uncaught to the asyncio event loop.
## Impact Assessment
### Immediate Effects
| Impact | Severity | Description |
|--------|----------|-------------|
| Connection interrupted | 🔴 High | The connection triggering the exception is terminated |
| Resource leak | 🟠 Medium | socket/connection not properly cleaned up |
| Other clients affected | 🟡 Low | Other clients on same port may miss broadcast data |
| Service stability | 🟡 Low | Long-running may accumulate zombie connections |
### User-Reported Symptoms
After this error occurs, users report:
1. **Cannot open the affected node** - Clicking on the node fails
2. **Cannot close the node** - Close button doesn't work
3. **"Node not found" errors** - Operations on the node return 404
4. **Refresh fixes it temporarily** - Reloading the page restores functionality
### Why This Happens
When the uncaught `OSError` occurs, the cleanup code at lines 217-227 **never executes**:
```python
except ConnectionError:
async with self._lock:
network_writer.close() # ✗ Not executed
if self._reader_process == network_reader:
self._reader_process = None # ✗ Not executed
if self._current_read is not None:
self._current_read.cancel() # ✗ Not executed
await connection.disconnected() # ✗ Not executed
del self._connections[network_writer] # ✗ Not executed
```
This leads to:
- **Resource leaks**: socket and writer not closed, file descriptors leaked
- **State inconsistency**: `_connections` dictionary retains disconnected connections
- **Potential deadlocks**: Locks may not be released if exception occurs while holding them
- **Subsequent operation failures**: Future operations may access zombie connections
### Effect on Node Process
**The Node process itself is NOT affected**:
- Node continues running normally
- Node's stdout has already been read by the proxy
- The bug occurs during the broadcast phase, after data has been read
The issue is in the **Telnet Proxy layer**, not the node itself.
## Trigger Conditions
This error is more likely to occur with:
| Scenario | Probability | Reason |
|----------|-------------|--------|
| **Automated scripts** | 🔴 High | Fast connect → execute → disconnect, small time window |
| **Manual operation** | 🟡 Medium | Can occur (e.g., closing terminal, network fluctuation) |
| **Normal usage** | 🟢 Low | Human operations slower, server usually detects EOF first |
### Typical Scenario
1. User opens Web Console (long-lived connection)
2. Automated script connects → executes command → quickly disconnects
3. While script disconnects, device has output that needs broadcasting
4. During connection iteration, script connection already closed
5. `getpeername()` call fails with OSError
## Related Issues
A secondary issue was found in the error handler:
**File**: `gns3server/api/server.py`
**Line**: 162
```python
@app.exception_handler(ControllerNotFoundError)
async def controller_not_found_error_handler(request: Request, exc: ControllerNotFoundError):
log.error(f"Controller not found error in {request.url.path} ({request.method}): {exc}")
# ^^^^^^^^^^^^^^^
return JSONResponse(...)
```
**Problem**: `request.method` only exists in HTTP requests, not WebSocket connections.
When a WebSocket request triggers this exception handler:
```
ControllerNotFoundError: Node ID xxx doesn't exist
↓
Attempt to log error
↓
AttributeError: 'WebSocket' object has no attribute 'method'
```
This masks the original error with an attribute error.
## Proposed Fix
### Primary Fix (Telnet Server)
Move `getpeername()` inside the try block and catch OSError:
```python
# Lines 303-314
for connection_key in list(self._connections.keys()):
connection = self._connections[connection_key]
client_info = None
try:
client_info = connection_key.get_extra_info("socket").getpeername()
connection.writer.write(data)
await asyncio.wait_for(connection.writer.drain(), timeout=10)
except (OSError, ConnectionError, asyncio.TimeoutError) as e:
log.debug(f"Error sending data to client {client_info}: {e}, closing and removing from connection table.")
connection.close()
del self._connections[connection_key]
```
### Secondary Fix (Top-level Exception Handler)
Catch OSError in the main handler to ensure cleanup:
```python
# Lines 212-227
try:
await self._write_intro(network_writer, echo=self._echo, binary=self._binary, naws=self._naws)
await connection.connected()
await self._process(network_reader, network_writer, connection)
except (ConnectionError, OSError): # ← Add OSError
async with self._lock:
network_writer.close()
if self._reader_process == network_reader:
self._reader_process = None
if self._current_read is not None:
self._current_read.cancel()
await connection.disconnected()
del self._connections[network_writer]
```
### Tertiary Fix (API Error Handler)
Fix the WebSocket error handler:
```python
@app.exception_handler(ControllerNotFoundError)
async def controller_not_found_error_handler(request: Request, exc: ControllerNotFoundError):
method = getattr(request, 'method', 'WebSocket')
log.error(f"Controller not found error in {request.url.path} ({method}): {exc}")
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content={"message": str(exc)},
)
```
## Reproduction Steps
**To be documented** after testing.
Potential reproduction scenario:
1. Start a GNS3 node with console enabled (e.g., VPCS)
2. Open web console to keep a long-lived connection
3. Run automated script that:
- Connects to the same console port
- Executes a command
- Immediately disconnects
4. While script is disconnecting, trigger device output
5. Observe the error in logs
## References
- **Files**:
- `gns3server/utils/asyncio/telnet_server.py:305` (primary issue)
- `gns3server/utils/asyncio/telnet_server.py:216` (exception handler)
- `gns3server/api/server.py:162` (secondary issue)
- **Related Commits**:
- Recent telnet-related work on feature branch
- **Error Patterns**:
- Race condition in connection management
- Incomplete exception handling in asyncio code
+146
View File
@@ -0,0 +1,146 @@
# Ubuntu 24.04 GNS3 Development Environment Setup
## Simplified Development Environment Setup
### 1. Add PPA
```bash
sudo apt update
sudo apt install software-properties-common
sudo add-apt-repository ppa:gns3/ppa
sudo apt update
```
### 2. Install Dependencies
Binary tools (Dynamips, VPCS, uBridge, etc.) are automatically installed as gns3-server dependencies
```bash
sudo apt install gns3-server tshark
```
### 3. Remove gns3-server While Keeping Dependencies
If you only want to run the server from source:
```bash
# Remove main package only, keep dependencies
sudo apt remove gns3-server
# Mark dependencies as manual to prevent autoremove deletion
sudo apt-mark manual ubridge dynamips vpcs libvirt
```
### 4. Install and Start Docker
```bash
sudo apt install docker.io
sudo systemctl enable --now docker
```
#### Configure Docker Mirror Accelerator (China Mainland)
If Docker Hub is inaccessible, configure mirror accelerators:
```bash
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<'EOF'
{
"registry-mirrors": [
"https://docker.1ms.run",
"https://docker.xuanyuan.me"
]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
```
Verify:
```bash
docker info | grep -i mirror
```
### 5. Add User to Groups
```bash
# If running as gns3 user (created by PPA install):
sudo usermod -aG ubridge,docker,kvm gns3
# If running as your own user, add yourself to these groups:
sudo usermod -aG ubridge,docker,kvm $USER
# Then logout and login again for group membership to take effect
```
### 6. Install Python venv
```bash
sudo apt install python3.12-venv
```
### 7. Run Development Version from Source
Clone the repository (official or your fork):
```bash
# Official repository
git clone https://github.com/GNS3/gns3-server.git
# Or your forked repository
git clone https://github.com/yourname/gns3-server.git
cd gns3-server
git checkout your-branch
```
Setup virtual environment and install dependencies:
```bash
python3 -m venv venv
source venv/bin/activate
# For China mainland users, use mirror:
# pip install -e . -i https://mirrors.aliyun.com/pypi/simple/
pip install -e . && gns3server-web-wireshark-setup
pip install -e .[ai-copilot]
pip install -e .[dev]
```
Run the server:
```bash
python3 -m gns3server
```
## Optional: Install AI Copilot Development Dependencies
```bash
python3 -m pip install .[ai-copilot,dev]
```
## Optional: Expand LVM Root Partition
If the root partition is low on space but the volume group has unallocated space:
Check LVM status:
```bash
sudo lvm pvdisplay
sudo lvm vgdisplay
sudo lvm lvdisplay
```
Extend the root LV using all free space:
```bash
sudo lvm lvextend -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv
sudo resize2fs /dev/mapper/ubuntu--vg-ubuntu--lv
```
Verify:
```bash
df -h
```
+80
View File
@@ -0,0 +1,80 @@
# API Error Response Format
## Overview
GNS3 Server uses a unified error response format across all API endpoints. All error responses return a JSON object with a `message` field containing the error details.
## Response Format
```json
{
"message": "Error description here"
}
```
## Error Types
| Error Type | HTTP Status | Description |
|------------|-------------|-------------|
| `ControllerBadRequestError` | 400 | Invalid request parameters (e.g., validation failure) |
| `ControllerUnauthorizedError` | 401 | Missing or invalid authentication |
| `ControllerForbiddenError` | 403 | User lacks required privileges |
| `ControllerNotFoundError` | 404 | Resource not found |
| `ControllerError` | 409 | General conflict (e.g., duplicate name) |
| `ControllerTimeoutError` | 408 | Operation timed out |
| `ComputeConflictError` | 409 | Compute node returned a conflict |
| `RequestValidationError` | 422 | Pydantic/FastAPI validation error (field missing or type mismatch) |
| `SQLAlchemyError` | 500 | Database error |
## Error Response Examples
### Template Already Exists (409)
```json
{
"message": "A template with name 'my-template' already exists"
}
```
### Validation Error (422)
```json
{
"message": "image field is required"
}
```
### Permission Denied (403)
```json
{
"message": "Permission denied (privilege Template.Allocate is required)"
}
```
### Invalid Template Type (400)
```json
{
"message": "JSON schema error received while creating new template: ..."
}
```
## Client-Side Error Handling
When handling errors in the GNS3 Web UI or API clients:
```javascript
// Extract error message from response
function extractErrorMessage(error) {
if (error.error && error.error.message) {
return error.error.message;
}
if (error.message) {
return error.message;
}
return 'Unknown error';
}
```
## References
- Exception handlers: `gns3server/api/server.py`
- Template validation: `gns3server/schemas/controller/templates/`
- Error classes: `gns3server/controller/controller_error.py`
+146
View File
@@ -0,0 +1,146 @@
<!--
SPDX-License-Identifier: CC-BY-SA-4.0
See LICENSE file for licensing information.
-->
> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.
# Controller + Compute Setup
This document describes the minimum configuration required to set up a GNS3 Controller with remote Compute nodes.
## Architecture Overview
- **Compute**: Runs individual nodes (QEMU, Docker, etc.) and provides resource monitoring
- **Controller**: Manages multiple computes, projects, and provides the REST API
- **Database**: Controller uses SQLite to store projects, nodes, and compute registration
## Minimum Configuration
### 1. Compute Node Configuration
Create the configuration file at `~/.config/GNS3/3.1/gns3_server.conf`:
```ini
[Server]
host = 0.0.0.0
port = 3080
compute_username = gns3
compute_password = gns3
```
Start the Compute:
```bash
gns3server
```
### 2. Controller Node Configuration
Create the configuration file at `~/.config/GNS3/3.1/gns3_server.conf`:
```ini
[Server]
host = 192.168.1.140
port = 3080
compute_username = gns3
compute_password = gns3
```
Start the Controller:
```bash
gns3server
```
### 3. Register Compute with Controller
Use the API to register a remote compute:
```bash
POST /v3/computes
{
"protocol": "http",
"host": "192.168.1.x",
"port": 3080,
"user": "gns3",
"password": "gns3"
}
```
## Important Notes
### Host Configuration
- **Controller `host`**: If set to `0.0.0.0`, the controller will register itself as `127.0.0.1`, which breaks remote compute connections
- **Symptom**: Compute nodes report "No common subnet for compute X (controller) and Y" even when on the same network
- **Solution**: Always use the actual LAN IP address for the Controller's `host` field (e.g., `host = 192.168.1.104`)
- **Dynamic IP**: If the controller machine uses DHCP, set a static lease on the router or use mDNS (`.local` domain)
### Password Configuration
- If `compute_password` is not set, a random 16-character password is auto-generated on startup
- The Controller must use the same credentials as the Compute's configuration
### Network Requirements
- Controller and Compute must be on the same LAN for cross-compute links to work
- UDP tunnel is used for cross-compute links, requiring network connectivity on UDP ports
### Configuration File Location
- Default location: `~/.config/GNS3/3.1/gns3_server.conf`
- Version `3.0` uses `~/.config/GNS3/3.0/`
## Troubleshooting
### 401 Unauthorized on Compute Connect
1. Check that Compute's `compute_username` and `compute_password` match what was passed to the API
2. Verify the Compute's configuration file is correctly loaded
3. Ensure the `[Server]` section is used (not `[Controller]`)
### No Common Subnet Error
If compute nodes report:
```
Cannot get an IP address on same subnet: No common subnet for compute X (controller) and Y
```
1. **Primary cause**: Controller's `host` is set to `0.0.0.0` - it registers as `127.0.0.1` which is unreachable from compute nodes
2. Check the controller's `/v3/version` endpoint - if `controller_host` shows `127.0.0.1`, this is the issue
3. Set the Controller's `host` to its actual LAN IP address (e.g., `192.168.1.104`)
4. Verify both machines are on the same network and can ping each other
5. Check firewall rules allow TCP/UDP communication on required ports
### WebSocket Console Authentication Failed
When connecting to a remote compute node's console via WebSocket, the connection may fail with errors in the logs:
**Controller log:**
```
New client 192.168.1.104:33268 has connected to controller console WebSocket
Forwarding console WebSocket to 'ws://192.168.1.3:3080/v3/compute/projects/.../console/ws'
Client 192.168.1.104:33268 has disconnected from controller console WebSocket
```
**Compute log:**
```
WebSocket /v3/compute/projects/.../console/ws" [accepted]
ERROR gns3server.api.routes.compute.dependencies.authentication:103 Could not authenticate while connecting to compute WebSocket: Could not validate credentials
```
**Cause**: The Controller forwards its own `compute_username` and `compute_password` credentials when connecting to the Compute's WebSocket endpoint. If the Compute node does not have matching credentials configured, authentication fails.
**Solution**: Ensure the Compute node's configuration file has matching credentials:
```ini
[Server]
enable_http_auth = True
compute_username = gns3
compute_password = your_password
```
The Controller automatically uses its configured `compute_username` and `compute_password` when establishing WebSocket connections to remote computes (see `gns3server/api/routes/controller/nodes.py` lines 627-628).
**Note**: If `compute_password` is not explicitly set in the Compute's config, a random 16-character password is auto-generated at startup, making authentication impossible for any external Controller.
+236
View File
@@ -0,0 +1,236 @@
<!--
SPDX-License-Identifier: CC-BY-SA-4.0
See LICENSE file for licensing information.
-->
> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.
# Statistics API
## Overview
Aggregated server statistics API for monitoring dashboards. Collects compute resources, project/node/link counts, and Web Wireshark container status in a single request.
## Architecture
```mermaid
graph TB
Client["Client (Dashboard)"]
subgraph Controller["GET /v3/statistics"]
direction TB
F1["1. Compute Resources<br/>GET compute/statistics"]
F2["2. Project/Node/Link<br/>Iterate in memory"]
F3["3. Web Wireshark<br/>Docker API + docker stats"]
end
Compute["Compute Nodes<br/>(psutil)"]
Memory["Controller<br/>(in-memory data)"]
Docker["Docker Daemon"]
Client -->|GET /v3/statistics| Controller
F1 -->|HTTP| Compute
F2 -->|read| Memory
F3 -->|Docker API| Docker
```
## Data Collection Flow
```mermaid
sequenceDiagram
participant Client
participant Controller as Controller API
participant Compute as Compute Nodes
participant Docker as Docker Daemon
Client->>Controller: GET /v3/statistics
rect rgb(240, 248, 255)
Note over Controller: 1. Compute Resources
loop For each compute
Controller->>Compute: GET /statistics
Compute-->>Controller: psutil data
Note over Controller: skip on error
end
end
rect rgb(240, 255, 240)
Note over Controller: 2. Projects / Nodes / Links
Note over Controller: Count projects by status (opened/closed)
Note over Controller: Count nodes: open→by_type+by_status, closed→by_type only
Note over Controller: Count links: total + capturing
end
rect rgb(255, 248, 240)
Note over Controller: 3. Web Wireshark
loop For opened projects only
Controller->>Docker: Query container status
Controller->>Docker: docker stats (2s timeout)
Docker-->>Controller: Container resource usage
end
end
Controller-->>Client: JSON response
```
## API Endpoints
| Method | Path | Description | Auth |
|--------|------|-------------|------|
| GET | `/v3/statistics` | Aggregated server statistics | Session |
| GET | `/v3/compute/statistics` | Single compute resource stats | Basic Auth |
## Response
```json
{
"computes": [
{
"compute_id": "local",
"compute_name": "Local",
"statistics": {
"memory_total": 16777216000,
"memory_free": 8000000000,
"memory_used": 8777216000,
"swap_total": 2147479552,
"swap_free": 1500000000,
"swap_used": 647279552,
"cpu_usage_percent": 45,
"memory_usage_percent": 52,
"swap_usage_percent": 30,
"disk_usage_percent": 67,
"load_average_percent": [12, 8, 5]
}
}
],
"projects": {
"total": 5,
"opened": 3,
"closed": 2
},
"nodes": {
"total": 42,
"open_project_nodes": 30,
"closed_project_nodes": 12,
"by_type": { "qemu": 20, "docker": 12, "dynamips": 6, "vpcs": 4 },
"by_status": { "started": 25, "stopped": 12, "suspended": 5 }
},
"links": {
"total": 38,
"capturing": 2
},
"webwireshark": {
"total_containers": 1,
"running_containers": 1,
"active_sessions": 2,
"containers": [
{
"project_id": "e16e2b51-9ba9-403b-9df4-b2915d7508a3",
"project_name": "test-project",
"container_id": "6edc9029bac0",
"status": "running",
"running": true,
"active_sessions": 2,
"memory_limit": "2.0 GB",
"cpu_limit": "1.0",
"pids_limit": 1000,
"memory": "272.5MiB / 4GiB",
"cpu": "0.23%",
"pids": 69
}
]
}
}
```
## Field Reference
### `computes[]`
| Field | Type | Description |
|-------|------|-------------|
| `compute_id` | string | Compute identifier |
| `compute_name` | string | Display name |
| `statistics` | object | Resource usage (see below) |
**`statistics` fields:**
| Field | Type | Description |
|-------|------|-------------|
| `memory_total` | int | Total RAM (bytes) |
| `memory_free` | int | Available RAM (bytes) |
| `memory_used` | int | Used RAM (bytes) |
| `swap_total` | int | Total swap (bytes) |
| `swap_free` | int | Free swap (bytes) |
| `swap_used` | int | Used swap (bytes) |
| `cpu_usage_percent` | int | CPU usage 0-100 |
| `memory_usage_percent` | int | RAM usage 0-100 |
| `swap_usage_percent` | int | Swap usage 0-100 |
| `disk_usage_percent` | int | Project dir disk usage 0-100 |
| `load_average_percent` | int[] | Load avg per core (1/5/15 min) |
### `projects`
| Field | Type | Description |
|-------|------|-------------|
| `total` | int | All projects |
| `opened` | int | Currently opened |
| `closed` | int | Currently closed |
### `nodes`
| Field | Type | Description |
|-------|------|-------------|
| `total` | int | All nodes across projects |
| `open_project_nodes` | int | Nodes in opened projects (has runtime status) |
| `closed_project_nodes` | int | Nodes in closed projects (from topology JSON, no status) |
| `by_type` | object | Count by node type (qemu, docker, etc.) |
| `by_status` | object | Count by status — **open project nodes only** |
### `links`
| Field | Type | Description |
|-------|------|-------------|
| `total` | int | All links across projects |
| `capturing` | int | Links currently capturing |
### `webwireshark`
| Field | Type | Description |
|-------|------|-------------|
| `total_containers` | int | All Wireshark containers |
| `running_containers` | int | Currently running |
| `active_sessions` | int | Active capture sessions |
| `containers` | array | Per-container details (opened projects only) |
**`containers[]` fields:**
| Field | Type | Description |
|-------|------|-------------|
| `project_id` | string | Project UUID |
| `project_name` | string | Project name |
| `container_id` | string | Docker container ID (12 chars) |
| `status` | string | Container status (running, exited, etc.) |
| `running` | bool | Is running |
| `active_sessions` | int | Active captures in this project |
| `memory_limit` | string | Configured memory limit (e.g. `"2.0 GB"`, `"unlimited"`) |
| `cpu_limit` | string | Configured CPU limit (e.g. `"1.0"`, `"unlimited"`) |
| `pids_limit` | int/string | Process limit (e.g. `1000`, `"unlimited"`) |
| `memory` | string | Live memory usage (conditional, see notes) |
| `cpu` | string | Live CPU usage (conditional, see notes) |
| `pids` | int | Live process count (conditional, see notes) |
## Error Responses
| Status | Description |
|--------|-------------|
| 401 | Unauthorized — invalid or missing session |
| 500 | Internal server error |
## Notes
- **Compute stats are best-effort**: if a compute is unreachable, it is skipped (logged as error) and other data is still returned
- **`by_status` only reflects open project nodes**: closed projects store topology in JSON without runtime status
- **Live container stats** (`memory`, `cpu`, `pids`) are only present when `docker stats` succeeds — may be absent if the Docker daemon is slow (2s timeout)
- **Web Wireshark containers** are only queried for opened projects
+402
View File
@@ -0,0 +1,402 @@
<!--
SPDX-License-Identifier: CC-BY-SA-4.0
See LICENSE file for licensing information.
-->
> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.
# VNC WebSocket Console
## Overview
GNS3 server supports WebSocket-based VNC console access, enabling browser-based graphical console connections to QEMU and Docker VMs without requiring standalone VNC client applications.
This implementation uses GNS3's API layer as a transparent WebSocket-to-TCP proxy, forwarding binary VNC protocol data between the browser and the VM's VNC server.
## Architecture
### Connection Flow
```mermaid
graph LR
A[Browser noVNC] -->|WebSocket binary| B[GNS3 Controller]
B -->|WebSocket + BasicAuth + SSL| C[GNS3 Compute]
C -->|TCP| D[QEMU/Docker VNC :5900]
```
**Controller** acts as a WebSocket-to-WebSocket relay (JWT auth, IPv6 handling).
**Compute** acts as a WebSocket-to-TCP bridge (validates node state, opens VNC TCP connection).
### Components
1. **Browser (noVNC)**
- HTML5 VNC client running in the browser
- Connects via WebSocket using `binary` subprotocol
- Handles RFB protocol (Remote Frame Buffer) for VNC
2. **GNS3 Controller API**
- WebSocket endpoint: `/v3/projects/{project_id}/nodes/{node_id}/console/vnc`
- Authentication: JWT token via query parameter
- Authorization: RBAC privilege check via `has_privilege_on_websocket("Node.Console")`
- Proxies WebSocket to compute node (WebSocket-to-WebSocket relay)
- Handles IPv6 addresses by wrapping in brackets
3. **GNS3 Compute API**
- WebSocket endpoint: `/v3/compute/projects/{project_id}/{node_type}/nodes/{node_id}/console/vnc`
- Authentication: HTTP Basic Auth via `ws_compute_authentication()`
- Establishes TCP connection to VNC server, bridges WebSocket ↔ TCP
4. **Node (QEMU/Docker)**
- VNC server listening on configured port (default: 5900+)
- RFB protocol for remote display
## API Endpoints
### Controller WebSocket Endpoint
**URL**: `ws://{controller_host}:{port}/v3/projects/{project_id}/nodes/{node_id}/console/vnc?token={jwt_token}`
**Authentication**:
- JWT token via query parameter
- User must have `Node.Console` privilege
**WebSocket Subprotocols**:
- Accepts: `binary`
- Used by noVNC for binary data transfer
**Request Example**:
```javascript
const token = "eyJ0eXAiOiJKV1QiLCJhbGc...";
const wsUrl = `ws://localhost:3080/v3/projects/${projectId}/nodes/${nodeId}/console/vnc?token=${token}`;
const ws = new WebSocket(wsUrl, 'binary');
ws.binaryType = 'arraybuffer';
```
### Compute WebSocket Endpoint
**URL**: `ws://{compute_host}:{port}/v3/compute/projects/{project_id}/{node_type}/nodes/{node_id}/console/vnc`
**Authentication**:
- HTTP Basic Auth via `ws_compute_authentication()` dependency
- Configured via `settings.Server.compute_username` (default: `gns3`) and `settings.Server.compute_password` (default: empty)
**Response**:
- Bidirectional binary WebSocket connection
- Transparent VNC protocol forwarding
## Supported Node Types
### QEMU VMs
**Console Type Configuration**:
```json
{
"console_type": "vnc",
"console": 5900,
"console_resolution": "1024x768"
}
```
**QEMU Parameters**:
```bash
-vnc :0 # VNC on display 0 (port = 5900 + display)
```
**Implementation**:
- File: `gns3server/api/routes/compute/qemu_nodes.py`
- Endpoint: `/{node_id}/console/vnc`
- Method: `start_vnc_websocket_console(websocket)`
### Docker Containers
**Console Type Configuration**:
```json
{
"console_type": "vnc",
"console": 5900,
"console_resolution": "1024x768",
"console_http_port": 8080,
"console_http_path": "/"
}
```
**Implementation**:
- File: `gns3server/api/routes/compute/docker_nodes.py`
- Endpoint: `/{node_id}/console/vnc`
- Method: `start_vnc_websocket_console(websocket)`
## WebSocket Data Forwarding
### Compute Layer (WebSocket ↔ TCP)
**Location**: `gns3server/compute/base_node.py` — `start_vnc_websocket_console()`
1. Validates node is started and `console_type == "vnc"`; closes WebSocket with code 1000 otherwise
2. Opens TCP connection to VNC server at `console_host:console_port`
3. Runs two concurrent tasks via `asyncio.wait(FIRST_COMPLETED)`:
- `ws_forward()`: WebSocket → TCP (catches `WebSocketDisconnect`)
- `vnc_forward()`: TCP → WebSocket (reads 65536-byte buffer)
4. Cancels pending tasks, closes TCP writer on completion
### Controller Layer (WebSocket ↔ WebSocket)
**Location**: `gns3server/api/routes/controller/nodes.py` — `vnc_console()`
1. Authenticates user via `has_privilege_on_websocket("Node.Console")` dependency
2. Constructs compute URL with IPv6 bracket handling
3. Connects to compute WebSocket using `aiohttp.ws_connect()` with HTTP Basic Auth and SSL context
4. Uses `asyncio.ensure_future()` for client→compute forwarding, `async for msg` iteration for compute→client
### Data Flow
```mermaid
sequenceDiagram
participant B as Browser (noVNC)
participant C as Controller
participant W as Compute
participant V as VNC Server
B->>C: WebSocket connect (binary, JWT token)
C->>W: WebSocket connect (binary, BasicAuth, SSL)
W->>V: TCP connect (asyncio.open_connection)
Note over B,V: Bidirectional binary forwarding active
B->>C: WebSocket binary frame
C->>W: aiohttp send_bytes()
W->>V: TCP write(data)
V->>W: TCP data (read 65536)
W->>C: WebSocket binary frame
C->>B: send_bytes()
```
## Authentication & Authorization
### Controller Layer
1. **Authentication**:
- JWT token validation via `has_privilege_on_websocket("Node.Console")` dependency
- Token passed as query parameter: `?token={jwt}`
2. **Authorization**:
- RBAC privilege check: `Node.Console`
- Per-node access control
3. **WebSocket Subprotocol**:
- Client requests: `binary`
- Server accepts: `binary` (if requested)
### Compute Layer
1. **Authentication**:
- HTTP Basic Auth
- Credentials from controller config
- Username: `settings.Server.compute_username`
- Password: `settings.Server.compute_password`
2. **Node Validation**:
- Check node exists
- Check node is started
- Check console type is `vnc`
## Configuration
### Server Settings
**Controller Configuration** (`gns3-server.conf`):
```ini
[Server]
host = 0.0.0.0
port = 3080
```
**Compute Configuration** (same file):
```ini
[Server]
compute_username = gns3
compute_password = # empty by default, must be set for compute auth
```
### VNC Port Range
VNC console ports are allocated from a configurable range (`gns3server/schemas/config.py`):
| Setting | Default | Range |
|---------|---------|-------|
| `vnc_console_start_port_range` | 5900 | 5900–65535 |
| `vnc_console_end_port_range` | 10000 | 5900–65535 |
Validation: `vnc_console_end_port_range` must be greater than `vnc_console_start_port_range`.
### Node Settings
**QEMU VM Example**:
```json
{
"name": "vm-1",
"node_type": "qemu",
"console_type": "vnc",
"console": 5900,
"console_resolution": "1280x720",
"properties": {
"qemu_path": "/usr/bin/qemu-system-x86_64"
}
}
```
**Docker Container Example**:
```json
{
"name": "container-1",
"node_type": "docker",
"console_type": "vnc",
"console": 5900,
"console_resolution": "1024x768",
"console_http_port": 8080
}
```
## Troubleshooting
### Common Issues
**1. "Node is not started"**
- **Symptom**: WebSocket closes immediately
- **Solution**: Start the VM before opening console
- **API Check**: `GET /v3/projects/{project_id}/nodes/{node_id}` → verify `status == "started"`
**2. "Console type is not vnc"**
- **Symptom**: WebSocket closes with error
- **Solution**: Set node console_type to "vnc"
- **API Update**: `PUT /v3/projects/{project_id}/nodes/{node_id}` with `{"console_type": "vnc"}`
**3. "Cannot connect to VNC server"**
- **Symptom**: Connection timeout
- **Possible Causes**:
- VNC server not listening
- Port conflict
- Firewall blocking connection
- **Verification**:
```bash
# Check if VNC is listening
netstat -tlnp | grep 5900
# Check QEMU process
ps aux | grep qemu
```
**4. WebSocket Subprotocol Negotiation Failed**
- **Symptom**: Connection rejected during handshake
- **Solution**: Ensure noVNC sends `binary` subprotocol
- **Browser Console Check**:
```javascript
console.log(ws.protocol); // Should be "binary"
```
**5. Authentication Errors**
- **Symptom**: HTTP 401/403 responses
- **Controller**: Check JWT token is valid and not expired
- **Compute**: Verify compute username/password in config
### Debug Logging
**Enable Detailed Logging**:
```ini
[gns3server]
debug = true
```
**Check Controller Logs**:
```bash
# Look for WebSocket connection messages
grep "VNC console WebSocket" /var/log/gns3/gns3.log
```
**Check Compute Logs**:
```bash
# Look for VNC forwarding messages
grep "Connected to VNC server" /var/log/gns3/gns3.log
```
**Browser Console**:
```javascript
// Enable noVNC debugging
RFB.messages.log = function(msg) { console.log(msg); };
```
## Security
### Authentication
1. **Controller → Client**
- JWT token with expiration
- RBAC authorization
- Privilege: `Node.Console`
2. **Controller → Compute**
- HTTP Basic Auth via `aiohttp.BasicAuth`
- SSL context from `Controller.instance().ssl_context()`
- Raises `ControllerForbiddenError` if `compute_username` is not set
3. **VNC Server**
- Optional VNC password (QEMU only)
- Configured via node properties
### Network Security
**Recommendations**:
1. Use HTTPS/WSS for production deployments
2. Firewall compute API ports
3. Use short-lived JWT tokens
4. Enable VNC password for sensitive VMs
**Example**:
```bash
# Controller with TLS
gns3server --ssl --cert /path/to/cert.pem --key /path/to/key.pem
# VNC with password
qemu-system-x86_64 -vnc :0,password
```
### WebSocket Security
**Subprotocol Validation**:
- Only `binary` subprotocol is accepted
- Prevents protocol downgrade attacks
**Origin Validation**:
- Browser enforces same-origin policy
- Configure CORS if using separate domains
## Limitations
1. **Single Client per VNC Port**
- VNC protocol supports one client at a time
- New connections disconnect existing clients
2. **No Audio Redirection**
- VNC protocol does not support audio
3. **No USB Redirection**
- VNC protocol does not support device redirection
4. **Performance Overhead**
- WebSocket-to-TCP bridging adds processing overhead compared to native VNC clients
## References
- [RFB Protocol 3.8](https://tools.ietf.org/html/rfc6143) - VNC Protocol Specification
- [noVNC Documentation](https://github.com/novnc/noVNC) - HTML5 VNC Client
- [FastAPI WebSocket](https://fastapi.tiangolo.com/advanced/websockets/) - WebSocket Implementation
- [GNS3 Documentation](https://docs.gns3.com/) - General GNS3 Documentation
## Version History
| Version | Date | Changes |
|---------|------|---------|
| 1.1 | 2026-04-19 | Review against code: fix buffer size, auth details, add port range config, remove unverified content |
| 1.0 | 2026-03-17 | Initial VNC WebSocket documentation |
@@ -0,0 +1,620 @@
<!--
SPDX-License-Identifier: CC-BY-SA-4.0
See LICENSE file for licensing information.
-->
> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.
# Web Wireshark Feature - Business Process Documentation
## Overview
The **Web Wireshark** feature enables users to run Wireshark packet capture analysis directly in a web browser without requiring a desktop environment or VNC connection. This is achieved through an **xpra** (persistent remote applications) HTML5 client running inside a Docker container.
## Feature Summary
- **Core Capability**: Web-based packet capture visualization using Wireshark
- **Technology Stack**: Docker container + xpra + HTML5 WebSocket proxy
- **Target Users**: Network engineers and students who need to analyze network traffic in GNS3 topologies
- **Key Benefit**: Zero-install, browser-based packet capture analysis
---
## Installation
Before using Web Wireshark, install the GNS3 server and set up the Docker image:
```bash
# Development install
pip install -e . && gns3server-web-wireshark-setup
# Production install
pip install gns3-server && gns3server-web-wireshark-setup
```
This command will:
1. Install the gns3-server package
2. Pull the `gns3/web-wireshark:latest` image from Docker Hub
3. If pull fails, build the image locally using the included Dockerfile
The `wireshark` command shows the raw output from `docker pull` or `docker build`, allowing you to see the full progress.
---
## Architecture Overview
```mermaid
graph TD
subgraph GNS3Server["GNS3 Server"]
WebUI["Web UI<br/>(Browser)"]
Controller["GNS3 Controller"]
LinkCtrl["Link Controller<br/>(start_capture / stop_capture)"]
Manager["WebWiresharkManager<br/>(called directly, not via CLI)"]
DockerClient["DockerHTTPClient<br/>(HTTP via Unix Socket)"]
end
subgraph DockerDaemon["Docker Daemon"]
Container["gns3-wireshark-{project_id}"]
Xvfb["Xvfb (virtual framebuffer)"]
Xpra["xpra server"]
Wireshark["Wireshark"]
end
ClientBrowser["Client Browser<br/>(HTML5 Client)"]
ClientBrowser -->|REST API| WebUI
WebUI -->|capture/start| Controller
Controller --> LinkCtrl
LinkCtrl -->|start/stop/restart session| Manager
Manager -->|Docker API<br/>/var/run/docker.sock| DockerClient
DockerClient -->|container lifecycle| Container
Container --- Xvfb
Container --- Xpra
Container --- Wireshark
ClientBrowser -.->|WebSocket proxy| Xpra
Xpra -.->|xpra HTML5| ClientBrowser
style GNS3Server fill:#e8f4fd,stroke:#2196f3
style DockerDaemon fill:#fff3e0,stroke:#ff9800
style ClientBrowser fill:#e8f5e9,stroke:#4caf50
```
---
## Component Description
### 1. Web UI (Client Browser)
- User interface for starting/stopping packet capture
- Receives WebSocket URL for connecting to xpra HTML5 client
- No plugins required - pure HTML5/JavaScript
### 2. GNS3 Controller
- Orchestrates the capture workflow
- Validates user permissions (RBAC)
- Manages link capture state
### 3. manage_wireshark.py (Management CLI)
- Command-line interface for container and session management
- **For manual debugging and testing only** — the server calls `WebWiresharkManager` directly at runtime
- Handles Docker container lifecycle
- Manages xpra sessions per link
### 4. WebWiresharkManager
- Core business logic for Web Wireshark
- Handles container creation, session startup/shutdown
- Deterministic port allocation based on link_id
### 5. DockerHTTPClient
- Async HTTP client for Docker API
- Communicates via Unix socket (/var/run/docker.sock)
- Manages container lifecycle
### 6. Docker Container (gns3-wireshark-{project_id})
- Runs xpra server with HTML5 support
- Contains Xvfb (virtual framebuffer) for headless Wireshark
- Streams display to browser via WebSocket
---
## Business Processes
### Process 1: Start Packet Capture with Web Wireshark
```mermaid
sequenceDiagram
actor User
participant WebUI as Web UI
participant Controller as Controller
participant LinkCtrl as Link Controller
participant Manager as WebWiresharkManager
User->>WebUI: 1. Start Capture
WebUI->>Controller: 2. POST /capture/start
Controller->>LinkCtrl: 3. _start_web_wireshark(jwt_token)
LinkCtrl->>Manager: 4. start_wireshark_session()
Note right of Manager: 5. Ensure network exists
Note right of Manager: 6. Get/create container
Note right of Manager: 7. Start xpra session<br/>(display + port)
Note right of Manager: 8. Start Wireshark<br/>(curl | wireshark -i - -k)
Manager-->>LinkCtrl: ws_url
LinkCtrl-->>Controller: ws_url
Controller-->>WebUI: 10. {capturing: true, ws_url: "ws://..."}
WebUI-->>User: 9. ws_url
Note over User,Manager: 11-12. Browser connects via WebSocket<br/>Server proxies to container xpra
```
**API Endpoint**: `POST /v3/projects/{project_id}/links/{link_id}/capture/start`
**Request Body**:
```json
{
"wireshark": true,
"data_link_type": "DLT_EN10MB",
"capture_file_name": "capture.pcap"
}
```
**Response**:
```json
{
"id": "link-uuid",
"capturing": true,
"ws_url": "ws://192.168.1.100:14500"
}
```
### Process 2: Container Lifecycle (Per Project)
```mermaid
flowchart TD
A["Project Open"] --> B["First Link Capture"]
B --> C["Container Created<br/>(one per project)"]
C --> D["Container Running"]
subgraph SessionArchitecture["Container Session Architecture"]
direction TB
D --> E["gns3-wireshark-{project_id}"]
subgraph Link1Session["Link 1 Session"]
L1Xpra["xpra :14503"]
L1Xvfb["Xvfb :14503<br/>(1920x1080x24)"]
L1WS["Wireshark<br/>(curl | wireshark -i -)"]
L1Bind["bind-ws=0.0.0.0:14503"]
L1Xpra --- L1Xvfb
L1Xpra --- L1WS
L1Xpra --- L1Bind
end
subgraph Link2Session["Link 2 Session"]
L2Xpra["xpra :11024"]
L2Xvfb["Xvfb :11024<br/>(1920x1080x24)"]
L2WS["Wireshark<br/>(curl | wireshark -i -)"]
L2Bind["bind-ws=0.0.0.0:11024"]
L2Xpra --- L2Xvfb
L2Xpra --- L2WS
L2Xpra --- L2Bind
end
E --> Link1Session
E --> Link2Session
Note1["display = port = 10000 + hash(link_id) % 10000<br/>Each xpra creates its own Xvfb (NOT shared)"]
end
D --> F["Project Close"]
F --> G["Stop Container<br/>(all sessions terminate)"]
G --> H["Container Stopped<br/>(preserved for reuse)"]
H -->|Project Reopened| D
I["Project Delete"] --> J["Delete Container"]
J --> K["Container Removed"]
style SessionArchitecture fill:#fff8e1,stroke:#ffa000
style Link1Session fill:#e3f2fd,stroke:#1976d2
style Link2Session fill:#e8f5e9,stroke:#388e3c
```
### Process 3: WebSocket Connection Flow
```mermaid
sequenceDiagram
participant Browser as Client Browser
participant Server as GNS3 Server (API Route)
participant Container as Docker Container (xpra)
Browser->>Server: 1. WSS Connect (ws://.../web-wireshark?token=jwt)
Note right of Server: 2. Validate JWT<br/>(RBAC: Link.Capture)
Note right of Server: 3. Get container IP<br/>from Docker API
Server-->>Browser: 4. Accept connection
Note right of Server: 5. Start WebSocket proxy
Server->>Container: 6. Connect to xpra<br/>ws://container:port
Container-->>Server: 7. xpra validates subprotocol
Note over Browser,Container: 8-9. Bidirectional WebSocket proxy<br/>Browser ⟺ Server ⟺ Container
Note left of Browser: 10. HTML5 client renders<br/>Wireshark window
```
**WebSocket Endpoint**: `ws://host/v3/projects/{project_id}/links/{link_id}/capture/web-wireshark?token=<jwt_token>`
### Process 4: Capture Data Flow
```mermaid
flowchart LR
subgraph GNS3Node["GNS3 Node"]
Router["Router / Switch"]
end
subgraph ComputeNode["Compute Node"]
Capture["Link Capture Buffer"]
end
Router -->|"TAP (raw packets)"| Capture
subgraph GNS3Server["GNS3 Server"]
APIRoute["GET /capture/stream<br/>Proxies pcap stream"]
end
Capture -->|"pcap stream"| APIRoute
subgraph DockerContainer["Docker Container"]
direction TB
Curl["curl -N -H 'Authorization: Bearer {jwt}'<br/>'{server}/capture/stream'"]
Wireshark["wireshark -i - -k"]
Xvfb["Xvfb :display<br/>(1920x1080x24)"]
Xpra["xpra :display<br/>ws://0.0.0.0:{port}"]
Curl -->|"stdin pipe"| Wireshark
Wireshark -->|"renders to"| Xvfb
Xvfb -->|"X11 display"| Xpra
end
APIRoute -.->|"pcap stream<br/>(curl fetches from container)"| Curl
subgraph Browser["Client Browser"]
HTML5Client["HTML5/xpra Client"]
WSUI["Wireshark Web Interface"]
HTML5Client --> WSUI
end
Xpra <-->|"WebSocket"| HTML5Client
style GNS3Node fill:#f3e5f5,stroke:#7b1fa2
style ComputeNode fill:#e8eaf6,stroke:#283593
style GNS3Server fill:#e8f4fd,stroke:#2196f3
style DockerContainer fill:#fff3e0,stroke:#ff9800
style Browser fill:#e8f5e9,stroke:#4caf50
```
---
## Port Allocation Strategy
### Deterministic Port Mapping
```mermaid
flowchart LR
LinkID["link_id (UUID)"] --> Hash["MD5 Hash"] --> Modulo["hash % 10000"]
Modulo --> Offset["+ 10000"]
Offset --> Result["display = port<br/>Range: 10000 - 19999"]
style LinkID fill:#e3f2fd,stroke:#1976d2
style Result fill:#e8f5e9,stroke:#388e3c
```
| link_id | port/display |
|---------|-------------|
| `f233f27f-7432-49c3-9aa2-50e326a10eec` | 14503 |
| `a1b2c3d4-1234-5678-90ab-cdef12345678` | 11024 |
| `12345678-90ab-cdef-1234-567890abcdef` | 17892 |
**Benefits**:
- Same link always gets same port (deterministic)
- Display number = port number (same hash)
- No port conflicts between sessions
- Easy to predict and debug
---
## Network Architecture
```mermaid
graph TD
subgraph HostMachine["Host Machine"]
subgraph GNS3ServerHost["GNS3 Server<br/>192.168.1.100:3080"]
WSProxy["WebSocket Proxy"]
end
subgraph DockerNetwork["Docker Bridge: gns3-wireshark<br/>Subnet: 172.31.0.0/22"]
Gateway["Bridge Gateway<br/>172.31.0.1"]
Container["gns3-wireshark-{project_id}<br/>172.31.0.x"]
Gateway --- Container
end
end
Client["Client Browser"] -->|"ws://192.168.1.100:3080<br/>/capture/web-wireshark"| WSProxy
WSProxy -->|"via gateway 172.31.0.1<br/>ws://172.31.0.x:{port}"| Container
style HostMachine fill:#fafafa,stroke:#9e9e9e
style DockerNetwork fill:#e3f2fd,stroke:#1976d2
style GNS3ServerHost fill:#e8f5e9,stroke:#388e3c
```
---
## Session Management Commands
> **Note**: These commands are for manual debugging and testing only. The GNS3 server calls `WebWiresharkManager` directly at runtime.
### start
Starts Web Wireshark session for a specific link.
| Argument | Required | Default | Description |
|----------|----------|---------|-------------|
| `--project-id` | Yes | - | Project UUID |
| `--link-id` | Yes | - | Link UUID |
| `--jwt-token` | Yes | - | JWT authentication token |
| `--capture-url` | No | auto-detected | PCAP stream URL |
| `--image` | No | `gns3/web-wireshark:latest` | Docker image |
| `--memory` | No | `2g` | Memory limit |
| `--cpus` | No | `1.0` | CPU cores |
| `--pids-limit` | No | `1000` | Process limit |
```bash
python manage_wireshark.py start \
--project-id "5af0fe00-..." \
--link-id "f233f27f-..." \
--jwt-token "eyJhbG..."
```
### Other Commands
| Command | Description | Key Arguments |
|---------|-------------|---------------|
| `stop` | Stop session for a specific link | `--project-id`, `--link-id` |
| `restart` | Restart session (reopens Wireshark window) | `--project-id`, `--link-id`, `--jwt-token` |
| `stop-all` | Stop all sessions for a project | `--project-id` |
| `delete` | Delete container (alias for delete-container) | `--project-id` |
| `stop-container` | Stop container without deleting | `--project-id` |
| `delete-container` | Delete the container | `--project-id` |
---
## Project Close/Delete Workflow
```mermaid
flowchart TD
A["Project Close"] --> B["_stop_web_wireshark_container()"]
B --> C["WebWiresharkManager.stop_container(project_id)"]
C --> D["docker stop<br/>(all xpra/Xvfb/Wireshark terminate)"]
D --> E["Container Stopped<br/>(preserved for reuse)"]
E -->|Project Reopened| F["First Capture Start"]
F --> G["docker start<br/>(container already exists, fast)"]
G --> H["New xpra sessions created"]
I["Project Delete"] --> J["_cleanup_web_wireshark_container()"]
J --> K["WebWiresharkManager.delete_container(project_id)"]
K --> L["docker rm<br/>(container removed)"]
style A fill:#e3f2fd,stroke:#1976d2
style I fill:#ffebee,stroke:#c62828
style E fill:#fff8e1,stroke:#ffa000
style L fill:#ffebee,stroke:#c62828
```
---
## API Endpoints Summary
| Method | Endpoint | Description | Privilege |
|--------|----------|-------------|-----------|
| POST | `/v3/projects/{id}/links/{id}/capture/start` | Start capture with Web Wireshark | Link.Capture |
| POST | `/v3/projects/{id}/links/{id}/capture/stop` | Stop capture | Link.Capture |
| POST | `/v3/projects/{id}/links/{id}/capture/wireshark/restart` | Restart Wireshark window | Link.Capture |
| GET | `/v3/projects/{id}/links/{id}/capture/stream` | Stream PCAP data | Link.Capture |
| GET | `/v3/projects/{id}/links/{id}/capture/file` | Download PCAP file | Link.Capture |
| WS | `/v3/projects/{id}/links/{id}/capture/web-wireshark` | WebSocket proxy for xpra | Link.Capture |
---
## Security Considerations
1. **RBAC Authentication**: All endpoints require `Link.Capture` privilege
2. **JWT Token Validation**: WebSocket connections validate JWT token
3. **WebSocket Subprotocol Negotiation**: Proper xpra subprotocol handling
4. **Container Isolation**: Each project gets its own container with isolated resources
5. **Network Segmentation**: Container runs on isolated Docker network (not host network)
---
## Performance Characteristics
### Startup & Shutdown Performance
| Step | Before Optimization | After Optimization | Improvement |
|------|---------------------|-------------------|-------------|
| **Health Check** | ~1.0s | ~0s | Docker native status |
| **Gateway Detection** | 0.85s | ~0.001s | Docker API vs exec |
| **Process Cleanup** | ~850ms | ~40ms | Host perspective recursive tree walk |
| **Xpra Startup** | 6.4s | ~3s | HTML5 client disabled |
| **Wireshark Launch** | ~1s | ~1s | No change |
| **Total Startup** | **~15s** | **~5-6s** | **67% faster** |
| Step | Before Optimization | After Optimization | Improvement |
|------|---------------------|-------------------|-------------|
| **Process Termination** | ~8s | ~20-40ms | Recursive tree walk, no orphans |
| **File Cleanup** | ~850ms | ~850ms | Docker exec (safety) |
| **Total Shutdown** | **~9s** | **~2s** | **78% faster** |
#### Startup Breakdown: First vs Subsequent
| Phase | First Startup (Container stopped) | Subsequent Startup (Container running) |
|-------|-----------------------------------|----------------------------------------|
| Container startup | ~1-2s | ~0s (already running) |
| Container health check | ~1s (unhealthy→healthy) | ~0s (already healthy) |
| Gateway detection | ~0.001s | ~0.001s |
| Process cleanup | ~40ms | ~40ms |
| Xpra startup | ~3s | ~3s |
| Wireshark launch | ~1s | ~1s |
| **Total** | **~6s** | **~5s** |
#### Measured Performance Data
**Startup (from production logs):**
```
13:46:53 → 13:46:59 = 6s (first startup with container start)
13:47:38 → 13:47:43 = 5s (subsequent startup, container running)
```
**Shutdown (from production logs):**
```
13:48:15 → 13:48:17 = 2s (complete cleanup, no orphan processes)
13:48:50 → 13:48:52 = 2s (complete cleanup, no orphan processes)
```
#### Key Optimizations
- **Complete Process Cleanup**: Recursive process tree traversal eliminates orphaned processes (Xvfb, pulseaudio, ibus-daemon)
- **Fast Gateway Detection**: Docker API query instead of container exec
- **Smart Health Check**: Trust Docker built-in status, no manual ping
- **Xpra Optimization**: Disabled unnecessary HTML5 client (`--html=off`)
- **Reduced Docker Exec Calls**: Combined X lock and xpra socket cleanup into single exec call
### Docker Exec Performance Limitations
**Important**: Docker daemon has internal queuing for concurrent exec requests to the same container.
#### Test Results (Same Container)
| Test Scenario | Execution Time | Avg Per Exec |
|--------------|----------------|--------------|
| Single docker exec | 0.854s | 0.854s |
| Serial 7 docker exec | 7.225s | 1.032s |
| Parallel 7 docker exec | 10.253s | 1.465s |
**Key Finding**: Parallel execution is **42% slower** than serial execution.
```
Serial: 7.225s (7 requests processed sequentially)
Parallel: 10.253s (Docker daemon still processes sequentially + context switch overhead)
```
#### Impact on Web Wireshark
When stopping multiple capture sessions quickly:
- Each stop requires 1 docker exec (cleanup files)
- Docker daemon processes exec requests sequentially
- 7 sessions × ~1s each = ~7-10 seconds total
- User requests appear to "queue" even though they're concurrent
#### Optimization Strategy
Since Docker exec cannot be parallelized effectively:
1. **Minimize exec calls** - Already implemented: 2 calls → 1 call
2. **Accept serial processing** - No benefit to parallel execution
3. **Focus on fast exec content** - Use simple `rm -f` commands
#### Future Optimization Options
- Use Docker API instead of exec (requires container filesystem access)
- Delay cleanup to next startup (increases startup complexity)
- Batch multiple stops into single operation (requires API changes)
### Resource Usage (Per Wireshark Instance)
| Resource | Typical Usage |
|----------|---------------|
| Memory | 150-250 MB |
| CPU | 0.5-2% (idle to active) |
| Threads | ~30 threads |
| Disk I/O | Minimal |
### Container Configuration
Configured via `WebWiresharkSettings` in `gns3server/schemas/config.py`:
| Parameter | Default | Recommended | Description |
|-----------|---------|-------------|-------------|
| enabled | true | - | Enable/disable Web Wireshark feature |
| image | gns3/web-wireshark:latest | - | Docker image name |
| network_subnet | 172.31.0.0/22 | - | Docker bridge network subnet |
| Memory | 2GB | 2-4GB | Container memory limit |
| CPUs | 1.0 | 1.0-2.0 | Container CPU limit |
| PIDs Limit | 1000 | 1000 | Container process limit |
### Scaling Guidelines
| Instances | Memory | Use Case |
|-----------|--------|----------|
| 1-3 | 450-750 MB | Light projects |
| 4-6 | 600-1.5 GB | Medium projects |
| 7-10 | 1-2.5 GB | Large projects |
| 10+ | >2.5 GB | Increase memory |
---
## 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/<pid>/cmdline`)
2. **Single Container**: All Wireshark instances run in a single container per project
3. **Docker Dependency**: Requires Docker daemon running on the server
4. **Browser Support**: Requires modern browser with WebSocket support
5. **Port Range**: Limited to 10,000 unique ports (10000-19999)
---
## File Structure
```
gns3server/
├── controller/
│ ├── link.py # Link capture lifecycle
│ └── project.py # Project cleanup hooks
├── api/routes/controller/
│ └── links.py # REST/WebSocket API endpoints
└── agent/web_wireshark/
├── setup_wireshark_image.py # Docker image setup tool
├── manage_wireshark.py # CLI management tool (manual/debug use only)
├── manager.py # Session management logic (called by server)
├── docker_client.py # Docker API client
├── stats.py # Container statistics collection
├── docker/
│ └── Dockerfile # Container image definition
└── WEB_WIRESHARK.md # Technical documentation
```
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,224 @@
<!--
SPDX-License-Identifier: CC-BY-SA-4.0
See LICENSE file for licensing information.
-->
> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.
# Command Security Configuration
## Overview
GNS3-Copilot includes multiple security layers to prevent execution of commands that may cause issues in the lab environment:
- **Command Filtering**: Prevents commands that may timeout or lock up the console
- **Configuration Safety**: Restricts dangerous configuration changes (AAA, passwords, etc.)
- **Multi-line Command Handling**: Properly processes commands with embedded newlines (banner, etc.)
## Architecture
```mermaid
graph TD
A[Tool Request] --> B{Command Filter}
B -->|allowed| C[Nornir/Netmiko Execution]
B -->|blocked| D[blocked_commands_info]
C --> E[Process Results]
D --> E
E --> F[Return Result]
subgraph Command Filter
B1[Load forbidden_commands.txt]
B2[Prefix match per command]
B1 --> B2
end
subgraph Security Layers
S1[Layer 1: Command Filter<br/>prefix-based blocking]
S2[Layer 2: AI Prompt Rules<br/>AAA/password guidance only]
S3[Layer 3: Multi-line Expansion<br/>banner/ACL handling]
S1 --> S2 --> S3
end
```
## Business Process
### Command Filtering Flow
```mermaid
flowchart TD
Start([Tool receives commands]) --> Load[Load forbidden patterns<br/>from config file]
Load --> Loop{For each command}
Loop -->|next command| Prefix{Command starts with<br/>forbidden pattern?}
Prefix -->|yes| Block[Add to blocked_commands_info]
Prefix -->|no| Allow[Add to allowed list]
Block --> Loop
Allow --> Loop
Loop -->|all checked| Empty{Allowed list empty?}
Empty -->|yes| Return1[Return partial_success<br/>with blocked info only]
Empty -->|no| Exec[Execute allowed commands<br/>via Nornir/Netmiko]
Exec --> HasBlocked{Has blocked commands?}
HasBlocked -->|yes| Return2[Return partial_success<br/>with output + blocked info]
HasBlocked -->|no| Return3[Return success<br/>with output]
```
### Multi-line Command Expansion Flow
```mermaid
flowchart TD
A[config_tools receives commands] --> B{Command contains \n?}
B -->|yes| C[Split by newline]
C --> D[Filter empty lines]
D --> E[Add expanded lines to command list]
B -->|no| F[Keep command as-is]
E --> G[Execute via netmiko_send_config]
F --> G
```
## Tools Using Command Security
| Tool | File | Filter | Multi-line Expansion | Result Fields |
|------|------|--------|---------------------|---------------|
| `ExecuteMultipleDeviceCommands` | `display_tools_nornir.py` | Yes | No | `diagnostic_commands` |
| `ExecuteMultipleDeviceConfigCommands` | `config_tools_nornir.py` | Yes | Yes | `config_commands` |
## Command Filtering System
### Forbidden Commands Configuration
The forbidden commands list is loaded from the external [GNS3-Skills](https://github.com/yueguobin/GNS3-Skills) repository at `config/forbidden_commands.txt`.
**Format:**
- One command pattern per line
- **Prefix matching** (case-insensitive) — matches the beginning of each command
- Empty lines and lines starting with `#` are ignored
**Default patterns (used when skills repository is unavailable):**
| Pattern | Reason |
|---------|--------|
| `traceroute` | Can run 30+ seconds, exceeds tool timeout |
| `tracepath` | Similar to traceroute, long execution time |
| `tracert` | Windows traceroute, same timeout issues |
| `ping -f` | Flood ping can overwhelm lab devices |
| `debug` | Can produce overwhelming output and destabilize devices |
| `test` | May affect device stability |
### Result Format
**Status values:**
| Status | Condition |
|--------|-----------|
| `"success"` | All commands executed, none blocked |
| `"partial_success"` | Some commands blocked (including all blocked + execution succeeded) |
| `"failed"` | Execution failed (device not found, connection error, etc.) |
**Example — partial success (display tool):**
```json
{
"device_name": "R-1",
"status": "partial_success",
"output": "...",
"diagnostic_commands": ["show version", "show ip int brief"],
"blocked_commands": ["traceroute 8.8.8.8"],
"blocked_info": {
"traceroute 8.8.8.8": "Command 'traceroute 8.8.8.8' is not allowed because it matches the forbidden pattern 'traceroute'. ..."
}
}
```
> **Note:** `diagnostic_commands` is specific to display tools. Config tools use `config_commands` instead.
## Module Structure
**Command Filter:** `gns3server/agent/gns3_copilot/utils/command_filter.py`
| Function | Purpose |
|----------|---------|
| `filter_forbidden_commands(commands)` | Returns `(allowed_commands, blocked_commands_info)` |
| `is_command_forbidden(command)` | Check if a single command matches a forbidden pattern |
| `get_forbidden_commands()` | Get current forbidden patterns list |
| `reload_forbidden_commands()` | Directly load and cache commands from skills repository |
**Integration points:**
- `display_tools_nornir.py` — `_filter_forbidden_commands_from_device_configs()`
- `config_tools_nornir.py` — `_filter_forbidden_commands_from_device_configs()` + `_expand_multiline_commands()`
## Configuration Safety (AI Prompt Level)
In addition to the command filter, the AI agent is instructed via system prompt (`lab_automation_assistant_prompt.py`) to handle sensitive configuration commands with caution:
**Prompt-enforced rules:**
- **FORBIDDEN (guidance only):** `enable secret`, `username`, `aaa new-model`, `service password-encryption`, `line vty` — AI provides configuration guidance instead of executing
- **Caution required:** `reload`, `erase`, `format` — AI warns user before executing destructive operations
> These are prompt-level rules, not code-level enforcement. Users can always execute commands directly via device console or SSH/Telnet.
## Multi-line Command Handling
Commands containing `\n` are automatically split before execution by `config_tools_nornir.py:_expand_multiline_commands()`.
**Example:** `["banner motd #\nWelcome\n#"]` becomes `["banner motd #", "Welcome", "#"]`
Applies to any command with embedded newlines: `banner`, multi-line ACLs, route-maps, etc.
## Configuration
### Customizing Forbidden Commands
Edit `config/forbidden_commands.txt` in the [GNS3-Skills repository](https://github.com/yueguobin/GNS3-Skills) and push the changes, then call `POST /copilot/reload/skills` to apply them without restarting the server.
## Implementation Verification
### Test Results (Live GNS3 Environment)
**Scenario:** IOU-L2-1 (Cisco IOS L3 switch), mixed allowed/forbidden commands.
```json
{
"device_name": "IOU-L2-1",
"status": "partial_success",
"diagnostic_commands": [
"show ip route", "show ip interface brief",
"ping 10.0.0.1", "ping 10.0.0.2", "ping 10.0.0.4"
],
"blocked_commands": ["traceroute 10.0.0.2"],
"blocked_info": {
"traceroute 10.0.0.2": "Command 'traceroute 10.0.0.2' is not allowed because it matches the forbidden pattern 'traceroute'. ..."
}
}
```
| Feature | Status | Notes |
|---------|--------|-------|
| Prefix match filtering | Verified | `traceroute` correctly matched and blocked |
| Partial execution | Verified | Other commands executed successfully |
| `partial_success` status | Verified | Set correctly when blocked + succeeded |
| Multi-device support | Verified | Each device filtered independently |
| Non-blocking behavior | Verified | No tool timeouts or console issues |
## Future Enhancements
- **Regex support** for more sophisticated pattern matching
- **Per-project override files** for forbidden commands
- **Web UI configuration** for managing forbidden commands
- **Audit logging** for blocked command analysis
- **Per-command timeouts** instead of blocking
- **Interrupt mechanism** (Ctrl+C) for long-running commands
## Troubleshooting
| Problem | Solution |
|---------|----------|
| Command blocked unexpectedly | Check `blocked_commands` in result, identify matching pattern, edit `forbidden_commands.txt` |
| "File not found, using defaults" | Verify `config/forbidden_commands.txt` exists and is readable |
| Changes not taking effect | Call `POST /copilot/reload/skills` or restart server |
## Related Documentation
- [Skills Repository](skills-repository.md)
- [Fault Injection](fault-injection.md)
- [Chat API](chat-api.md)
@@ -0,0 +1,203 @@
<!--
SPDX-License-Identifier: CC-BY-SA-4.0
See LICENSE file for licensing information.
-->
> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.
# LLM Context Window Management
## Overview
GNS3 Copilot's context window management prevents LLM context overflow by trimming conversation history to fit within configured token limits. It uses tiktoken for accurate token counting, supports three context strategies (conservative/balanced/aggressive), and automatically injects GNS3 topology information into the system prompt before each LLM call.
## Architecture
```mermaid
graph TD
subgraph "context_manager.py"
A[create_pre_model_hook] --> B[_inject_topology_into_system]
A --> C[estimate_tool_tokens]
A --> D[trim_messages via LangChain]
E[count_tokens via tiktoken]
E --> C
E --> F[_count_tokens_for_message]
F --> D
end
subgraph "gns3_copilot.py"
G[llm_call node] --> H[Get topology via GNS3TopologyTool]
H --> A
A --> I[model_with_tools.invoke]
end
style A fill:#4a9eff,color:#fff
style D fill:#ff6b6b,color:#fff
style E fill:#51cf66,color:#fff
```
## Key Components
| Function | File | Purpose |
|----------|------|---------|
| `create_pre_model_hook()` | `context_manager.py` | Factory that creates the preprocessing hook |
| `_inject_topology_into_system()` | `context_manager.py` | Injects topology into system prompt via `{{topology_info}}` placeholder |
| `count_tokens()` | `context_manager.py` | Accurate token counting using tiktoken (`cl100k_base`) |
| `estimate_tool_tokens()` | `context_manager.py` | Estimates token cost of tool definitions (JSON serialization) |
| `_count_tokens_for_message()` | `context_manager.py` | Token counter adapter for LangChain's `trim_messages` |
| `llm_call()` | `gns3_copilot.py` | StateGraph node that orchestrates the full LLM call pipeline |
**Token Counting**: Uses tiktoken `cl100k_base` encoding (GPT-4 compatible, 95%+ accuracy for most modern LLMs).
**tiktoken Cache**: Encoding files are cached locally at `<agent_package>/cache/tiktoken/` to avoid repeated downloads. This is configured before tiktoken import via `TIKTOKEN_CACHE_DIR` environment variable.
**Required Dependency**: `tiktoken>=0.8.0` — if not installed, `ModuleNotFoundError` is raised at startup.
## Trimming Process
```mermaid
flowchart TD
A[llm_call node invoked] --> B[Get topology via GNS3TopologyTool]
B --> C[Create pre_model_hook]
C --> D[Inject topology into system prompt]
D --> E{topology data available?}
E -->|Yes| F[Replace placeholder with topology content]
E -->|No| G[Replace placeholder with 'No topology information available']
F --> H[Estimate tool definition tokens]
G --> H
H --> I[Calculate trim_messages budget]
I --> J[trim_messages with strategy='last']
J --> K[Return prepared messages]
K --> L[model.invoke with prepared messages]
```
### Token Budget Calculation
```mermaid
flowchart LR
A["context_limit<br/>(e.g. 128K)"] -->|"× strategy_ratio"| B["Input Budget<br/>(e.g. 96K @ balanced)"]
B -->|"− tool_tokens"| C["trim_messages Budget<br/>(includes system message)"]
C --> D["trim_messages<br/>max_tokens parameter"]
```
**Key**: `max_tokens` passed to `trim_messages` **includes** the system message. System tokens are NOT subtracted separately — `trim_messages` handles system message preservation via `include_system=True`.
### Budget Calculation Example
| Step | Calculation | Result |
|------|------------|--------|
| Context limit | 128 (configured) | 128K tokens |
| Strategy ratio | balanced = 0.75 | 128 × 1000 × 0.75 = 96,000 |
| Tool definitions | estimated from JSON schema | ~1,725 tokens |
| trim_messages budget | 96,000 − 1,725 | **94,275 tokens** (includes system) |
### Trimming Priority
| Priority | Content | Behavior |
|----------|---------|----------|
| 1 | System Message (system prompt + topology) | Always kept (`include_system=True`) |
| 2 | Latest conversation messages | Kept from newest to oldest |
| 3 | Older conversation history | Discarded first when exceeding budget |
**Note**: System prompt and topology are merged into a single `SystemMessage` via the `{{topology_info}}` template variable and cannot be trimmed independently.
### Edge Case Handling
| Scenario | Behavior |
|----------|----------|
| System + tools > input budget | ERROR log with recommendations (reduce prompt/tools, use larger model, switch to conservative). `trim_messages` is still called — `include_system=True` ensures system message is kept, but no history messages will fit. |
| Trim budget < system × 1.5 | WARNING log: minimal room for conversation history |
| Invalid context_strategy | Falls back to `balanced` with WARNING log |
| `trim_messages` throws exception | Returns untrimmed messages (graceful degradation) |
| Missing `{{topology_info}}` placeholder | Returns original messages without injection, WARNING log |
| Topology retrieval fails | Injects `"(No topology information available)"` placeholder |
## Integration with GNS3 Copilot
**File**: `gns3server/agent/gns3_copilot/agent/gns3_copilot.py`
### Why Direct Call, Not Config-based?
The agent uses a **custom StateGraph** (`StateGraph(MessagesState)`), not LangGraph's pre-built agent. LangGraph's `pre_model_hook` config parameter only works with pre-built agents like `create_react_agent`.
| Agent Type | pre_model_hook Support |
|------------|------------------------|
| `create_react_agent` | Via `config={"configurable": {"pre_model_hook": ...}}` |
| `chat_agent_executor` | Via `config={"configurable": {"pre_model_hook": ...}}` |
| **Custom StateGraph** | **Not supported** — must call directly |
### Execution Flow
In the `llm_call` node of `gns3_copilot.py`:
1. Get `llm_config` from request-scoped context variable
2. Get `project_id` from `config["configurable"]`
3. Retrieve topology via `GNS3TopologyTool._run(project_id)`
4. Select tools based on `copilot_mode` (`teaching_assistant` vs `lab_automation_assistant`)
5. Load system prompt and create `pre_model_hook`
6. **Directly call** `pre_hook({"messages": messages, "topology_info": topology_info})` to prepare messages
7. Invoke `model_with_tools.invoke(prepared_messages)`
> **Note**: The actual `llm_call` function also includes defensive checks for missing config, empty messages, and topology retrieval errors — see source code for full details.
## Strategy Implementation
### Context Strategy Ratios
Defined as `CONTEXT_STRATEGY_RATIOS` in `context_manager.py`, with default `DEFAULT_CONTEXT_STRATEGY = "balanced"`.
| Strategy | Input Ratio | Output Reserved | Calculation |
|----------|-------------|-----------------|-------------|
| Conservative | 60% | 40% | `context_limit × 1000 × 0.60` |
| Balanced | 75% | 25% | `context_limit × 1000 × 0.75` |
| Aggressive | 85% | 15% | `context_limit × 1000 × 0.85` |
## Log Output
### Normal Case (topology injected)
```
INFO: Calling pre_hook to prepare 1 messages
INFO: ✓ Topology injected: 7722 chars, nodes: ['netshoot-1', 'R1', 'R2', 'IOU-L3-1', 'IOU-L3-2']
INFO: Context ready: 2 msgs, ~3815 tokens + 1725 tools = 5540 / 128K (4.3%), strategy=conservative
INFO: Messages prepared: 1 → 2
INFO: LLM call completed: tool_calls=0
```
### When Trimming Occurs
```
INFO: Calling pre_hook to prepare 50 messages
INFO: ✓ Topology injected: 8500 chars, nodes: ['R1', 'R2', ...]
INFO: Messages trimmed: 50 → 25 msgs. Total: ~82000 tokens + 1725 tools = 83725 / 128K (65.4%), strategy=balanced
INFO: Messages prepared: 50 → 25
```
### When Topology is None
```
INFO: Calling pre_hook to prepare 1 messages
WARNING: ✗ Topology data is None, injecting placeholder
INFO: Context ready: 2 msgs, ~800 tokens + 1725 tools = 2525 / 128K (2.0%), strategy=balanced
```
## Error Handling
| Error | Behavior | Resolution |
|-------|----------|------------|
| tiktoken not installed | `ModuleNotFoundError` at startup | `pip install tiktoken>=0.8.0` |
| `context_limit` missing | `ValueError` raised | Add `context_limit` to LLM config |
| Invalid `context_limit` | `ValueError` raised | Must be positive integer |
| Invalid `context_strategy` | WARNING, fallback to `balanced` | Use `conservative`/`balanced`/`aggressive` |
| `trim_messages` failure | ERROR log, returns untrimmed messages | Check message format compatibility |
## Deprecated API
`prepare_context_messages()` is deprecated. It remains in `context_manager.py` for backward compatibility but emits `DeprecationWarning`. Use `create_pre_model_hook()` instead.
## Related Source Files
- `gns3server/agent/gns3_copilot/agent/context_manager.py` — Context management core logic
- `gns3server/agent/gns3_copilot/agent/gns3_copilot.py` — LLM call node (StateGraph)
- `gns3server/agent/gns3_copilot/agent/model_factory.py` — Model creation and tool binding
@@ -0,0 +1,159 @@
<!--
SPDX-License-Identifier: CC-BY-SA-4.0
See LICENSE file for licensing information.
-->
> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.
# Fault Injection Feature
## Overview
The fault injection feature enables GNS3 Copilot to automatically inject realistic network faults into GNS3 labs for troubleshooting training. The agent analyzes the topology, selects an appropriate fault based on the configured protocols, injects it, and documents the results.
## Architecture
```mermaid
graph TD
subgraph "Agent Workflow"
TG[Topology Gathering]
CFG[Config Collection]
FA[Fault Analysis]
FI[Fault Injection]
FD[Fault Documentation]
end
subgraph "Skills Repository"
IS[INJECTION_SKILLS_REGISTRY<br/>39 categories, 368 issues]
SK[injection_skills Tool]
end
subgraph "Tools"
DC[execute_multiple_device_commands]
CC[execute_multiple_device_config_commands]
GK[GNS3TopologyTool]
end
TG -->|Get topology| GK
CFG -->|Get configs| DC
FA -->|Query filtered skills| IS
FA -->|Select fault| SK
FI -->|Inject config changes| CC
FD -->|Output report| FD
```
## Injection Skills Tool
The `InjectionSkillsTool` (LangChain `BaseTool`) is the primary interface for querying available faults.
### Listing Faults (with context filter)
The LLM MUST pass topology context when listing faults:
```json
{"action": "list", "context": ["ospf", "bgp", "vlan"]}
```
This returns only faults matching the protocols found in the topology. The tool rejects calls without `context`:
```json
{
"error": "context parameter is required when action='list'",
"hint": "Analyze the topology and device configurations first...",
"available_categories": ["bgp", "interface", "mpls", "ospf", ...]
}
```
### Getting Fault Details
Token-efficient usage pattern:
```json
// Step 1: List issue names only (~300 tokens)
{"device_type": "injection_ospf", "detail": "index"}
// Step 2: Get single issue detail (~500 tokens)
{"device_type": "injection_ospf", "issue": "ospf_hello_dead_mismatch"}
```
### Parameters
| Parameter | Required | Description |
|-----------|----------|-------------|
| `action` | No (`"get"`) | `"list"` to browse, `"get"` for details |
| `context` | **Yes** for `action="list"` | Protocols found in topology: `["ospf", "bgp"]` |
| `device_type` | Yes for `action="get"` | e.g. `"injection_ospf"` |
| `detail` | No (`"full"`) | `"index"` (names), `"summary"` (+desc), `"full"` (all) |
| `issue` | No | Single issue key for targeted detail |
## Fault Injection Workflow
```mermaid
sequenceDiagram
participant LLM
participant InjectionSkillsTool
participant TopologyTool
participant DeviceCommands
LLM->>TopologyTool: Get topology info
TopologyTool-->>LLM: Topology + device types
LLM->>DeviceCommands: Get device configs
DeviceCommands-->>LLM: Running configurations
Note over LLM: Analyze which protocols are in use
LLM->>InjectionSkillsTool: {"action": "list", "context": ["ospf", "vlan"]}
InjectionSkillsTool-->>LLM: Matching fault categories
LLM->>InjectionSkillsTool: {"device_type": "injection_ospf", "detail": "index"}
InjectionSkillsTool-->>LLM: Issue names (token-efficient)
LLM->>InjectionSkillsTool: {"device_type": "injection_ospf", "issue": "ospf_hello_dead_mismatch"}
InjectionSkillsTool-->>LLM: Full fault detail + config commands
LLM->>DeviceCommands: Inject fault configuration
DeviceCommands-->>LLM: Execution result
Note over LLM: Document fault in response
```
## Injection Skills Repository
Skills are organized by protocol/category in the external [GNS3-Skills](https://github.com/yueguobin/GNS3-Skills) repository:
| Category | File | Example Issues |
|----------|------|----------------|
| OSPF | `injection/ospf_issues.yaml` | Hello/Dead mismatch, MTU mismatch, area mismatch |
| BGP | `injection/bgp_issues.yaml` | AS-path prepend, next-hop unreachable, route filtering |
| VLAN | `injection/vlan_issues.yaml` | Trunk allowed mismatch, native VLAN mismatch |
| STP | `injection/stp_issues.yaml` | Root guard, loop guard, port priority |
| MPLS | `injection/mpls_issues.yaml` | LDP session down, label binding failure |
| ... | 34 more files | 368 issues total |
## Recovery
Each injected fault includes restore commands in the documentation. The LLM always provides commands to fully revert all changes.
## API Endpoint
### POST /copilot/projects/{project_id}/chat/inject
Dedicated endpoint for fault injection. Internally sets `copilot_mode` to `troubleshooting_injection` and runs the agent.
**Request:**
```json
{
"message": "Inject a network fault for troubleshooting practice",
"session_id": "optional-session-uuid"
}
```
**Response:** Server-Sent Events (SSE) stream identical to the chat stream endpoint.
## Related Documentation
- [Skills Repository](skills-repository.md)
- [Command Security](command-security.md)
- [Chat API](chat-api.md)
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,772 @@
<!--
SPDX-License-Identifier: CC-BY-SA-4.0
See LICENSE file for licensing information.
-->
> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.
# Multi-Vendor Network Device Support
## Overview
GNS3-Copilot supports network devices from multiple vendors through Netmiko and Nornir integration. The system includes a custom Netmiko driver for Huawei devices in GNS3 emulation environments and supports dynamic device type detection.
## Supported Vendors
| Vendor | Device Type | Protocol | Status |
|--------|-------------|----------|--------|
| **Cisco** | `cisco_ios_telnet` | Telnet | ✅ Tested |
| **Huawei** | `gns3_huawei_telnet_ce` | Telnet | ✅ Tested (Custom Driver) |
| **Ruijie (锐捷)** | `gns3_ruijie_telnet` | Telnet | ✅ Tested (Custom Driver) |
| **VPCS** | `gns3_vpcs_telnet` | Telnet | ✅ Tested (Custom Driver, Simulator) |
## Custom VPCS Driver (`VPCSTelnet`)
### Problem Statement
VPCS (Virtual PC Simulator) is a lightweight virtual PC simulator used in GNS3 lab environments. Unlike network devices (routers/switches), VPCS devices:
1. **No authentication** - Direct console access without username/password
2. **Simple command interface** - No configuration modes
3. **Simple prompt pattern** - `PC1>`, `PC2>`, etc.
### Solution: Lightweight Custom Driver
```mermaid
graph TD
A[BaseConnection<br/>Netmiko base class] --> B[VPCSTelnet<br/>Custom GNS3 driver]
B --> C[No authentication]
B --> D[Simple prompt: PC\d+]
B --> E[No config mode]
B --> F[ANSI stripping]
```
**Why Not Use Standard Telnet Driver?**
- Standard drivers attempt authentication (times out)
- No support for VPCS-specific prompt patterns (`PC\d+>`)
- No need for configuration mode handling
### VPCSTelnet Implementation
#### Location
```
gns3server/agent/gns3_copilot/utils/custom_netmiko/vpcs_telnet.py
```
#### Key Features
**1. No Authentication**
```python
def telnet_login(self, pri_prompt_terminator=r"PC\d+>", ...):
# Send returns until VPCS prompt detected
for i in range(max_loops):
self.write_channel(self.RETURN)
output = self.read_channel()
if re.search(pri_prompt_terminator, output):
return output # Success - VPCS prompt detected
```
**2. Simple Prompt Recognition**
```
PC1> ip 10.10.0.12/24 10.10.0.254
PC1> ping 10.10.0.254
```
**3. No Configuration Mode**
```python
def check_config_mode(self) -> bool:
return False # VPCS has no config mode
def config_mode(self) -> str:
return "" # No config mode to enter
def exit_config_mode(self) -> str:
return "" # No config mode to exit
```
**4. No Paging**
```python
def disable_paging(self) -> str:
return "" # VPCS doesn't use paging
```
**5. Custom `send_command`**
VPCS overrides `send_command` with non-standard defaults:
- `strip_prompt=False`, `strip_command=False`, `normalize=False` — VPCS output doesn't need standard Netmiko processing
- Uses `_strip_ansi_codes()` to clean terminal escape sequences from VPCS output
**6. ANSI Escape Code Stripping**
VPCS output often contains ANSI terminal codes. The `_strip_ansi_codes()` method strips bold, underline, reset, and color codes before returning output to callers.
### VPCS Tool Usage
The VPCS driver is used by the `execute_vpcs_commands` tool:
> File: `gns3server/agent/gns3_copilot/tools_v2/vpcs_tools_netmiko.py`
**VPCS Connection Parameters:**
VPCS devices use additional Netmiko parameters for reliable connections:
| Parameter | Value | Reason |
|-----------|-------|--------|
| `fast_cli` | `False` | VPCS is slow, disable fast CLI mode |
| `global_delay_factor` | `2.0` | Double the delay between commands |
```python
"connection_options": {
"netmiko": {
"extras": {
"device_type": "gns3_vpcs_telnet",
"fast_cli": False,
"global_delay_factor": 2.0,
}
}
}
```
### VPCS Built-in Template Configuration
**✨ Automatic Tags - No Manual Configuration Required**
VPCS nodes created from the built-in template automatically include the necessary tags:
| Tag | Value | Purpose |
|-----|-------|---------|
| `device_type` | `gns3_vpcs_telnet` | Netmiko driver selection |
**Built-in Template Definition:**
> File: `gns3server/services/templates.py`
The VPCS built-in template includes `"tags": ["device_type:gns3_vpcs_telnet"]`, which is automatically applied to all VPCS nodes created from the template.
**User Benefits:**
- ✅ **No manual tagging required** — Tags are applied automatically when creating VPCS nodes
- ✅ **Automatic driver selection** — Copilot tools automatically use the correct Netmiko driver
- ✅ **Consistent behavior** — All VPCS nodes from the built-in template work identically
- ✅ **Zero configuration** — Users don't need to understand device_type tags
**How It Works:**
1. User creates a VPCS node from the built-in "VPCS" template
2. Node automatically inherits the tag: `device_type:gns3_vpcs_telnet`
3. Copilot tools read the tag and select the appropriate VPCS Netmiko driver
4. Commands execute using the VPCS-optimized driver (no authentication, simple prompts)
### Supported VPCS Commands
| Command | Description | Example |
|---------|-------------|---------|
| `ip` | Configure/show IP address | `ip 10.10.0.12/24 10.10.0.254` |
| `ping` | Test connectivity | `ping 10.10.0.254` |
| `arp` | Display ARP table | `arp` |
| `show ip` | Show IP configuration | `show ip` |
| `version` | Show VPCS version | `version` |
| `save` | Save configuration | `save` |
| `load` | Load configuration | `load` |
### Architecture Benefits
**Unified Tool Architecture:**
- ✅ Uses Nornir for connection management (same as network device tools)
- ✅ Uses Netmiko for command execution (consistent with other tools)
- ✅ Follows same patterns as `config_tools_nornir.py` and `display_tools_nornir.py`
- ✅ Simplified codebase - no need for separate telnetlib3 implementation
**Migration from telnetlib3:**
| Aspect | Old (telnetlib3) | New (Netmiko + Nornir) |
|--------|-----------------|-------------------------|
| Library | telnetlib3 | Netmiko |
| Framework | Manual threading | Nornir |
| Code Lines | ~490 lines | ~580 lines (with better structure) |
| Consistency | Unique implementation | Same as other tools |
| Maintenance | Separate code path | Unified architecture |
---
## Custom Huawei Driver (`GNS3HuaweiTelnetCE`)
### Problem Statement
GNS3-emulated Huawei devices connect via console **without requiring authentication**. Standard Netmiko drivers attempt username/password authentication, causing connection timeouts.
**Standard Driver Behavior:**
```
Telnet Connection → Wait for username prompt → Send username → Wait for password → Send password → Access
^ Times out after 20 seconds
```
**GNS3 Huawei Device:**
```
Telnet Connection → Direct access to command line (no login prompts)
<HUAWEI>
```
### Solution: Custom Driver Architecture
```mermaid
graph TD
A[BaseConnection] --> B[CiscoBaseConnection]
B --> C[HuaweiBase<br/>VRP support, system-view,<br/>prompt patterns, paging]
C --> D[GNS3HuaweiTelnetCE<br/>Overrides telnet_login only]
D --> E[Skip authentication]
D --> F[Auto-commit before exit]
D --> G[y/n auto-confirm]
```
**Why Inherit from HuaweiBase?**
- ✅ Built-in VRP (Versatile Routing Platform) command handling
- ✅ Huawei-specific configuration mode (`system-view`)
- ✅ Huawei prompt patterns (`<...>`, `[...]`)
- ✅ Huawei paging disable (`screen-length 0 temporary`)
- ✅ Minimal code changes - only override authentication
### GNS3HuaweiTelnetCE Implementation
#### Location
```
gns3server/agent/gns3_copilot/utils/custom_netmiko/huawei_ce.py
```
**Package Structure:**
```
custom_netmiko/
├── __init__.py # Package initialization, auto-registers all drivers
├── huawei_ce.py # Huawei CloudEngine custom driver
├── README.md # Driver development guide
└── tests/ # Unit tests
├── __init__.py
└── test_huawei_ce.py # Huawei CE driver tests
```
#### Key Features
1. **Skip Authentication**
- Directly detect Huawei prompt patterns
- No username/password prompts
- Connection ready in < 1 second
2. **VRP Prompt Recognition**
```
User view: <HUAWEI>
System view: [HUAWEI]
Interface: [HUAWEI-GigabitEthernet0/0/1]
```
3. **Automatic Confirmation Handling**
- Detects and responds to `[y/n]` prompts
- Example: `return` command asks "Return to user view? [y/n]:"
- Automatically sends `y` to confirm
4. **Proper Output Collection**
- Uses Netmiko's `read_channel_timing()` for reliable output
- Waits for command completion (2s no new data = done)
- 30-second absolute timeout prevents hanging
5. **Auto-Commit Before Exit**
- Automatically sends `commit` command before exiting config mode
- Prevents "Uncommitted configurations [Y/N/C]" prompt
- Ensures configuration changes are saved
#### Limitations
**Why Not Use the Standard `huawei_telnet` Driver?**
- The standard Netmiko Huawei driver (`huawei_telnet`) has known issues in GNS3 emulation environments
- This is the primary reason the custom `gns3_huawei_telnet_ce` driver was developed
- The custom driver is designed for GNS3 devices **without authentication**
- If your GNS3 Huawei device has been configured with authentication, remove it for GNS3 testing to use the custom driver
**When to Use:**
| Scenario | Recommendation |
|----------|---------------|
| GNS3 Huawei device | Always use `gns3_huawei_telnet_ce` |
| GNS3 device has auth configured | Remove auth, then use `gns3_huawei_telnet_ce` |
#### Method Overrides
**1. `telnet_login` - Skip Authentication**
```python
def telnet_login(self, pri_prompt_terminator=r"<\S+>|>\s*$",
alt_prompt_terminator=r"\[\S+\]", ...) -> str:
# Clear buffer
self.read_channel()
# Send returns until prompt detected
for i in range(max_loops):
self.write_channel(self.RETURN)
output = self.read_channel()
# Check for Huawei prompts
if re.search(pri_prompt_terminator, output):
return output # Success!
return output # Best effort
```
**2. `send_config_set` - Configuration Commands**
```python
def send_config_set(self, config_commands, **kwargs) -> str:
# Enter config mode
output += self.config_mode(config_command="system-view")
# Send all commands
for cmd in config_commands:
self.write_channel(f"{cmd}{self.RETURN}")
time.sleep(delay_factor * 0.05)
# Collect output using Netmiko standard method
output += self.read_channel_timing(read_timeout=30, last_read=2.0)
# Auto-commit before exit (prevents [Y/N/C] prompt)
self.write_channel(f"commit{self.RETURN}")
time.sleep(0.5 * self.global_delay_factor)
output += self.read_channel()
# Exit config mode
output += self.exit_config_mode()
return output
```
**3. `exit_config_mode` - Handle Confirmation**
```python
def exit_config_mode(self, exit_config="return", pattern=r"<\S+>|>\s*$") -> str:
self.write_channel(f"return{self.RETURN}")
# Look for confirmation prompt
for _ in range(20):
new_output = self.read_channel()
if re.search(r"\[y/n\]", new_output):
self.write_channel(f"y{self.RETURN}") # Auto-confirm
if re.search(pattern, new_output):
return output # Back to user view
return output
```
### Device Type Registration
The custom driver must be registered with Netmiko's global mappings:
```python
def register_custom_device_type() -> None:
import importlib
sd = importlib.import_module("netmiko.ssh_dispatcher")
# Register in CLASS_MAPPER (for ConnectHandler)
sd.CLASS_MAPPER["gns3_huawei_telnet_ce"] = GNS3HuaweiTelnetCE
sd.CLASS_MAPPER["huawei_ce"] = GNS3HuaweiTelnetCE
# Register in CLASS_MAPPER_BASE (for base class definitions)
sd.CLASS_MAPPER_BASE["gns3_huawei_telnet_ce"] = GNS3HuaweiTelnetCE
sd.CLASS_MAPPER_BASE["huawei_ce"] = GNS3HuaweiTelnetCE
# CRITICAL: Rebuild static lists
sd.platforms = list(sd.CLASS_MAPPER.keys())
sd.platforms.sort()
sd.telnet_platforms = [x for x in sd.platforms if "telnet" in x]
```
**Important: Static List Problem**
- `ssh_dispatcher.platforms` is computed at module import time
- Modifying `CLASS_MAPPER` doesn't automatically update `platforms`
- Must manually rebuild the list after registration
**Auto-Registration**
```python
# Automatically runs on module import
try:
register_custom_device_type()
except Exception as e:
logger.warning(f"Failed to register custom device type: {e}")
```
## Custom Ruijie Driver (`RuijieTelnetEnhanced`)
### Problem Statement
Ruijie (锐捷) network devices exhibit Cisco-like command syntax but have interactive prompts during configuration that can cause standard Netmiko drivers to fail.
**Common Issue - OSPF Router-ID:**
```
Router(config-router)#router-id 10.0.0.1
Change router-id and update OSPF process! [yes/no]:
```
Standard Netmiko's `send_config_set()` waits for a prompt pattern, but the `[yes/no]:` prompt doesn't match the expected config mode pattern, causing:
1. **ReadTimeout**: Netmiko times out waiting for the prompt
2. **Command Failure**: Subsequent commands are not executed
3. **Device Lockup**: Console remains in the waiting state
### Solution: Hybrid Strategy with Interactive Prompt Handling
```mermaid
graph TD
A[BaseConnection] --> B[CiscoBaseConnection]
B --> C[RuijieOSBase<br/>Netmiko built-in]
C --> D[RuijieTelnetEnhanced<br/>Interactive prompt handling]
D --> E[Preprocessing<br/>Insert yes after known commands]
D --> F[Batch send<br/>Fast path 2-3s]
D --> G[Fallback one-by-one<br/>Slow but reliable ~10s]
```
**Why Hybrid Strategy?**
1. **Preprocessing**: Automatically insert `yes` after known interactive commands
2. **Fast Path**: Batch send for most commands (2-3 seconds for 13 commands)
3. **Fallback**: One-by-one send with real-time detection (reliable but slower)
### RuijieTelnetEnhanced Implementation
#### Location
```
gns3server/agent/gns3_copilot/utils/custom_netmiko/ruijie_telnet.py
```
#### Key Features
**1. Preprocessing - Known Interactive Commands**
```python
INTERACTIVE_PATTERNS = [
re.compile(r'^router-id\s+', re.IGNORECASE), # OSPF/EIGRP/BGP router-id
re.compile(r'^erase\s+', re.IGNORECASE), # erase startup-config
re.compile(r'^delete\s+', re.IGNORECASE), # delete files
re.compile(r'^format\s+', re.IGNORECASE), # format filesystem
re.compile(r'^reload\b', re.IGNORECASE), # reload/reboot
re.compile(r'^boot\s+system\s+', re.IGNORECASE), # change boot image
]
```
**2. Hybrid Send Strategy**
```mermaid
flowchart TD
A[Input Configuration Commands] --> B[Preprocessing]
B --> B1[Detect interactive commands]
B1 --> B2[Insert 'yes' after them]
B2 --> C[Try Batch Send - Fast]
C --> C1[Write all commands rapidly]
C1 --> C2[Read output once, last_read=2.0s]
C2 --> D{Success?}
D -->|Yes| E[Return output]
D -->|No| F[Fallback: One-by-One]
F --> F1[Send each command individually]
F1 --> F2[Detect prompts after each]
F2 --> F3[Send 'yes' when needed, last_read=0.5s]
F3 --> E
```
**3. Batch Send Performance**
- **13 commands** with 1 interactive command
- **Fast path**: ~2-3 seconds (includes device processing time)
- **Fallback**: ~10 seconds (if batch fails)
**4. Real-time Detection (Fallback)**
```python
# After each command
new_output = self.read_channel_timing(read_timeout=10, last_read=0.5)
# Check for [yes/no] prompt
if re.search(r"\[yes/no\]", new_output, re.IGNORECASE):
self.write_channel(f"yes{self.RETURN}")
output += self.read_channel_timing(read_timeout=30, last_read=0.5)
```
#### GNS3 Node Tag Configuration
**For Ruijie devices in GNS3:**
```
device_type:gns3_ruijie_telnet
```
**Example Usage:**
```python
from netmiko import ConnectHandler
from gns3server.agent.gns3_copilot.utils import custom_netmiko
device = {
"device_type": "gns3_ruijie_telnet",
"host": "127.0.0.1",
"port": 5000,
}
with ConnectHandler(**device) as conn:
# These commands include router-id (interactive)
config = [
"router ospf 1",
"router-id 10.0.0.1", # Triggers [yes/no] prompt
"network 192.168.1.0 0.0.0.255 area 0",
]
# Automatically handles the [yes/no] prompt
output = conn.send_config_set(config)
```
#### Limitations
**Interactive Command Coverage:**
- **Covered**: `router-id`, `erase`, `delete`, `format`, `reload`, `boot system`
- **Not Covered**: Unknown or vendor-specific interactive prompts
- **Fallback**: If batch fails, falls back to one-by-one with real-time detection
**When to Use:**
| Scenario | Use Driver |
|----------|------------|
| GNS3 Ruijie (known interactive commands) | `gns3_ruijie_telnet` (batch + auto-fallback) |
| GNS3 Ruijie (unknown interactive prompts) | `gns3_ruijie_telnet` (auto-fallback to one-by-one) |
## Dynamic Device Type Detection
### GNS3 Node Tags
Device type is extracted from GNS3 node tags:
```
device_type:gns3_huawei_telnet_ce → Netmiko driver selection
```
**Tag Examples:**
| Vendor | Device Type Tag | Template Source |
|--------|----------------|-----------------|
| Cisco IOS | `device_type:cisco_ios_telnet` | User appliance |
| Huawei CE | `device_type:gns3_huawei_telnet_ce` | User appliance |
| Ruijie | `device_type:gns3_ruijie_telnet` | User appliance |
| **VPCS** | `device_type:gns3_vpcs_telnet` | **Built-in ✅** |
**VPCS Built-in Template:**
- VPCS has a **built-in template** with pre-configured `device_type` tag
- The tag is **automatically applied** when creating VPCS nodes
- No manual configuration required — works out of the box
- Other devices require users to import appliances and configure tags manually
### Nornir Best Practice: Host-Level Connection Configuration
The system uses **Nornir's configuration priority** (host > group > defaults) to handle multi-vendor environments efficiently:
```python
# From get_gns3_device_port.py
hosts_data[device_name] = {
"port": console_port,
"groups": ["network_devices"], # All devices share one group
"connection_options": {
"netmiko": {
"extras": {"device_type": device_type} # Device-specific driver
}
},
}
```
**Why Host-Level `connection_options`?**
- ✅ Each device has its own `device_type` (host-level config)
- ✅ All devices share common settings via group inheritance (`hostname`, `timeout`)
- ✅ No need to dynamically create multiple groups for each device type
- ✅ Cleaner code structure — single generic group for all devices
- ✅ Follows Nornir best practice: "configuration proximity"
**Configuration Priority:**
```mermaid
graph TD
A["Host Level<br/>connection_options.device_type<br/>(gns3_huawei_telnet_ce, cisco_ios_telnet, etc.)"]
B["Group Level<br/>hostname=127.0.0.1, timeout=120<br/>username, password"]
C["Defaults Level<br/>data.location=gns3"]
A -->|overrides| B
B -->|overrides| C
```
| Aspect | Old: Dynamic Groups | New: Host-Level Config |
|--------|--------------------|-----------------------|
| Group count | One per vendor type | Single `network_devices` group |
| Device type location | Group `connection_options` | Host `connection_options` |
| Complexity | Dynamic group creation logic | No dynamic group logic |
## Architecture Evolution
### Problem: Multi-Vendor Device Support
**Initial Challenge:**
```mermaid
graph TD
A[Topology: Cisco R1 + Huawei SW1 + Juniper SRX] --> B[Need: Different Netmiko drivers per device]
B --> C[Question: How to configure Nornir for multiple device types?]
```
### Solution Evolution
| Approach | Strategy | Issue |
|----------|----------|-------|
| ❌ Approach 1 | Single group with first device's `device_type` | All devices use wrong driver |
| ❌ Approach 2 | Dynamic groups per vendor (`huawei_telnet`, `cisco_telnet`, ...) | Complex logic, code duplication, not Nornir best practice |
| ✅ Approach 3 (Current) | **Host-level `connection_options`** + single generic group | Clean, follows Nornir host > group > defaults priority |
> See `get_gns3_device_port.py:get_device_ports_from_topology()` for the current implementation.
### Configuration Priority Demonstration
```mermaid
graph TD
A["Host Level (highest)<br/>device_type: gns3_huawei_telnet_ce"]
B["Group Level (middle)<br/>hostname: 127.0.0.1, timeout: 120"]
C["Defaults Level (lowest)<br/>data.location: gns3"]
A -->|overrides| B -->|overrides| C
D[Result] --> D1["Each device uses its own device_type"]
D --> D2["All share hostname, timeout from group"]
D --> D3["All share data.location from defaults"]
```
## Usage Examples
### Direct Netmiko Usage
**Custom driver auto-registers on import. No username/password needed for GNS3 devices.**
| Device Type | `device_type` | Auth | Notes |
|-------------|---------------|------|-------|
| Huawei CE | `gns3_huawei_telnet_ce` | None | Custom driver, skip auth |
| Ruijie | `gns3_ruijie_telnet` | None | Custom driver, interactive prompts handled |
| VPCS | `gns3_vpcs_telnet` | None | Custom driver, no config mode |
| Cisco IOS | `cisco_ios_telnet` | Required | Standard Netmiko driver |
### GNS3 Copilot Tool Usage
> Tools: `tools_v2/display_tools_nornir.py`, `tools_v2/config_tools_nornir.py`, `tools_v2/vpcs_tools_netmiko.py`
Each tool reads node tags to determine `device_type`, builds host-level Nornir inventory, and dispatches commands to the appropriate Netmiko driver. Multi-vendor topologies are handled transparently — each device uses its own driver.
## Module Structure
```
gns3server/agent/gns3_copilot/
├── utils/
│ ├── custom_netmiko/ # Custom Netmiko drivers package
│ │ ├── __init__.py # Auto-registers all drivers on import
│ │ ├── huawei_ce.py # Huawei CloudEngine driver
│ │ ├── ruijie_telnet.py # Ruijie enhanced driver
│ │ ├── vpcs_telnet.py # VPCS simulator driver
│ │ ├── README.md # Driver development guide
│ │ └── tests/ # Unit tests (52 total)
│ │ ├── __init__.py
│ │ ├── test_huawei_ce.py # Huawei CE driver tests (9)
│ │ ├── test_ruijie_telnet.py # Ruijie driver tests (15)
│ │ └── test_vpcs_telnet.py # VPCS driver tests (28)
│ └── get_gns3_device_port.py # Device port extraction
│ └── get_device_ports_from_topology() # Extract ports + device_type from tags
├── tools_v2/
│ ├── display_tools_nornir.py # Multi-vendor display commands
│ ├── config_tools_nornir.py # Multi-vendor config commands
│ │ └── _expand_multiline_commands() # Expand banner commands
│ └── vpcs_tools_netmiko.py # VPCS commands using Nornir + Netmiko
│ └── VPCSCommands # VPCS tool class
```
**Key Architectural Changes (2026-03-14):**
- ❌ Removed: `vpcs_tools_telnetlib3.py` - Replaced with Netmiko implementation
- ✅ Added: `vpcs_telnet.py` - Custom VPCS driver for Netmiko
- ✅ Added: `vpcs_tools_netmiko.py` - VPCS tool using Nornir + Netmiko
- ✅ Simplified: Unified tool architecture - all tools use Nornir + Netmiko
- ✅ Updated: Module structure - all tools follow same pattern
**Key Architectural Changes (2026-03-13):**
- ❌ Removed: `_get_nornir_groups_config()` - No longer needed
- ❌ Removed: `_get_nornir_group()` - No longer needed
- ✅ Simplified: `_initialize_nornir()` - Uses single generic group
- ✅ Updated: `get_gns3_device_port.py()` - Returns host-level `connection_options`
- ✅ Added: `ruijie_telnet.py` - Custom Ruijie driver with interactive prompt handling
- ✅ Added: `_expand_multiline_commands()` - Auto-expands banner and multi-line commands
- ✅ Added: device_type tag validation with error feedback (inline in `_run`)
## Unit Testing
### Test Coverage
> Test files located at: `gns3server/agent/gns3_copilot/utils/custom_netmiko/tests/`
**Total: 52 tests across 3 test files**
| Test File | Tests | Coverage Focus |
|-----------|-------|----------------|
| `test_huawei_ce.py` | 9 | Registration, inheritance, VRP methods, mock connection |
| `test_ruijie_telnet.py` | 15 | Registration, interactive patterns, preprocessing, batch/fallback |
| `test_vpcs_telnet.py` | 28 | Registration, login, command sending, ANSI stripping, config mode |
#### Huawei CE Tests (`TestHuaweiTelnetCEDriver` + `TestHuaweiTelnetCEIntegration`)
- Device type registration in CLASS_MAPPER
- Inheritance from HuaweiBase
- VRP-specific methods (`config_mode`, `check_config_mode`, `exit_config_mode`)
- Prompt pattern constants
- Mock telnet connection
#### Ruijie Tests (`TestRuijieTelnetEnhancedDriver` + `TestRuijieTelnetEnhancedIntegration`)
- Device type registration
- Inheritance from RuijieOSBase
- Interactive pattern matching (`router-id`, `erase`, `delete`, etc.)
- Preprocessing of interactive commands
- Batch and fallback send strategies
#### VPCS Tests (`TestVPCSTelnetDriver` + 5 more test classes)
- Device type registration
- Inheritance from BaseConnection
- No-auth login with `PC\d+>` prompt
- `send_command` / `send_command_timing` overrides
- ANSI escape code stripping (8 dedicated tests)
- Config mode always returns empty/false
**Running Tests:**
```bash
source venv/bin/activate
python -m pytest gns3server/agent/gns3_copilot/utils/custom_netmiko/tests/
```
**Current Test Status:** ✅ All 52 tests passing
## Related Documentation
- [Custom Netmiko README](../../../../../gns3server/agent/gns3_copilot/utils/custom_netmiko/README.md) - Driver development guide
- [Node Control Tools](./node-control-tools.md) - Device lifecycle management
- [Command Security](./command-security.md) - Command filtering and validation
- [Chat API](./chat-api.md) - Session management and SSE
## References
- [Netmiko Documentation](https://ktbyers.github.io/netmiko/)
- [Netmiko PLATFORMS.md](https://github.com/ktbyers/netmiko/blob/master/PLATFORMS.md)
- [Nornir Documentation](https://nornir.readthedocs.io/)
---
_Implementation Date: 2026-03-12_
_Last Updated: 2026-04-20 (Documentation review: fixed VPCS template tags, updated test coverage, added Mermaid diagrams, removed code bloat)_
_Status: ✅ Implemented - Custom drivers for Huawei, Ruijie, and VPCS; multi-vendor support with Cisco IOS, Huawei, Ruijie, and VPCS tested_
_Architecture: Nornir best practice - host-level connection_options with single generic group_
_Unit Tests: ✅ 52/52 passing (9 Huawei + 15 Ruijie + 28 VPCS)_
_Changelog:_
- **2026-03-14**: Added VPCS support and unified tool architecture
- Implemented `VPCSTelnet` custom Netmiko driver for VPCS simulator
- Replaced `vpcs_tools_telnetlib3.py` with `vpcs_tools_netmiko.py`
- Unified all tools to use Nornir + Netmiko architecture
- Removed dependency on telnetlib3 for VPCS devices
- Improved code consistency and maintainability
- **2026-03-13 (Evening)**: Added Ruijie enhanced driver and interactive command handling
- Implemented `RuijieTelnetEnhanced` with hybrid batch/fallback strategy
- Added automatic `yes` insertion for known interactive commands (`router-id`, `erase`, etc.)
- Achieves ~2-3 seconds for 13 commands (vs 10+ seconds for one-by-one)
- Auto-fallback to real-time detection for unknown interactive prompts
- Registered `gns3_ruijie_telnet` device type
- **2026-03-13 (Afternoon)**: Enhanced configuration safety and command handling
- Added AAA/password configuration prohibition in system prompts
- Implemented multi-line command expansion (`_expand_multiline_commands()`)
- Added `device_type` tag validation with error feedback
- Prevents execution of commands that could lock users out of devices
- Properly handles banner and other multi-line configuration commands
- **2026-03-13 (Morning)**: Refactored to use host-level `connection_options` instead of dynamic groups
- Removed `_get_nornir_groups_config()` and `_get_nornir_group()` helper functions
- Simplified `_initialize_nornir()` to use single generic group
- Updated `get_gns3_device_port.py()` to return host-level configuration
- **2026-03-12**: Initial implementation with custom Huawei driver
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,171 @@
<!--
SPDX-License-Identifier: CC-BY-SA-4.0
See LICENSE file for licensing information.
-->
> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.
# External Skills Repository
## Overview
GNS3 Copilot loads all skills, prompts, and security configurations from an external Git repository at [github.com/yueguobin/GNS3-Skills](https://github.com/yueguobin/GNS3-Skills). This enables dynamic updates without server redeployment.
The repository provides:
- **Injection skills** (39 categories): Network fault scenarios for troubleshooting practice
- **Device skills**: Device-specific command knowledge (VPCS, etc.)
- **Feature skills**: Topology planning, network design
- **System prompts**: Agent personality and behavior definitions
- **Forbidden commands**: Security rules for command filtering
## Architecture
```mermaid
graph TD
subgraph "GNS3-Skills Repository"
YAML[injection/*.yaml<br/>device/*.yaml<br/>feature/*.yaml]
MD[prompts/*.md]
CFG[config/forbidden_commands.txt]
end
subgraph "GNS3 Server"
SM[SkillsManager]
SL[SkillsLoader]
REG[Registry<br/>SKILLS_REGISTRY<br/>INJECTION_SKILLS_REGISTRY]
PROMPT[PROMPTS_CACHE]
FC[command_filter]
end
YAML --> SL
MD --> SL
CFG --> FC
SL --> REG
SL --> PROMPT
SM --> SL
SM -->|git pull| YAML
```
## Repository Structure
```
GNS3-Skills/
├── injection/ # 39 YAML files, one per protocol/category
│ ├── ospf_issues.yaml
│ ├── bgp_issues.yaml
│ ├── vlan_issues.yaml
│ └── ...
├── device/ # Device-specific skills
│ └── vpcs.yaml
├── feature/ # Feature skills
│ └── topology_planner.yaml
├── prompts/ # System prompts (Markdown)
│ ├── teaching_assistant.md
│ ├── lab_automation_assistant.md
│ ├── troubleshooting_injection.md
│ └── title.md
└── config/ # Security configuration
└── forbidden_commands.txt
```
## Configuration
Skills repository settings are configured in `gns3_server.conf` under the `[Server]` section:
```ini
[Server]
skills_repo_url = https://github.com/yueguobin/GNS3-Skills.git
skills_repo_branch = main
skills_auto_update = true
```
| Setting | Default | Description |
|---------|---------|-------------|
| `skills_repo_url` | `https://github.com/yueguobin/GNS3-Skills.git` | Git repository URL |
| `skills_repo_branch` | `main` | Git branch to track |
| `skills_auto_update` | `true` | Automatically pull on reload |
## Initialization Flow
```mermaid
sequenceDiagram
participant Server
participant agent/__init__.py
participant SkillsManager
participant Git
Server->>agent/__init__.py: Import module
agent/__init__.py->>SkillsManager: Start background init thread
Note over SkillsManager: Thread.join(5s timeout)
SkillsManager->>Git: Check local repo exists?
alt No local repo
Git->>SkillsManager: git clone (with timeout env)
else Local repo exists
SkillsManager->>Git: Check uncommitted changes?
alt Has uncommitted changes
Git-->>SkillsManager: Warn, skip pull
else No changes
SkillsManager->>Git: git fetch (timeout 10s)
SkillsManager->>Git: Behind remote?
alt Behind
Git->>SkillsManager: git pull
else Up to date
Git-->>SkillsManager: Nothing to do
end
end
end
SkillsManager->>SkillsManager: reload_skills() - load YAML files
SkillsManager->>SkillsManager: reload_prompts() - load Markdown files
```
### Git Timeout Configuration
Git operations use per-command environment variables to prevent hanging:
```python
_GIT_TIMEOUT_ENV = {
'GIT_HTTP_TIMEOUT': '10', # Connection timeout (default: 120s)
'GIT_HTTP_LOW_SPEED_TIME': '5', # Slow speed threshold window
'GIT_HTTP_LOW_SPEED_LIMIT': '1000', # < 1 KB/s = slow → abort
}
```
These apply only to the specific `clone`/`fetch`/`pull` subprocess, not to the global environment.
## API Endpoint
### POST /copilot/reload/skills
Triggers a full reload of the skills repository. Performs one git update check, then reloads all skills, prompts, and forbidden commands from local files.
**Response:**
```json
{
"success": true,
"skills": true,
"skill_count": 39,
"prompts": true,
"prompt_count": 4,
"forbidden_commands": 6,
"version": "abc123def456..."
}
```
| Field | Description |
|-------|-------------|
| `success` | Overall success (true if skills or prompts loaded) |
| `skills` | Skills reload result |
| `skill_count` | Number of injection skills loaded |
| `prompts` | Prompts reload result |
| `prompt_count` | Number of prompts loaded |
| `forbidden_commands` | Number of forbidden command patterns |
| `version` | Git commit hash of the repository |
## Related Documentation
- [Fault Injection](fault-injection.md)
- [Command Security](command-security.md)
- [Chat API](chat-api.md)
@@ -0,0 +1,64 @@
<!--
SPDX-License-Identifier: CC-BY-SA-4.0
See LICENSE file for licensing information.
-->
> This document is a roadmap/planning document. The described features have not been implemented yet.
# Injection Fault Tracking — Roadmap
## Problem
When using the AI Copilot in troubleshooting injection mode across multiple chat sessions within the same project, there is no mechanism to prevent the same fault from being injected repeatedly. Each new session starts with a clean slate, unaware of what faults have already been used. This leads to:
- Duplicate injection scenarios across different sessions
- No visibility for instructors into which faults have been used
- No record of which devices were affected or whether faults were resolved
## Proposed Solution
Track injected faults per chat session and feed the history into the LLM's system prompt as context, so the model actively avoids repeating scenarios.
### 1. Storage
The existing `chat_sessions` table has `metadata` and `stats` JSON columns. After each fault injection, the agent records:
```json
// stored in chat_session.metadata
{
"injected_fault": {
"fault_type": "injection_ospf",
"issue_key": "ospf_hello_dead_mismatch",
"issue_name": "OSPF Hello/Dead Interval Mismatch",
"device": "R1",
"severity": "major",
"injected_at": "2026-05-11T23:00:00Z",
"resolved": false
}
}
```
### 2. Context Injection
When starting a new troubleshooting session, query all previous sessions for the project, filter by `copilot_mode = "troubleshooting_injection"`, extract `injected_fault` data, and inject a summary into the system prompt via a placeholder (similar to the existing `{{topology_info}}` mechanism):
```
Previously injected faults in this project:
1. OSPF Hello/Dead Interval Mismatch on R1 (2026-05-11) — resolved
2. BGP Route Reflector misconfiguration on R3 (2026-05-10) — resolved
Select a fault that has NOT been used before.
```
### 3. Prompt Update
The `troubleshooting_injection.md` prompt in the GNS3-Skills repository gains an `{{injection_history}}` placeholder, and the agent is instructed to choose a fault not in the history list.
## Status
- [ ] GNS3-Skills: add `{{injection_history}}` placeholder to `troubleshooting_injection.md`
- [ ] gns3-server: implement injection fault recording in agent chat session flow
- [ ] gns3-server: implement injection history query across sessions per project
- [ ] gns3-server: inject history into system prompt via `context_manager.py`
- [ ] gns3-server: add `exclude` parameter to `InjectionSkillsTool` for filtering used faults
+255
View File
@@ -0,0 +1,255 @@
<!--
SPDX-License-Identifier: CC-BY-SA-4.0
See LICENSE file for licensing information.
-->
> This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.
# Netmiko Supported Devices
**Netmiko Version:** 4.6.0
**Generated:** 2026-03-14 13:10:00
**Last Updated:** 2026-03-14 (Added VPCS support)
---
## Summary
- **Total Device Types:** 366
- **SSH Devices:** 154
- **Telnet Devices:** 55
- **Custom Devices:** 3
## Custom GNS3 Drivers
GNS3-Copilot includes custom Netmiko drivers optimized for GNS3 emulation:
| Platform | Device Type | Description |
|----------|-------------|-------------|
| Huawei CE | `gns3_huawei_telnet_ce` | Huawei CloudEngine driver with no authentication |
| Ruijie | `gns3_ruijie_telnet` | Ruijie OS enhanced driver with interactive command handling |
| VPCS | `gns3_vpcs_telnet` | VPCS simulator driver with ANSI code stripping |
## SSH Supported Devices
| Platform | Device Type | Source |
|----------|-------------|--------|
| A10 | `a10_ssh` | Netmiko |
| Accedian | `accedian_ssh` | Netmiko |
| Adtran | `adtran_os_ssh` | Netmiko |
| Adva | `adva_fsp150f2_ssh` | Netmiko |
| Adva | `adva_fsp150f3_ssh` | Netmiko |
| Alaxala | `alaxala_ax26s_ssh` | Netmiko |
| Alaxala | `alaxala_ax36s_ssh` | Netmiko |
| Alcatel | `alcatel_aos_ssh` | Netmiko |
| Alcatel | `alcatel_sros_ssh` | Netmiko |
| Allied | `allied_telesis_awplus_ssh` | Netmiko |
| Apresia | `apresia_aeos_ssh` | Netmiko |
| Arista | `arista_eos_ssh` | Netmiko |
| Arris | `arris_cer_ssh` | Netmiko |
| Aruba | `aruba_aoscx_ssh` | Netmiko |
| Aruba | `aruba_os_ssh` | Netmiko |
| Aruba | `aruba_osswitch_ssh` | Netmiko |
| Aruba | `aruba_procurve_ssh` | Netmiko |
| Asterfusion | `asterfusion_asternos_ssh` | Netmiko |
| Audiocode | `audiocode_66_ssh` | Netmiko |
| Audiocode | `audiocode_72_ssh` | Netmiko |
| Audiocode | `audiocode_shell_ssh` | Netmiko |
| Avaya | `avaya_ers_ssh` | Netmiko |
| Avaya | `avaya_vsp_ssh` | Netmiko |
| Bintec | `bintec_boss_ssh` | Netmiko |
| Broadcom | `broadcom_icos_ssh` | Netmiko |
| Brocade | `brocade_fastiron_ssh` | Netmiko |
| Brocade | `brocade_fos_ssh` | Netmiko |
| Brocade | `brocade_netiron_ssh` | Netmiko |
| Brocade | `brocade_nos_ssh` | Netmiko |
| Brocade | `brocade_vdx_ssh` | Netmiko |
| Brocade | `brocade_vyos_ssh` | Netmiko |
| Calix | `calix_b6_ssh` | Netmiko |
| Casa | `casa_cmts_ssh` | Netmiko |
| Cdot | `cdot_cros_ssh` | Netmiko |
| Centec | `centec_os_ssh` | Netmiko |
| Check Point | `checkpoint_gaia_ssh` | Netmiko |
| Ciena | `ciena_saos10_ssh` | Netmiko |
| Ciena | `ciena_saos_ssh` | Netmiko |
| Ciena | `ciena_waveserver_ssh` | Netmiko |
| Cisco | `cisco_apic_ssh` | Netmiko |
| Cisco | `cisco_asa_ssh` | Netmiko |
| Cisco | `cisco_ftd_ssh` | Netmiko |
| Cisco | `cisco_ios_ssh` | Netmiko |
| Cisco | `cisco_nxos_ssh` | Netmiko |
| Cisco | `cisco_s200_ssh` | Netmiko |
| Cisco | `cisco_s300_ssh` | Netmiko |
| Cisco | `cisco_tp_ssh` | Netmiko |
| Cisco | `cisco_viptela_ssh` | Netmiko |
| Cisco | `cisco_wlc_ssh` | Netmiko |
| Cisco | `cisco_xe_ssh` | Netmiko |
| Cisco | `cisco_xr_ssh` | Netmiko |
| Cloudgenix | `cloudgenix_ion_ssh` | Netmiko |
| Corelight | `corelight_linux_ssh` | Netmiko |
| Coriant | `coriant_ssh` | Netmiko |
| Cumulus | `cumulus_linux_ssh` | Netmiko |
| Dell | `dell_dnos9_ssh` | Netmiko |
| Dell | `dell_force10_ssh` | Netmiko |
| Dell | `dell_isilon_ssh` | Netmiko |
| Dell | `dell_os10_ssh` | Netmiko |
| Dell | `dell_os6_ssh` | Netmiko |
| Dell | `dell_os9_ssh` | Netmiko |
| Dell | `dell_powerconnect_ssh` | Netmiko |
| Dell | `dell_sonic_ssh` | Netmiko |
| Digi | `digi_transport_ssh` | Netmiko |
| Dlink | `dlink_ds_ssh` | Netmiko |
| Edgecore | `edgecore_sonic_ssh` | Netmiko |
| Ekinops | `ekinops_ek360_ssh` | Netmiko |
| Eltex | `eltex_esr_ssh` | Netmiko |
| Eltex | `eltex_ssh` | Netmiko |
| Endace | `endace_ssh` | Netmiko |
| Enterasys | `enterasys_ssh` | Netmiko |
| Ericsson | `ericsson_ipos_ssh` | Netmiko |
| Ericsson | `ericsson_mltn63_ssh` | Netmiko |
| Ericsson | `ericsson_mltn66_ssh` | Netmiko |
| Extreme | `extreme_ers_ssh` | Netmiko |
| Extreme | `extreme_exos_ssh` | Netmiko |
| Extreme | `extreme_netiron_ssh` | Netmiko |
| Extreme | `extreme_nos_ssh` | Netmiko |
| Extreme | `extreme_slx_ssh` | Netmiko |
| Extreme | `extreme_ssh` | Netmiko |
| Extreme | `extreme_tierra_ssh` | Netmiko |
| Extreme | `extreme_vdx_ssh` | Netmiko |
| Extreme | `extreme_vsp_ssh` | Netmiko |
| Extreme | `extreme_wing_ssh` | Netmiko |
| F5 | `f5_linux_ssh` | Netmiko |
| F5 | `f5_ltm_ssh` | Netmiko |
| F5 | `f5_tmsh_ssh` | Netmiko |
| Fiberstore | `fiberstore_fsos_ssh` | Netmiko |
| Fiberstore | `fiberstore_fsosv2_ssh` | Netmiko |
| Fiberstore | `fiberstore_networkos_ssh` | Netmiko |
| Flexvnf | `flexvnf_ssh` | Netmiko |
| Fortinet | `fortinet_ssh` | Netmiko |
| Garderos | `garderos_grs_ssh` | Netmiko |
| Generic | `generic_ssh` | Netmiko |
| Generic | `generic_termserver_ssh` | Netmiko |
| H3C (华三) | `h3c_comware_ssh` | Netmiko |
| HP | `hp_comware_ssh` | Netmiko |
| HP | `hp_procurve_ssh` | Netmiko |
| Hillstone | `hillstone_stoneos_ssh` | Netmiko |
| Huawei | `huawei_olt_ssh` | Netmiko |
| Huawei | `huawei_smartax_ssh` | Netmiko |
| Huawei | `huawei_smartaxmmi_ssh` | Netmiko |
| Huawei | `huawei_ssh` | Netmiko |
| Huawei | `huawei_vrp_ssh` | Netmiko |
| Huawei | `huawei_vrpv8_ssh` | Netmiko |
| Infinera | `infinera_packet_ssh` | Netmiko |
| Ipinfusion | `ipinfusion_ocnos_ssh` | Netmiko |
| Juniper | `juniper_junos_ssh` | Netmiko |
| Juniper | `juniper_screenos_ssh` | Netmiko |
| Juniper | `juniper_ssh` | Netmiko |
| Keymile | `keymile_nos_ssh` | Netmiko |
| Keymile | `keymile_ssh` | Netmiko |
| Lancom | `lancom_lcossx4_ssh` | Netmiko |
| Linux | `linux_ssh` | Netmiko |
| Maipu (迈普) | `maipu_ssh` | Netmiko |
| Mellanox | `mellanox_mlnxos_ssh` | Netmiko |
| Mellanox | `mellanox_ssh` | Netmiko |
| Mikrotik | `mikrotik_routeros_ssh` | Netmiko |
| Mikrotik | `mikrotik_switchos_ssh` | Netmiko |
| Mrv | `mrv_lx_ssh` | Netmiko |
| Mrv | `mrv_optiswitch_ssh` | Netmiko |
| Nec | `nec_ix_ssh` | Netmiko |
| Netapp | `netapp_cdot_ssh` | Netmiko |
| Netgear | `netgear_prosafe_ssh` | Netmiko |
| Netscaler | `netscaler_ssh` | Netmiko |
| Nokia | `nokia_srl_ssh` | Netmiko |
| Nokia | `nokia_sros_ssh` | Netmiko |
| Oneaccess | `oneaccess_oneos_ssh` | Netmiko |
| Ovs | `ovs_linux_ssh` | Netmiko |
| Palo Alto | `paloalto_panos_ssh` | Netmiko |
| Pluribus | `pluribus_ssh` | Netmiko |
| Quanta | `quanta_mesh_ssh` | Netmiko |
| Rad | `rad_etx_ssh` | Netmiko |
| Raisecom | `raisecom_roap_ssh` | Netmiko |
| Ruckus | `ruckus_fastiron_ssh` | Netmiko |
| Ruijie (锐捷) | `ruijie_os_ssh` | Netmiko |
| Silverpeak | `silverpeak_vxoa_ssh` | Netmiko |
| Sixwind | `sixwind_os_ssh` | Netmiko |
| Sophos | `sophos_sfos_ssh` | Netmiko |
| Supermicro | `supermicro_smis_ssh` | Netmiko |
| Telcosystems | `telcosystems_binos_ssh` | Netmiko |
| Teldat | `teldat_cit_ssh` | Netmiko |
| Tplink | `tplink_jetstream_ssh` | Netmiko |
| Ubiquiti | `ubiquiti_edge_ssh` | Netmiko |
| Ubiquiti | `ubiquiti_edgerouter_ssh` | Netmiko |
| Ubiquiti | `ubiquiti_edgeswitch_ssh` | Netmiko |
| Ubiquiti | `ubiquiti_unifiswitch_ssh` | Netmiko |
| Vertiv | `vertiv_mph_ssh` | Netmiko |
| Vyatta | `vyatta_vyos_ssh` | Netmiko |
| Vyos | `vyos_ssh` | Netmiko |
| Watchguard | `watchguard_fireware_ssh` | Netmiko |
| Yamaha | `yamaha_ssh` | Netmiko |
| ZTE (中兴) | `zte_zxros_ssh` | Netmiko |
| Zyxel | `zyxel_os_ssh` | Netmiko |
## Telnet Supported Devices
| Platform | Device Type | Source |
|----------|-------------|--------|
| Adtran | `adtran_os_telnet` | Netmiko |
| Apresia | `apresia_aeos_telnet` | Netmiko |
| Arista | `arista_eos_telnet` | Netmiko |
| Aruba | `aruba_procurve_telnet` | Netmiko |
| Audiocode | `audiocode_66_telnet` | Netmiko |
| Audiocode | `audiocode_72_telnet` | Netmiko |
| Audiocode | `audiocode_shell_telnet` | Netmiko |
| Bintec | `bintec_boss_telnet` | Netmiko |
| Brocade | `brocade_fastiron_telnet` | Netmiko |
| Brocade | `brocade_netiron_telnet` | Netmiko |
| Calix | `calix_b6_telnet` | Netmiko |
| Centec | `centec_os_telnet` | Netmiko |
| Ciena | `ciena_saos_telnet` | Netmiko |
| Cisco | `cisco_ios_telnet` | Netmiko |
| Cisco | `cisco_nxos_telnet` | Netmiko |
| Cisco | `cisco_s200_telnet` | Netmiko |
| Cisco | `cisco_s300_telnet` | Netmiko |
| Cisco | `cisco_xr_telnet` | Netmiko |
| Dell | `dell_dnos6_telnet` | Netmiko |
| Dell | `dell_powerconnect_telnet` | Netmiko |
| Dlink | `dlink_ds_telnet` | Netmiko |
| Extreme | `extreme_exos_telnet` | Netmiko |
| Extreme | `extreme_netiron_telnet` | Netmiko |
| Extreme | `extreme_telnet` | Netmiko |
| Fiberstore | `fiberstore_fsosv2_telnet` | Netmiko |
| Generic | `generic_telnet` | Netmiko |
| Generic | `generic_termserver_telnet` | Netmiko |
| Genexis | `genexis_solt33_telnet` | Netmiko |
| HP | `hp_comware_telnet` | Netmiko |
| HP | `hp_procurve_telnet` | Netmiko |
| Huawei | `huawei_olt_telnet` | Netmiko |
| Huawei | `huawei_telnet` | Netmiko |
| Huawei | `gns3_huawei_telnet_ce` | Custom ✨ (GNS3) |
| Infinera | `infinera_packet_telnet` | Netmiko |
| Ipinfusion | `ipinfusion_ocnos_telnet` | Netmiko |
| Juniper | `juniper_junos_telnet` | Netmiko |
| Maipu (迈普) | `maipu_telnet` | Netmiko |
| Nec | `nec_ix_telnet` | Netmiko |
| Nokia | `nokia_sros_telnet` | Netmiko |
| Oneaccess | `oneaccess_oneos_telnet` | Netmiko |
| Optilink | `optilink_eolt11444_telnet` | Netmiko |
| Optilink | `optilink_eolt9702_telnet` | Netmiko |
| Palo Alto | `paloalto_panos_telnet` | Netmiko |
| Rad | `rad_etx_telnet` | Netmiko |
| Raisecom | `raisecom_telnet` | Netmiko |
| Ruckus | `ruckus_fastiron_telnet` | Netmiko |
| Ruijie (锐捷) | `ruijie_os_telnet` | Netmiko |
| Ruijie (锐捷) | `gns3_ruijie_telnet` | Custom ✨ (GNS3 Enhanced) |
| Supermicro | `supermicro_smis_telnet` | Netmiko |
| VPCS | `gns3_vpcs_telnet` | Custom ✨ (GNS3 VPCS Simulator) |
| Telcosystems | `telcosystems_binos_telnet` | Netmiko |
| Teldat | `teldat_cit_telnet` | Netmiko |
| Tplink | `tplink_jetstream_telnet` | Netmiko |
| Yamaha | `yamaha_telnet` | Netmiko |
| ZTE (中兴) | `zte_zxros_telnet` | Netmiko |
---
*This document was generated automatically by the Netmiko device list script.*
@@ -0,0 +1,92 @@
<!--
SPDX-License-Identifier: CC-BY-SA-4.0
See LICENSE file for licensing information.
-->
> This document is a roadmap/planning document. The described features have not been implemented yet.
# Protocol-Oriented Packet Analysis — Roadmap
## Problem
The current `PacketCaptureTool` (`analyze_packets`) only accepts a single `packet_number` parameter and runs `tshark -V` on that one frame. This approach:
- Forces the LLM to guess packet numbers without any visibility into the capture
- Returns raw verbose output instead of structured data
- Has no protocol awareness — every protocol looks the same to the tool
- Provides no built-in anomaly detection; the LLM must infer issues from raw output each time
- Downloads the same pcap from the server on every call (no caching)
## Proposed Architecture
Move from number-based to **protocol-oriented** packet analysis. Protocol definitions (fields, display filters, anomaly checks) are stored as YAML files in the GNS3-Skills repository. The tool only needs `link_id` + `protocol` — no complex parameters for the LLM to get wrong.
### Data Flow
```
GNS3-Skills/packet_analysis/<protocol>.yaml
│
▼ loaded at startup / reload
PACKET_ANALYSIS_REGISTRY (in-memory dict)
│
▼
PacketAnalysisTool(link_id, protocol)
│
├── download pcap (with link_id caching)
├── tshark -T fields -e <predefined fields>
├── run anomaly checks from YAML
└── return structured JSON + check results
│
▼
LLM produces natural language explanation
```
### YAML Format
```yaml
name: "OSPF Packet Analysis"
protocol: "ospf"
display_filter: "ospf"
fields:
- label: "Source IP"
field: "ip.src"
description: "Source IPv4 address"
- label: "OSPF Message Type"
field: "ospf.msg"
description: "1=Hello, 2=DBD, 3=LSR, 4=LSU, 5=LSAck"
checks:
- name: hello_dead_mismatch
severity: critical
message: "Hello/Dead Interval mismatch between {src} and {dst}"
condition: "Same broadcast domain has inconsistent hello/dead intervals"
```
### Planned Protocols
| File | Protocols | Key Checks |
|------|-----------|------------|
| `arp.yaml` | ARP, NDP (ICMPv6 NS/NA) | Duplicate IP, no ARP reply, ARP flooding |
| `icmp.yaml` | ICMPv4, ICMPv6 | Unreachable classification, ping loss, PMTUD issues |
| `ospf.yaml` | OSPFv2, OSPFv3 | Hello/Dead mismatch, Area ID mismatch, Router ID conflict |
| `bgp.yaml` | BGPv4, BGP+ | Hold timer mismatch, Notification analysis, AS_PATH loop |
### Tool Interface
```json
{
"link_id": "uuid (required)",
"protocol": "arp | icmp | ospf | bgp (required)",
"summary_only": "bool (optional, default: false)"
}
```
## Status
- [ ] GNS3-Skills: create `packet_analysis/` directory and YAML definitions
- [ ] gns3-server: add `PACKET_ANALYSIS_REGISTRY` loading from skills repo
- [ ] gns3-server: implement `PacketAnalysisTool` with tshark field extraction
- [ ] gns3-server: implement protocol-specific anomaly checks
- [ ] gns3-server: add pcap caching by `link_id`
- [ ] gns3-server: register tool in teaching assistant and lab automation modes
- [ ] gns3-server: deprecate and remove old `PacketCaptureTool`
@@ -0,0 +1,67 @@
<!--
SPDX-License-Identifier: CC-BY-SA-4.0
See LICENSE file for licensing information.
-->
> This document is a roadmap/planning document. The described features have not been implemented yet.
# Server Settings REST API — Roadmap
## Problem
Currently, `gns3_server.conf` can only be modified by directly editing the file on disk. There is no REST API endpoint to read or write server configuration, which prevents the Web UI from offering a settings page for server parameters.
## Proposed API
```
GET /v3/settings → Return all current server settings
PUT /v3/settings → Update and persist server settings
```
### Implementation Plan
**1. Add `save_config()` to `Config` class** (`gns3server/config.py`)
The `Config` class currently only reads configuration (via `read_config()` / `reload()`). A `save_config()` method is needed to serialize the in-memory `ServerConfig` pydantic model back to INI format and write it to disk.
Serialization details:
- `bool` → `"True"` / `"False"` (configparser convention)
- `SecretStr` → `get_secret_value()`
- `Enum` → `.value`
- `List[str]` → semi-colon for `additional_images_paths`, comma for `allowed_interfaces`
- `None` → skip
**2. Add a settings getter/setter** to `Config` to allow programmatic updates to the in-memory settings.
**3. New route file** (`gns3server/api/routes/controller/settings.py`):
- `GET /v3/settings` — returns the full `ServerConfig` as JSON (pydantic automatically masks `SecretStr` fields as `"********"`)
- `PUT /v3/settings` — accepts `ServerConfig`, merges existing secrets when placeholder values (`"********"`) are submitted, calls `save_config()`, and triggers runtime config update callbacks
Both endpoints require `get_current_active_user` for authentication.
**4. Register the new router** in `gns3server/api/routes/controller/__init__.py` under the `/settings` prefix.
### Security
- All settings endpoints require admin authentication (`get_current_active_user`)
- `SecretStr` fields (`compute_password`, `default_admin_password`, `jwt_secret_key`) are masked in responses
- On write, unchanged secrets are preserved via placeholder detection
### Related Files
| File | Role |
|------|------|
| `gns3server/config.py` | Config singleton with `read_config()` / `reload()` |
| `gns3server/schemas/config.py` | `ServerConfig` pydantic model with all 9 sub-models |
| `gns3server/api/routes/controller/__init__.py` | Controller router mounting |
| `gns3server/controller/__init__.py` | `Controller._update_config()` for runtime credential sync |
## Status
- [ ] gns3server/config.py: add `save_config()` method
- [ ] gns3server/config.py: add settings getter/setter
- [ ] gns3server/api/routes/controller/settings.py: new route file with GET and PUT endpoints
- [ ] gns3server/api/routes/controller/__init__.py: register settings router under `/settings`
- [ ] gns3server/api/routes/controller/controller.py: add notification emission on config change
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,60 @@
<!--
SPDX-License-Identifier: CC-BY-SA-4.0
See LICENSE file for licensing information.
-->
> This document is a roadmap/planning document. The described features have not been implemented yet.
# User Preferences API — Roadmap
## Problem
Currently, user-specific preferences (UI theme, language, AI Copilot settings, workspace layout, etc.) can only be stored in browser `localStorage`. This has several drawbacks:
1. **localStorage clearing** — clearing browser data or switching devices loses all settings
2. **No cross-device sync** — users must reconfigure preferences on each device
3. **No API access** — CLI tools and third-party clients cannot read/write preferences
4. **Fragile persistence** — localStorage can be cleared by browser maintenance or incognito mode
## Proposed API
| Method | Path | Description |
|--------|------|-------------|
| `GET` | `/v3/access/users/me/preferences` | Get all preferences for current user |
| `PUT` | `/v3/access/users/me/preferences` | Merge preferences (top-level merge, not replace) |
| `DELETE` | `/v3/access/users/me/preferences` | Clear all preferences |
### Storage
A `preferences` JSON column on the existing `users` table:
- Type: `JSON`, non-nullable, default `'{}'`
- No schema enforcement — clients can store arbitrary key-value pairs
- Examples: `{"theme": "dark", "language": "zh-CN", "copilot_default_mode": "teaching_assistant"}`
### Implementation Plan
1. Add `preferences` JSON column to `User` ORM model
2. Create Alembic migration
3. Add `UserPreferencesUpdate` Pydantic schema with `extra="allow"`
4. Add `get_user_preferences()` and `update_user_preferences()` to `UsersRepository`
5. Add three endpoints to `users.py` router: `GET/PUT/DELETE /me/preferences`
### Related Files
| File | Role |
|------|------|
| `gns3server/db/models/user.py` | User ORM model |
| `gns3server/db/repositories/users.py` | UsersRepository with database operations |
| `gns3server/api/routes/controller/users.py` | User API router |
| `gns3server/schemas/` | Pydantic schemas |
| `gns3server/db/versions/` | Alembic migrations |
## Status
- [ ] Add `preferences` JSON column to User ORM model
- [ ] Create Alembic migration
- [ ] Add `UserPreferencesUpdate` schema
- [ ] Add repository methods
- [ ] Add API endpoints to users.py router
+4
View File
@@ -1,3 +1,7 @@
<!--
SPDX-License-Identifier: CC-BY-SA-4.0
See LICENSE file for licensing information.
-->
<!DOCTYPE html>
<html>
+24 -12
View File
@@ -1,14 +1,12 @@
<!--
SPDX-License-Identifier: CC-BY-SA-4.0
See LICENSE file for licensing information.
-->
<!DOCTYPE html>
<html>
<head>
<title>GNS3 controller API - ReDoc</title>
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
<link type="text/css" rel="stylesheet" href="/static/swagger-ui.css">
<link rel="shortcut icon" href="https://fastapi.tiangolo.com/img/favicon.png">
<!--
ReDoc doesn't change outer page styles
@@ -21,11 +19,25 @@
</style>
</head>
<body>
<noscript>
ReDoc requires Javascript to function. Please enable it to browse the documentation.
</noscript>
<redoc spec-url="openapi.json"></redoc>
<script src="https://cdn.jsdelivr.net/npm/redoc@2/bundles/redoc.standalone.js"> </script>
<div id="swagger-ui">
</div>
<script src="/static/swagger-ui-bundle.js"></script>
<!-- `SwaggerUIBundle` is now available on the page -->
<script>
const ui = SwaggerUIBundle({
url: 'openapi.json',
oauth2RedirectUrl: window.location.origin + '/docs/oauth2-redirect',
dom_id: '#swagger-ui',
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIBundle.SwaggerUIStandalonePreset
],
layout: "BaseLayout",
deepLinking: true,
showExtensions: true,
showCommonExtensions: true
})
</script>
</body>
</html>