Skip to content
Open
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
50 changes: 50 additions & 0 deletions docs/03-Metrics/plugins/Windows/ebpfwindows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# `ebpfwindows`

> **Status: Work-in-progress (proposal).** This plugin is a compiling skeleton that
> expresses the intended design for sourcing Windows node observability from the
> eBPF-for-Windows data plane. The live consumer is not yet implemented.

Gathers network telemetry on a Windows node from the [eBPF-for-Windows](https://github.com/microsoft/ebpf-for-windows) data plane and the WCN/Cilium-on-Windows observability surface, as an alternative to the legacy HNS/VFP hnsstats path.

## Motivation

Retina's Linux plugins rely on eBPF to collect high-fidelity flow, drop-reason and packet-forward telemetry. On Windows today, observability is limited to node-level TCP/drop counts via HNS/VFP (hnsstats). As the Windows datapath moves to the WCN/Cilium-on-Windows architecture backed by eBPF-for-Windows, this plugin provides a home for a native eBPF-backed telemetry source on Windows, keeping Retina's flow objects as the portability boundary between platforms.

## Architecture

Interfaces with the eBPF-for-Windows runtime on a Windows node.

### Intended data flow

eBPF-for-Windows maps / ring buffers -> wcnagent / Microsoft.Wcn.Observability.eBPF adapter -> ebpfwindows plugin (Start) -> Retina flow objects -> metrics / control plane

### Code Locations

- Plugin code: pkg/plugin/ebpfwindows
- Registration: pkg/plugin/include_windows.go

## Status

Implemented: the `ebpfwindows` plugin consumes flows from the WCN/eBPF-for-Windows
observability producer via a gRPC Observer stream over a node-local socket
(`ObserverSource` in `source.go`), the same mechanism the `pktmon` plugin uses.
Each incoming flow is normalized (`normalizeFlow` in `normalize.go`) so that the
WCN flows carry the verdict, traffic direction, and a drop reason extension that
Retina's advanced flow metrics read. Unit tests run an in-process Observer
server over a local socket (no WCN runtime required) and exercise the full
lifecycle, forwarding, drop, and nil-event paths.

## Open items

- Validate end-to-end against a real Windows Server 2025 + Cilium-on-Windows
node where the WCN observability producer runs (not available in dev).
- Confirm/align the default socket path with the WCN deployable.
- Fidelity/parity review against the Linux dropreason / flow path.

## Metrics

Flows are mapped onto Retina's advanced flow metric model. `Verdict` (DROPPED /
FORWARDED) drives whether a flow is counted by the drop or forward metric
(`adv_drop_count` / `adv_forward_count`), `TrafficDirection` is the metric
`direction` label, and a DROPPED flow carries a `drop_reason` extension used for
the drop metric's `reason` label.
1 change: 1 addition & 0 deletions docs/03-Metrics/plugins/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ To run Retina without any plugins, the `CAP_BPF` capability (since Linux 5.8) is
| `hnstats` (Windows) | Gathers TCP statistics and counts number of packets/bytes forwarded or dropped in HNS and VFP. | [Basic Mode](../modes/basic.md#plugin-hnsstats-windows) | Same metrics as Basic mode | [Dev Guide](./Windows/hnsstats.md) |
| `packetparser` (Linux) | Captures TCP and UDP packets traveling to and from pods and nodes. | No basic metrics | [Advanced Mode](../modes/advanced.md#plugin-packetparser-linux) | [Dev Guide](./Linux/packetparser.md) |
| `cilium` (Linux) | Collect agent and perf events from cilium via monitor1_2 socket and process flows in our hubble observer | [Metrics](./Linux/ciliumeventobserver.md#metrics) | Same metrics as Basic mode | [Dev Guide](./Linux/ciliumeventobserver.md) |
| `ebpfwindows` (Windows, WIP) | Sources telemetry from the eBPF-for-Windows / WCN data plane (alternative to HNS/VFP). Proposal skeleton. | See [Dev Guide](./Windows/ebpfwindows.md) | TBD | [Dev Guide](./Windows/ebpfwindows.md) |
2 changes: 2 additions & 0 deletions pkg/config/testwith/config-win.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ apiServer:
# Supported - debug, info, error, warn, panic, fatal.
logLevel: info
enabledPlugin: ["hnsstats"]
# Experimental: source telemetry from eBPF-for-Windows / WCN instead of HNS/VFP.
# enabledPlugin: ["ebpfwindows"]
# Interval, in seconds, to scrape/publish metrics.
metricsIntervalDuration: "10s"
# used to export telemetry to AppInsights
Expand Down
188 changes: 188 additions & 0 deletions pkg/plugin/ebpfwindows/ebpfwindows_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
// Package ebpfwindows is a Retina plugin for Windows that sources network
// telemetry from the eBPF-for-Windows data plane (the WCN / Cilium-on-Windows
// observability API) instead of the legacy HNS/VFP (hnsstats) path.
//
// Design: the plugin implements the Retina registry.Plugin contract and
// delegates event production to an EventSource. The source is an interface so
// it can be backed by a real WCN/eBPF reader in production and by a synthetic
// harness in unit tests. This keeps the plugin testable without a Windows
// cluster.
//
// Data flow:
//
// EventSource.Start(ctx) -> (<-chan *v1.Event, error)
// -> run loop: enricher.Write(ev)
// -> forward ev to the external channel (drop when full, count lost)
package ebpfwindows

import (
"context"
"fmt"
"sync"

v1 "github.com/cilium/cilium/pkg/hubble/api/v1"
"github.com/microsoft/retina/pkg/config"
"github.com/microsoft/retina/pkg/enricher"
"github.com/microsoft/retina/pkg/log"
"github.com/microsoft/retina/pkg/metrics"
"github.com/microsoft/retina/pkg/plugin/registry"
"github.com/microsoft/retina/pkg/utils"
"go.uber.org/zap"
)

const name = "ebpfwindows"

func init() {
// Self-register so pluginmanager can look it up by name via registry.Get.
registry.Add(name, New)
}

// New is the registry.PluginFunc. It returns a plugin using the default
// production source; tests inject a synthetic source via newPlugin.
func New(cfg *config.Config) registry.Plugin {
return newPlugin(cfg, newDefaultSource())
}

// newPlugin wires an explicit source (test injection point).
func newPlugin(_ *config.Config, src EventSource) registry.Plugin {
return &Plugin{
l: log.Logger().Named(name),
src: src,
}
}

// EventSource abstracts "produce flow events on this node". Implementations must
// be safe to Start once and Stop once and must close the returned channel when
// the context is cancelled so the plugin loop can unblock.
type EventSource interface {
// Start connects to the data plane and returns a channel of events.
Start(ctx context.Context) (<-chan *v1.Event, error)
// Stop releases resources held by the source.
Stop() error
}

// newDefaultSource returns the production WCN/eBPF-for-Windows flow source.
// It consumes flows from the WCN observability gRPC server over a node-local
// socket (see source.go). The socket path can be overridden via config.
func newDefaultSource() EventSource {
return newObserverSource(defaultObserverPath())
}

func defaultObserverPath() string { return defaultSocketPath }

// Plugin consumes events from a WCN/eBPF source and feeds them into Retina.
type Plugin struct {
l *log.ZapLogger
src EventSource
enricher enricher.EnricherInterface
out chan *v1.Event

mu sync.Mutex
started bool
cancel context.CancelFunc
wg sync.WaitGroup
}

// Name returns the plugin name registered in the allowed plugin list.
func (p *Plugin) Name() string { return name }

// Generate is a no-op: eBPF-for-Windows programs are provided by the OS/WCN
// data plane, not generated by Retina at build time.
func (p *Plugin) Generate(_ context.Context) error {
p.l.Debug("ebpfwindows: Generate is a no-op")
return nil
}

// Compile is a no-op for the same reason as Generate.
func (p *Plugin) Compile(_ context.Context) error {
p.l.Debug("ebpfwindows: Compile is a no-op")
return nil
}

// Init prepares plugin runtime state.
func (p *Plugin) Init() error {
p.l.Debug("ebpfwindows: Init")
return nil
}

// Start begins consumption. It is idempotent.
func (p *Plugin) Start(ctx context.Context) error {
p.mu.Lock()
defer p.mu.Unlock()
if p.started {
return nil
}

// Use the singleton enricher only if the caller has not injected one
// (e.g. via newPlugin in tests). This preserves test injection.
if p.enricher == nil {
p.enricher = enricher.Instance()
}
ctx, cancel := context.WithCancel(ctx)
p.cancel = cancel
p.started = true

p.wg.Add(1)
go func() {
defer p.wg.Done()
if err := p.run(ctx); err != nil {
p.l.Error("ebpf-windows source loop exited", zap.Error(err))
}
}()
return nil
}

// SetupChannel wires the downstream channel that receives the emitted flows.
func (p *Plugin) SetupChannel(ch chan *v1.Event) error {
p.out = ch
return nil
}

// Stop tears down consumption. It is idempotent.
func (p *Plugin) Stop() error {
p.mu.Lock()
if p.cancel != nil {
p.cancel()
}
p.started = false
p.mu.Unlock()

p.wg.Wait()
if p.src != nil {
_ = p.src.Stop()
}
return nil
}

// run drives the source loop until the context is done.
func (p *Plugin) run(ctx context.Context) error {
ch, err := p.src.Start(ctx)
if err != nil {
return fmt.Errorf("starting event source: %w", err)
}

for {
select {
case <-ctx.Done():
return nil
case ev, ok := <-ch:
if !ok {
return nil
}
if ev == nil {
continue
}

if p.enricher != nil {
p.enricher.Write(ev)
}
if p.out != nil {
select {
case p.out <- ev:
default:
metrics.LostEventsCounter.WithLabelValues(utils.ExternalChannel, name).Inc()
}
}
}
}
}
Loading
Loading