Merge branch '3.1' into fix/ghost-docker-node-vnc-timeout

This commit is contained in:
Guobin Yue authored and GitHub committed 2026-05-31 22:18:28 +08:00
commit 75445886fe
6 files changed
+384 -16

No files matched your search

+10 -2
View File
@@ -23,6 +23,7 @@ import html
from .controller_error import ControllerError, ControllerNotFoundError
from gns3server.agent.web_wireshark.manager import WebWiresharkManager
from gns3server.config import Config
from gns3server.utils.packet_filter_validation import validate_all_filters, FilterValidationError
import logging
@@ -47,7 +48,7 @@ FILTERS = [
"name": "Delay",
"description": "Delay packets in milliseconds. You can add jitter in milliseconds (+/-) of the delay",
"parameters": [
{"name": "Latency", "minimum": 0, "maximum": 32767, "unit": "ms", "type": "int"},
{"name": "Latency", "minimum": 1, "maximum": 32767, "unit": "ms", "type": "int"},
{"name": "Jitter (-/+)", "minimum": 0, "maximum": 32767, "unit": "ms", "type": "int"},
],
},
@@ -161,6 +162,12 @@ class Link:
if len(values) != 0 and values[0] != 0 and values[0] != "":
new_filters[filter] = values
# Validate filter parameters before applying
try:
validate_all_filters(new_filters)
except FilterValidationError as e:
raise ControllerError(f"Invalid packet filter parameters: {str(e)}")
if new_filters != self.filters:
self._filters = new_filters
if self._created:
@@ -576,7 +583,7 @@ class Link:
"suspend": self._suspended,
"show_filters_icon": getattr(self, '_show_filters_icon', True),
}
return {
result = {
"nodes": res,
"link_id": self._id,
"project_id": self._project.id,
@@ -591,3 +598,4 @@ class Link:
"wireshark": self._wireshark,
"show_filters_icon": getattr(self, '_show_filters_icon', True),
}
return result
+7 -1
View File
@@ -1205,7 +1205,13 @@ class Project:
continue
link = await self.add_link(link_id=link_data["link_id"])
if "filters" in link_data:
await link.update_filters(link_data["filters"])
try:
await link.update_filters(link_data["filters"])
except ControllerError as e:
log.warning(
"Dropping invalid filters on link %s: %s",
link_data.get("link_id"), e
)
if "link_style" in link_data:
await link.update_link_style(link_data["link_style"])
if "show_filters_icon" in link_data:
@@ -0,0 +1,191 @@
"""
Packet filter parameter validation utilities.
"""
import logging
import subprocess
from typing import Dict, List, Any, Optional
log = logging.getLogger(__name__)
class FilterValidationError(Exception):
"""Raised when packet filter parameters fail validation."""
pass
def validate_bpf_syntax(bpf_expression: str) -> Dict[str, Optional[str]]:
"""
Validate BPF filter expression syntax using tcpdump.
Uses `tcpdump -d` to compile the BPF expression into filter instructions.
This calls pcap_compile() internally (same as ubridge) but does not
capture traffic, so it returns immediately for both valid and invalid
expressions.
Args:
bpf_expression: BPF filter expression to validate
Returns:
dict with 'valid' (bool) and 'error' (str or None) keys
"""
try:
result = subprocess.run(
["tcpdump", "-d", bpf_expression],
capture_output=True,
text=True,
)
if result.returncode != 0:
# Extract meaningful error from tcpdump's stderr
# Skip "Warning: assuming Ethernet" lines, keep only error lines
error_lines = []
for line in result.stderr.split("\n"):
line = line.strip()
if line and not line.startswith("Warning:"):
# Strip "tcpdump: " prefix
for prefix in ["tcpdump: "]:
if line.startswith(prefix):
line = line[len(prefix):]
error_lines.append(line)
error_msg = " ".join(error_lines) if error_lines else "Invalid BPF expression"
log.warning("BPF syntax validation failed: %s", error_msg)
return {"valid": False, "error": error_msg}
log.info("BPF syntax validation passed")
return {"valid": True, "error": None}
except FileNotFoundError:
log.warning(
"tcpdump not found, skipping BPF syntax validation. "
"Install tcpdump to enable BPF validation."
)
return {"valid": True, "error": None}
except Exception as e:
log.error("Unexpected error during BPF validation: %s", e)
return {"valid": False, "error": f"BPF validation error: {str(e)}"}
def validate_filter_parameters(filter_type: str, values: List[Any]) -> None:
"""
Validate packet filter parameters.
Args:
filter_type: Type of packet filter
values: List of parameter values
Raises:
FilterValidationError: If parameters are invalid
"""
# Define validation rules based on ubridge implementation
VALIDATION_RULES = {
"frequency_drop": {
"params_count": 1,
"ranges": [(-1, 32767)], # min, max
"names": ["Frequency"],
"units": ["th packet"]
},
"packet_loss": {
"params_count": 1,
"ranges": [(0, 100)],
"names": ["Chance"],
"units": ["%"]
},
"delay": {
"params_count": 2, # latency, jitter
"ranges": [(1, 32767), (0, 32767)], # ubridge rejects latency <= 0
"names": ["Latency", "Jitter"],
"units": ["ms", "ms"]
},
"corrupt": {
"params_count": 1,
"ranges": [(0, 100)],
"names": ["Chance"],
"units": ["%"]
},
"bpf": {
"params_count": 1,
"is_text": True,
"names": ["Filters"]
}
}
if filter_type not in VALIDATION_RULES:
raise FilterValidationError(f"Unknown filter type: {filter_type}")
rules = VALIDATION_RULES[filter_type]
# Check parameter count
if len(values) != rules["params_count"]:
raise FilterValidationError(
f"{filter_type} expects {rules['params_count']} parameter(s), got {len(values)}"
)
# Validate each parameter
for i, value in enumerate(values):
if rules.get("is_text"):
# Text validation (BPF)
if not isinstance(value, str):
raise FilterValidationError(
f"{filter_type} parameter {rules['names'][i]} must be a string"
)
# Validate BPF syntax using tshark (same method as gns3_copilot)
# The value may be a multi-line string; each line becomes a
# separate ubridge filter. Validate each line individually.
value = value.strip()
if value:
lines = value.split("\n")
for line_num, line in enumerate(lines):
line = line.strip()
if not line:
continue
bpf_result = validate_bpf_syntax(line)
if not bpf_result["valid"]:
raise FilterValidationError(
f"{filter_type} parameter {rules['names'][i]} line {line_num + 1} "
f"has invalid syntax: {bpf_result['error']}"
)
else:
# Integer parameter validation
try:
if isinstance(value, str):
value = value.strip()
int_value = int(value)
else:
int_value = int(value)
except (ValueError, TypeError):
raise FilterValidationError(
f"{filter_type} parameter {rules['names'][i]} must be an integer, got: {value}"
)
# Range validation
min_val, max_val = rules["ranges"][i]
if int_value < min_val or int_value > max_val:
raise FilterValidationError(
f"{filter_type} parameter {rules['names'][i]} must be between "
f"{min_val} and {max_val} {rules['units'][i]}, got: {int_value}"
)
def validate_all_filters(filters: Dict[str, List[Any]]) -> None:
"""
Validate all packet filters.
Args:
filters: Dictionary mapping filter types to their values
Raises:
FilterValidationError: If any filter is invalid
"""
if not filters:
return
for filter_type, values in filters.items():
if not values or (isinstance(values, list) and len(values) == 0):
continue
validate_filter_parameters(filter_type, values)