gns3-server/docs/features/marker-traffic-insight.md
YueGuobin 71fa778d50
marker: let callers pin the capture node via capture_node_id
A marker is single-sided — only the chosen capture node's uBridge installs the
mark filter — and dir=tx|rx is interpreted from that node's perspective. Until
now the observer was always auto-picked (_choose_marker_side), so dir=tx meant
"the auto-chosen endpoint is sending", which is unpredictable and makes the
direction filter hard to render meaningfully in the Web UI.

Add an optional create-only capture_node_id to MarkerCreate: when set, the
marker is pinned to that endpoint's uBridge (validated as a link endpoint and a
marker-capable type); when omitted, behavior is unchanged (auto-pick). The
chosen id is already echoed back as capture_node_id and in MARK signals, so the
UI can always render the observer regardless of who picked it.

capture_node_id is create-only (changing it would silently flip the meaning of
stored direction; recreate instead) and is not accepted on project-level
definitions — they are link-agnostic and have no endpoints, so inherited
markers keep auto-picking per link.

Plumbed through REST create_marker, the MCP link_marker tool, and base
Link.start_marker. update_marker does not forward it.
2026-08-01 22:24:47 +08:00

13 KiB

This documentation is organized by AI with reference to actual code. AI can make mistakes — please verify against the source code when in doubt.

Marker (Traffic Insight)

Overview

A marker is a passive traffic-insight tap attached to a link. It runs a libpcap BPF expression inside uBridge; on every match uBridge emits a real-time MARK signal and appends the matching packet to a per-marker pcap file. Markers exist at two layers that coexist on the same link: per-link private markers and project-level definitions that are inherited by every capable link.

Architecture

graph TB
    UI["Web UI"]

    subgraph Controller["Controller"]
        DEF["Project definitions<br/>(inheritance templates)"]
        LNK["Per-link markers"]
    end

    Compute["Compute Node"]
    UB["uBridge<br/>mark filter"]
    PCAP[("pcap file")]
    LSTN["Marker listener<br/>(UDP, per compute)"]

    UI -->|"REST + notifications ws"| Controller
    DEF -.->|"fan-out: global-{name}"| LNK
    LNK -->|"node.post /markers"| Compute
    Compute --> UB
    UB -->|"BPF match"| PCAP
    UB -->|"UDP MARK signal"| LSTN
    LSTN -->|"marker.match"| UI

Inheritance is a controller-only fan-out: a definition CRUD loops over links and reuses the existing per-link marker operations, so the compute side sees an ordinary marker and is unchanged. Each compute process runs one UDP listener serving every uBridge on that host; the node and link fields in each signal together identify the source link (see Per-link attribution).

Business Process

sequenceDiagram
    participant UI as Web UI
    participant C as Controller
    participant L as Capable Link
    participant N as Compute / uBridge

    UI->>C: POST /marker-definitions {name, bpf, ...}
    C->>C: store definition
    loop every capable link
        C->>L: start_marker("global-{name}")
        L->>N: install mark filter (BPF + pcap)
    end
    C-->>UI: 201 + link_ids

    Note over N: later: a packet matches the BPF
    N->>N: emit MARK signal + append pcap
    N-->>UI: marker.match notification (per-project ws)

Updating a definition syncs bpf / tag / color / highlight_duration to every inherited copy; deleting a definition removes every inherited copy. A newly created link inherits all existing definitions automatically.

A uBridge MARK signal carries node, filter, link, tag, and len — but no bridge name. When one node is the capture side for several links — the common case for a project-level global-{name} marker on a multi-interface router — node + filter alone are identical across those links, so they cannot tell the signals (or pcap files) apart. The link field resolves this:

  1. At install time the controller stamps each filter with its link id (mark <bpf> [tag <id>] link <link_id> [pcap <path>]).
  2. uBridge treats link as opaque and echoes it verbatim in the signal (link=<link_id>).
  3. The listener takes the signal's link= as the authoritative link_id of the marker.match event, falling back to its registry only for legacy signals that carry no link=.

