YueGuobin 33b28a3b5f
fix: fetch remote-compute marker pcaps in tag replay
Marker pcaps are written by uBridge on the compute that hosts the capture
node. The tag replay only looked in the project folder on the controller.
So a source from a remote compute showed count 0 and no frames, with no
error. Its frame detail returned 404 and said nothing ever matched (#3010).

Each source now gets its pcap through the compute of its capture node.
Local files are read as before. Remote files are fetched over HTTP on
every acquire into a small cache (32 idle files, cleared at shutdown).
A file is replaced only when its bytes changed. This keeps the old mtime,
so the sharkd session for that file stays alive.

When a compute cannot be reached, its source now shows an error message
in the timeline instead of just showing nothing. The detail endpoint now
returns an error instead of 404.
2026-10-18 16:36:11 +08:00

118 lines
3.6 KiB
Python

#
# Copyright (C) 2026 GNS3 Technologies Inc.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from typing import List, Optional
from pydantic import BaseModel, ConfigDict, Field
class ReplaySource(BaseModel):
"""
A capture source (one marker pcap) of a replay tag.
"""
node_id: str
link_id: str
marker: str
data_link_type: str
count: int = Field(..., description="Total frames in the capture, unaffected by filters")
error: Optional[str] = Field(
None, description="Set when the capture file could not be fetched from the compute hosting it"
)
class ReplayFrame(BaseModel):
"""
One frame of the merged replay timeline.
"""
ts: str = Field(..., description="Timestamp string to send back unchanged")
len: int = Field(..., description="Captured length in bytes")
node_id: str
link_id: str
marker: str
frame_number: int = Field(..., description="1-based frame number in the source pcap")
src: Optional[str] = None
dst: Optional[str] = None
proto: Optional[str] = None
info: Optional[str] = None
bg: Optional[str] = Field(None, description="Wireshark coloring rule background")
fg: Optional[str] = Field(None, description="Wireshark coloring rule foreground")
class ReplayTimeline(BaseModel):
"""
Replay timeline of a tag.
"""
tag: int
start: Optional[str] = Field(None, description="Timestamp of the first matching frame")
end: Optional[str] = Field(None, description="Timestamp of the last matching frame")
frame_count: int
sources: List[ReplaySource]
frames: List[ReplayFrame]
class ReplayFrames(BaseModel):
"""
Frames of a replay time window.
"""
frames: List[ReplayFrame]
class ReplayFrameSource(BaseModel):
"""
The source pcap and frame a decoded frame comes from.
"""
node_id: str
link_id: str
marker: str
frame_number: int
class ReplayTreeNode(BaseModel):
"""
A node of the decoded protocol tree.
Keys sharkd adds in the future are passed through unchanged.
"""
model_config = ConfigDict(extra="allow")
element: Optional[str] = Field(None, description="Node type")
label: Optional[str] = Field(None, description="Display text")
name: Optional[str] = Field(None, description="Field name, e.g. 'ip.ttl'")
filter_expr: Optional[str] = Field(None, description="Display filter with the value baked in")
pos: Optional[int] = Field(None, description="Byte offset for hex highlighting")
size: Optional[int] = Field(None, description="Byte length for hex highlighting")
expert: Optional[str] = Field(None, description="Expert severity")
generated: Optional[bool] = Field(None, description="Generated by Wireshark")
children: Optional[List["ReplayTreeNode"]] = Field(None, description="Nested fields")
class ReplayFrameDetail(BaseModel):
"""
One decoded frame.
"""
ts: str
source: ReplayFrameSource
field_count: int
hex: str = Field(..., description="Raw frame bytes as hex")
tree: List[ReplayTreeNode]