Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions _Log.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
- **File(s)**: `pkg/daemon/daemon_ha_userspace.go`, `pkg/daemon/userspace_sync_test.go`, `_Log.md`
- **Validation**: `gofmt -w pkg/daemon/daemon_ha_userspace.go pkg/daemon/userspace_sync_test.go`; `go test ./pkg/daemon ./pkg/dataplane/userspace ./pkg/logging`; `git diff --check`

- **Timestamp**: 2026-05-17T06:39:00Z
- **Action**: PR #1376 housekeeping — restored `go.mod` after an unintended direct/indirect dependency classification flip from automation so this branch remains scoped to mirror snapshot fixes.
- **File(s)**: `go.mod`, `_Log.md`

- **Timestamp**: 2026-05-17T06:45:00Z
- **Action**: PR #1376 re-review follow-up — renamed the mirror snapshot fail-closed wrapper to match behavior, rejected negative port-mirroring input rates before uint32 conversion to prevent wraparound, and updated mirror protocol/build tests accordingly.
- **File(s)**: `pkg/dataplane/userspace/snapshot.go`, `pkg/dataplane/userspace/protocol_test.go`, `pkg/dataplane/userspace/manager_test.go`, `_Log.md`

- **Timestamp**: 2026-05-17T05:06:00Z
- **Action**: PR #1395 cleanup — reverted an unintended `go.mod` direct/indirect dependency classification change introduced by automated tooling so the round-4 fix stays scoped to three-color policer compiler logic/tests/docs.
- **File(s)**: `go.mod`, `_Log.md`
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ require (
github.com/insomniacslk/dhcp v0.0.0-20251020182700-175e84fbb167
github.com/mdlayher/ndp v1.1.0
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2
github.com/vishvananda/netlink v1.3.1
golang.org/x/net v0.47.0
golang.org/x/sync v0.18.0
Expand All @@ -28,6 +27,7 @@ require (
github.com/mdlayher/socket v0.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
Comment on lines 27 to 32
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
Expand Down
163 changes: 163 additions & 0 deletions pkg/dataplane/userspace/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1923,6 +1923,169 @@ func TestBuildSnapshotIncludesThreeColorPolicerSchemaWhileGateClosed(t *testing.
}
}

func TestDeriveUserspaceCapabilitiesKeepsPortMirroringFailClosed(t *testing.T) {
cfg := &config.Config{}
cfg.ForwardingOptions.PortMirroring = &config.PortMirroringConfig{
Instances: map[string]*config.PortMirrorInstance{
"span1": {
Name: "span1",
InputRate: 10,
Input: []string{"ge-0/0/0.0"},
Output: "ge-0/0/1.0",
},
},
}

caps := deriveUserspaceCapabilities(cfg)
if caps.ForwardingSupported {
t.Fatal("ForwardingSupported = true, want false until mirror snapshot and runtime clone/inject are both wired")
}
found := false
for _, r := range caps.UnsupportedReasons {
if r == "port mirroring is not implemented in the userspace dataplane" {
found = true
}
}
if !found {
t.Fatalf("expected port mirroring unsupported reason, got: %+v", caps.UnsupportedReasons)
}
}

func TestBuildMirrorConfigSnapshots(t *testing.T) {
cfg := &config.Config{}
cfg.ForwardingOptions.PortMirroring = &config.PortMirroringConfig{
Instances: map[string]*config.PortMirrorInstance{
"span1": {
Name: "span1",
InputRate: 50,
Input: []string{"ge-0/0/0.0"},
Output: "ge-0/0/1.0",
},
},
}
interfaces := []InterfaceSnapshot{
{Name: "ge-0/0/0.0", LinuxName: "ge-0-0-0.0", Ifindex: 11},
{Name: "ge-0/0/1.0", LinuxName: "ge-0-0-1.0", Ifindex: 22},
}

got, err := buildMirrorConfigSnapshots(cfg, interfaces)
if err != nil {
t.Fatalf("buildMirrorConfigSnapshots: %v", err)
}
want := []MirrorConfigSnapshot{{IngressIfindex: 11, OutputIfindex: 22, Rate: 50}}
if !reflect.DeepEqual(got, want) {
t.Fatalf("mirror snapshots = %+v, want %+v", got, want)
}
}

func TestBuildSnapshotIncludesMirrorConfigsFromRealInterfaceSnapshot(t *testing.T) {
cfg := &config.Config{}
cfg.Interfaces.Interfaces = map[string]*config.InterfaceConfig{
"lo": {Name: "lo"},
}
cfg.ForwardingOptions.PortMirroring = &config.PortMirroringConfig{
Instances: map[string]*config.PortMirrorInstance{
"span-loopback": {
Name: "span-loopback",
InputRate: 7,
Input: []string{"lo"},
Output: "lo",
},
},
}

snap := buildSnapshot(cfg, config.UserspaceConfig{}, 1, 0)
var loIfindex int
for _, iface := range snap.Interfaces {
if iface.Name == "lo" {
loIfindex = iface.Ifindex
break
}
}
if loIfindex <= 0 {
t.Fatalf("buildSnapshot did not resolve loopback ifindex in interfaces: %+v", snap.Interfaces)
}
want := []MirrorConfigSnapshot{{IngressIfindex: loIfindex, OutputIfindex: loIfindex, Rate: 7}}
if !reflect.DeepEqual(snap.MirrorConfigs, want) {
t.Fatalf("MirrorConfigs = %+v, want %+v", snap.MirrorConfigs, want)
}
}

func TestBuildMirrorConfigSnapshotsRejectsDuplicateIngressIfindex(t *testing.T) {
cfg := &config.Config{}
cfg.ForwardingOptions.PortMirroring = &config.PortMirroringConfig{
Instances: map[string]*config.PortMirrorInstance{
"span1": {
Name: "span1",
Input: []string{"ge-0/0/0.0"},
Output: "ge-0/0/1.0",
},
"span2": {
Name: "span2",
Input: []string{"ge-0-0-0.0"},
Output: "ge-0/0/1.0",
},
},
}
interfaces := []InterfaceSnapshot{
{Name: "ge-0/0/0.0", LinuxName: "ge-0-0-0.0", Ifindex: 11},
{Name: "ge-0/0/1.0", LinuxName: "ge-0-0-1.0", Ifindex: 22},
}

_, err := buildMirrorConfigSnapshots(cfg, interfaces)
if err == nil || !strings.Contains(err.Error(), "duplicate port-mirroring ingress ifindex 11") {
t.Fatalf("error = %v, want duplicate ingress ifindex rejection", err)
}
}

func TestBuildMirrorConfigSnapshotsSkipsMissingOutputIfindex(t *testing.T) {
cfg := &config.Config{}
cfg.ForwardingOptions.PortMirroring = &config.PortMirroringConfig{
Instances: map[string]*config.PortMirrorInstance{
"span1": {
Name: "span1",
Input: []string{"ge-0/0/0.0"},
Output: "ge-0/0/9.0",
},
},
}
interfaces := []InterfaceSnapshot{
{Name: "ge-0/0/0.0", LinuxName: "ge-0-0-0.0", Ifindex: 11},
{Name: "ge-0/0/9.0", LinuxName: "ge-0-0-9.0", Ifindex: 0},
}

got, err := buildMirrorConfigSnapshots(cfg, interfaces)
if err != nil {
t.Fatalf("buildMirrorConfigSnapshots: %v", err)
}
if len(got) != 0 {
t.Fatalf("mirror snapshots = %+v, want missing output ifindex skipped", got)
}
}

func TestBuildMirrorConfigSnapshotsRejectsNegativeInputRate(t *testing.T) {
cfg := &config.Config{}
cfg.ForwardingOptions.PortMirroring = &config.PortMirroringConfig{
Instances: map[string]*config.PortMirrorInstance{
"span1": {
Name: "span1",
InputRate: -1,
Input: []string{"ge-0/0/0.0"},
Output: "ge-0/0/1.0",
},
},
}
interfaces := []InterfaceSnapshot{
{Name: "ge-0/0/0.0", LinuxName: "ge-0-0-0.0", Ifindex: 11},
{Name: "ge-0/0/1.0", LinuxName: "ge-0-0-1.0", Ifindex: 22},
}

_, err := buildMirrorConfigSnapshots(cfg, interfaces)
if err == nil || !strings.Contains(err.Error(), "negative input rate") {
t.Fatalf("error = %v, want negative input rate rejection", err)
}
}

func TestDeriveUserspaceCapabilitiesAllowsFirewallFilters(t *testing.T) {
cfg := &config.Config{}
cfg.Firewall.FiltersInet = map[string]*config.FirewallFilter{
Expand Down
11 changes: 11 additions & 0 deletions pkg/dataplane/userspace/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ type ConfigSnapshot struct {
ThreeColorPolicers []ThreeColorPolicerSnapshot `json:"three_color_policers,omitempty"`
ClassOfService *ClassOfServiceSnapshot `json:"class_of_service,omitempty"`
FlowExport *FlowExportSnapshot `json:"flow_export,omitempty"`
MirrorConfigs []MirrorConfigSnapshot `json:"mirror_configs,omitempty"`
Config *config.Config `json:"config,omitempty"`
Userspace config.UserspaceConfig `json:"userspace"`
DeferWorkers bool `json:"defer_workers,omitempty"`
Expand Down Expand Up @@ -357,6 +358,16 @@ type FlowExportSnapshot struct {
InactiveTimeout int `json:"inactive_timeout,omitempty"` // seconds, 0=default 15
}

// MirrorConfigSnapshot captures one ingress SPAN mapping for userspace-dp.
// It is snapshot/admission state only until the userspace runtime clone path is
// wired. Runtime delivery must use full-L2 cross-binding inject; the L3 TUN
// slow-path is not a valid mirror sink because it strips Ethernet framing.
type MirrorConfigSnapshot struct {
IngressIfindex int `json:"ingress_ifindex"`
OutputIfindex int `json:"output_ifindex"`
Rate uint32 `json:"rate"`
}

type PolicyApplicationSnapshot struct {
Name string `json:"name"`
Protocol string `json:"protocol,omitempty"`
Expand Down
40 changes: 40 additions & 0 deletions pkg/dataplane/userspace/protocol_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,46 @@ func TestBindingStatusTXSharedRecycleUnknownSlotDropsRoundTrip(t *testing.T) {
}
}

func TestConfigSnapshotMirrorConfigsRoundTrip(t *testing.T) {
in := ConfigSnapshot{
Version: ProtocolVersion,
MirrorConfigs: []MirrorConfigSnapshot{
{IngressIfindex: 11, OutputIfindex: 22, Rate: 100},
{IngressIfindex: 12, OutputIfindex: 22},
},
}
raw, err := json.Marshal(&in)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var obj map[string]json.RawMessage
if err := json.Unmarshal(raw, &obj); err != nil {
t.Fatalf("unmarshal obj: %v", err)
}
if _, ok := obj["mirror_configs"]; !ok {
t.Fatalf("wire key missing from ConfigSnapshot JSON: %s", string(raw))
}
var mirrorObjects []map[string]json.RawMessage
if err := json.Unmarshal(obj["mirror_configs"], &mirrorObjects); err != nil {
t.Fatalf("unmarshal mirror_configs: %v", err)
}
for i, mirror := range mirrorObjects {
for _, key := range []string{"ingress_ifindex", "output_ifindex", "rate"} {
if _, ok := mirror[key]; !ok {
t.Fatalf("mirror_configs[%d] missing wire key %q: %s", i, key, string(raw))
}
}
}

var back ConfigSnapshot
if err := json.Unmarshal(raw, &back); err != nil {
t.Fatalf("unmarshal ConfigSnapshot: %v", err)
}
if !reflect.DeepEqual(back.MirrorConfigs, in.MirrorConfigs) {
t.Fatalf("mirror config round-trip mismatch: got %+v, want %+v", back.MirrorConfigs, in.MirrorConfigs)
}
}

func TestBindingCountersSnapshotTXSharedRecycleUnknownSlotDropsRoundTrip(t *testing.T) {
in := BindingCountersSnapshot{
WorkerID: 3,
Expand Down
81 changes: 81 additions & 0 deletions pkg/dataplane/userspace/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ func buildSnapshotWithSchedulerState(cfg *config.Config, ucfg config.UserspaceCo
ThreeColorPolicers: buildThreeColorPolicerSnapshots(cfg),
ClassOfService: buildClassOfServiceSnapshot(cfg),
FlowExport: buildFlowExportSnapshot(cfg),
MirrorConfigs: buildMirrorConfigSnapshotsFailClosed(cfg, interfaces),
Config: cfg,
Summary: SnapshotSummary{
HostName: cfg.System.HostName,
Expand All @@ -79,6 +80,86 @@ func buildSnapshotWithSchedulerState(cfg *config.Config, ucfg config.UserspaceCo
}
}

func buildMirrorConfigSnapshotsFailClosed(cfg *config.Config, interfaces []InterfaceSnapshot) []MirrorConfigSnapshot {
mirrors, err := buildMirrorConfigSnapshots(cfg, interfaces)
if err != nil {
// The mirror table contract is one output per ingress ifindex. Keep
// snapshot publication fail-closed by omitting an ambiguous mirror table
// if this helper is called on an invalid config.
slog.Warn("userspace snapshot: invalid port-mirroring config skipped", "err", err)
return nil
}
return mirrors
}

func buildMirrorConfigSnapshots(cfg *config.Config, interfaces []InterfaceSnapshot) ([]MirrorConfigSnapshot, error) {
if cfg == nil || cfg.ForwardingOptions.PortMirroring == nil || len(cfg.ForwardingOptions.PortMirroring.Instances) == 0 {
return nil, nil
}
ifindexByName := make(map[string]int, len(interfaces))
for _, iface := range interfaces {
if iface.Ifindex > 0 {
ifindexByName[iface.Name] = iface.Ifindex
if iface.LinuxName != "" {
ifindexByName[iface.LinuxName] = iface.Ifindex
}
}
}

instanceNames := make([]string, 0, len(cfg.ForwardingOptions.PortMirroring.Instances))
for name := range cfg.ForwardingOptions.PortMirroring.Instances {
instanceNames = append(instanceNames, name)
}
sort.Strings(instanceNames)

seenIngress := make(map[int]string)
out := make([]MirrorConfigSnapshot, 0)
for _, name := range instanceNames {
inst := cfg.ForwardingOptions.PortMirroring.Instances[name]
if inst == nil {
continue
}
if inst.Output == "" {
slog.Warn("port-mirroring instance has no output interface", "name", name)
continue
}
outputIfindex := ifindexByName[inst.Output]
if outputIfindex <= 0 {
outputIfindex = ifindexByName[config.LinuxIfName(inst.Output)]
}
if outputIfindex <= 0 {
slog.Warn("port-mirroring output interface not found",
"name", name, "interface", inst.Output)
continue
}

for _, input := range inst.Input {
ingressIfindex := ifindexByName[input]
if ingressIfindex <= 0 {
ingressIfindex = ifindexByName[config.LinuxIfName(input)]
}
if ingressIfindex <= 0 {
slog.Warn("port-mirroring input interface not found",
"name", name, "interface", input)
continue
}
if previous, ok := seenIngress[ingressIfindex]; ok {
return nil, fmt.Errorf("duplicate port-mirroring ingress ifindex %d in instances %q and %q", ingressIfindex, previous, name)
}
if inst.InputRate < 0 {
return nil, fmt.Errorf("port-mirroring instance %q has negative input rate %d", name, inst.InputRate)
}
Comment on lines +136 to +151
seenIngress[ingressIfindex] = name
out = append(out, MirrorConfigSnapshot{
IngressIfindex: ingressIfindex,
OutputIfindex: outputIfindex,
Rate: uint32(inst.InputRate),
})
Comment on lines +153 to +157
}
Comment on lines +153 to +158
}
return out, nil
}

const (
// Keep logical-only synthetic ifindexes in a high private range so
// they do not collide with kernel-assigned ifindexes in practical
Expand Down
Loading
Loading