This is also why the pcap path is keyed on link — <project>/markers/<node_id>_<link_id>_<filter>.pcap, not on bridge+filter: a single uBridge bridge can serve several links, and only the link id keeps their captures distinct.

IOU: one bridge, many interfaces

IOU runs a single IOL-BRIDGE per node shared by every interface, so bridge+filter are identical across that node's links. uBridge keeps a separate filter list per port (bay/unit) within the bridge, so each interface gets its own global-{name} filter, its own pcap file, and its own link=. The shared bridge name is irrelevant to attribution. Other capable node types (qemu, docker, vpcs, cloud) already use one bridge per link; link applies uniformly to all of them.

Direction

A MARK signal optionally carries dir=<tx|rx> — the matched packet's travel direction relative to the capture node (the node=<id> in the same signal, i.e. the node whose uBridge hosts the marker):

dir Ingress NIO Meaning
tx device side (source_nio on a generic bridge; the IOL instance on an IOU IOL-BRIDGE) capture node is sending
rx link side (destination_nio on a generic bridge; the NIO side on an IOU IOL-BRIDGE) capture node is receiving

A marker is single-sided: only the chosen capture node's uBridge installs the mark filter, yet both directions of the link transit that one bridge (it carries exactly two NIOs — the device side and the link side), so that single uBridge observes and classifies both directions. The marker.match event forwards dir through unchanged; the Web UI combines it with the link's two endpoints and the capture node_id to draw an arrow:

  • dir=txcapture_node → far_node
  • dir=rxfar_node → capture_node
  • dir absent (older uBridge) → undirected highlight (current behaviour)

Because the listener ignores unknown keys, dir is additive: an older server silently drops it and an older uBridge simply omits it — either way the system falls back to undirected rendering with no error.

Choosing the capture node

Since dir is relative to the capture node, which endpoint is the observer decides what tx/rx mean. By default the server auto-picks (first started marker-capable endpoint, in link-endpoint order). To pin it — e.g. so dir=tx unambiguously means "vpcs1 is sending" — pass capture_node_id on marker create:

{ "bpf": "icmp", "direction": "tx", "capture_node_id": "<vpcs1 node uuid>" }

The value must be one of the link's two endpoints and a marker-capable type (vpcs, qemu, docker, iou, dynamips, cloud); any other id is rejected with 409. Omit it to keep the auto-pick. The chosen id is echoed back as capture_node_id in the marker entry and in each MARK signal's node=<id>, so the Web UI always knows the observer regardless of who picked it.

capture_node_id is create-only: it is fixed once the marker exists (changing the observer would silently flip the meaning of stored direction, so recreate the marker instead). It is not accepted on project-level definitions — a definition is link-agnostic and has no endpoints to choose from, so inherited markers always auto-pick per link.

API Endpoints

All endpoints require a JWT bearer token (POST /v3/access/users/authenticate). The Auth column lists the required privilege.

Method Path Description Auth
GET /v3/projects/{pid}/links/{lid}/markers List markers on a link Link.Audit
POST /v3/projects/{pid}/links/{lid}/markers Attach a marker Link.Modify
PUT /v3/projects/{pid}/links/{lid}/markers/{name} Update a marker Link.Modify
DELETE /v3/projects/{pid}/links/{lid}/markers/{name} Remove a marker Link.Modify

Project-level definitions

Method Path Description Auth
GET /v3/projects/{pid}/marker-definitions List definitions + bound link_ids Project.Audit
POST /v3/projects/{pid}/marker-definitions Create definition (fans out to every link) Project.Modify
PUT /v3/projects/{pid}/marker-definitions/{name} Update definition (syncs all copies) Project.Modify
DELETE /v3/projects/{pid}/marker-definitions/{name} Delete definition (clears all copies) Project.Modify

Aggregation

Method Path Description Auth
GET /v3/projects/{pid}/markers All markers across links, flat Project.Audit

The link object returned by GET /v3/projects/{pid}/links[/{lid}] also carries a markers field (including inherited markers), so the Web UI can render a link's markers without an extra request.

Request / Response

Marker create body (MarkerCreate, shared by per-link POST and PUT):

{
  "name": "icmp",
  "bpf": "icmp",
  "tag": 1,
  "direction": "tx",
  "capture_node_id": "a37e2235-e21f-46c9-a2ab-ba0f8c5465e6",
  "color": "#ff5722",
  "highlight_duration": 800,
  "enabled": true
}

direction and capture_node_id are both optional and create-only (see Direction).

Definition create body (MarkerDefinitionCreate, shared by POST and PUT):

{
  "name": "arp",
  "bpf": "arp",
  "tag": 5,
  "color": "#ff5722",
  "highlight_duration": 1200
}

Marker entry (returned by GET/POST/PUT, and the value of each link's markers[name]):

{
  "bpf": "icmp",
  "tag": 1,
  "enabled": true,
  "color": "#ff5722",
  "highlight_duration": 800,
  "capture_node_id": "a37e2235-e21f-46c9-a2ab-ba0f8c5465e6",
  "inherited_from": null
}

Definition GET response (adds link_ids):

{
  "arp": {
    "bpf": "arp",
    "tag": 5,
    "color": null,
    "highlight_duration": 1200,
    "link_ids": ["656ed826-...", "6bd9d156-..."]
  }
}

Field Reference

Marker entry

Field Type Description
bpf string libpcap BPF expression (required)
tag int | null Correlation id echoed in MARK signals
enabled bool Whether the marker is active
color string | null Hex color render hint, e.g. #ff5722
highlight_duration int | null UI highlight duration in ms after a match; null = UI default
direction string | null tx / rx filter relative to the capture node; null = both
capture_node_id string Node whose uBridge hosts the marker — caller-set on create, else auto-picked
inherited_from string Source definition name — present on inherited markers only

Definition

Field Type Description
bpf string libpcap BPF expression (required)
tag int | null Correlation id
color string | null Hex color render hint
highlight_duration int | null UI highlight duration in ms; null = UI default
link_ids string[] Links currently carrying an inherited copy (GET only)

Notifications

Event Payload Delivered to
link.updated Link object (its markers field is the source of truth) Project notification ws
marker.match project_id, node_id, link_id, filter, tag, ts, len, dir Project notification ws only

The marker.match link_id is taken from the signal's link= field (authoritative); see Per-link attribution. The dir field is the matched packet's travel direction relative to the capture node; see Direction.

Error Responses

Status Description
401 Not authenticated
404 Link / marker / definition not found
409 Per-link edit or delete of an inherited marker; reserved (global) name or duplicate name on create
422 Validation failure (name format, highlight_duration < 1, missing bpf)

Notes

  • Marker name is immutable. It is the identifier across the controller, the uBridge filter, the pcap filename, and MARK signal routing — so rename is a delete + recreate, not a field update. PUT ignores the body name; the {name} path parameter identifies the target, and only bpf / tag / color / enabled / highlight_duration are changeable.
  • global prefix reserved. User-chosen names may not start with global; inherited markers are stored as global-{definition_name} so the two namespaces cannot collide. Omitting name on create yields an auto-generated, prefix-free name.
  • Inherited markers are read-only per-link. PUT/DELETE on an inherited marker returns 409 — edit them through the definitions API.
  • Render hints are not enforced. color and highlight_duration (milliseconds, >= 1) are stored on the link and never sent to uBridge; null lets the UI apply its own default. A partial PUT (e.g. changing only bpf) leaves them untouched.
  • Supported node types. A marker needs a uBridge bridge: vpcs, qemu, docker, iou, dynamips, cloud (one capable endpoint suffices). Types without a uBridge are silently skipped by the inheritance fan-out. IOU uses one shared IOL-BRIDGE per node but keeps filters, pcap files, and link= ids per port, so multi-interface nodes are handled (see Per-link attribution).
  • Shared capture-side node. When one node hosts markers for several links (typical for global-* definitions on a router), each filter is stamped with its link_id so signals and pcap files stay link-distinct; the controller never collapses them to a single link.
  • Persistence. Definitions and private markers persist in the topology; inherited markers are re-created from definitions on project load, so reopening a project restores the same configuration and stale inherited copies cannot survive on disk.