Skip to main content

gNMI target

Every simulated device exposes a read-only gNMI gRPC server on TCP port 9339. The target serves OpenConfig interface state and counter telemetry, scoped to /interfaces/interface[name=*]/state/*. Counter values come from the same IfCounterCycler.GetDynamicAt dispatcher that drives SNMP and sFlow, so gNMI / SNMP / sFlow agree byte-for-byte at the same instant.

Enablement

The gNMI subsystem is always-on by default. Every device gets a listener; no per-device opt-in. To turn the subsystem off simulator-wide, pass -gnmi-disable.

FlagDefaultPurpose
-gnmi-port9339TCP port for the gNMI listener on each device.
-gnmi-disablefalseDisable the subsystem; no device listens on the gNMI port.

TLS

The server presents the simulator's shared self-signed certificate (the same cert used by the HTTPS REST surface). Client-certificate authentication is not required. Connect with gnmic --skip-verify for the easy path, or gnmic --tls-ca <path> if you want the cert chain validated.

The shared-cert model is a simulator convention — every simulated device presents the same certificate. The simulator does not pretend to model PKI.

Supported paths

Path coverage is scoped to the OpenConfig interfaces model, read-only:

Path leafTypeSource
/interfaces/interface[name=*]/state/namestringifDescr.<N>
/interfaces/interface[name=*]/state/ifindexuint32<N> (the ifIndex)
/interfaces/interface[name=*]/state/oper-statusenuminterface state engine (UP / DOWN / TESTING / DORMANT / NOT_PRESENT / LOWER_LAYER_DOWN)
/interfaces/interface[name=*]/state/admin-statusenuminterface state engine (UP / DOWN / TESTING)
/interfaces/interface[name=*]/state/last-changeuint64absolute Unix nanoseconds of the most recent state transition
/interfaces/interface[name=*]/state/counters/in-octetsuint64ifHCInOctets.<N>
/interfaces/interface[name=*]/state/counters/out-octetsuint64ifHCOutOctets.<N>
/interfaces/interface[name=*]/state/counters/in-unicast-pktsuint64ifHCInUcastPkts.<N>
/interfaces/interface[name=*]/state/counters/in-multicast-pktsuint64ifHCInMulticastPkts.<N>
/interfaces/interface[name=*]/state/counters/in-broadcast-pktsuint64ifHCInBroadcastPkts.<N>
/interfaces/interface[name=*]/state/counters/out-unicast-pktsuint64ifHCOutUcastPkts.<N>
/interfaces/interface[name=*]/state/counters/out-multicast-pktsuint64ifHCOutMulticastPkts.<N>
/interfaces/interface[name=*]/state/counters/out-broadcast-pktsuint64ifHCOutBroadcastPkts.<N>
/interfaces/interface[name=*]/state/counters/in-discardsuint64ifInDiscards.<N>
/interfaces/interface[name=*]/state/counters/in-errorsuint64ifInErrors.<N>
/interfaces/interface[name=*]/state/counters/out-discardsuint64ifOutDiscards.<N>
/interfaces/interface[name=*]/state/counters/out-errorsuint64ifOutErrors.<N>

Wildcards (name=*) enumerate every ifIndex known to the device. Subtree subscribes (e.g. /state/counters with no leaf) flatten to all 12 counter leaves in one tick. Specific names (name=GigabitEthernet0/0) reverse-resolve via the ifDescr table; unknown names return codes.NotFound.

Paths outside the table above — including /interfaces/interface/config/*, /interfaces/interface/subinterfaces, and anything outside /interfaces/ — return codes.NotFound.

Subscribe semantics

RPCStatus
Capabilitiesimplemented; advertises JSON_IETF, PROTO, gNMI 0.10.0, openconfig-interfaces
Getimplemented for any supported path
Subscribe (STREAM/SAMPLE)implemented
Subscribe (STREAM/ON_CHANGE)implemented for state-leaf paths; rejected for counter paths
Subscribe (ONCE)implemented; one batch + sync_response then close
Subscribe (TARGET_DEFINED)treated as SAMPLE
Subscribe (mixed ON_CHANGE + SAMPLE)rejected with InvalidArgument — split into two SubscribeRequests
Subscribe (POLL)rejected with Unimplemented
Setrejected with Unimplemented (read-only simulator)

Sample-interval clamp: any sample_interval below 1 second is silently clamped to 1 second. The same clamp applies to heartbeat_interval on ON_CHANGE subscriptions.

Backpressure: each STREAM/SAMPLE stream owns a 100-deep send buffer with oldest-drop on overflow. ON_CHANGE streams own a 16-deep listener channel (state events are rare; depth 16 absorbs multi-second collector stalls). Both drop counters are simulator-wide and exposed via GET /api/v1/gnmi/status as updates_dropped and state_events_dropped respectively.

Per-leaf ON_CHANGE acceptance. ON_CHANGE is accepted only when every subscription's resolved path touches the state-engine-backed leaves; counter leaves are rejected because counters change continuously under the analytical engine (every observation produces a different value, which would degenerate to unbounded fan-out). The rejection error names the offending leaf and recommends SAMPLE.

LeafON_CHANGESAMPLE
state/name
state/ifindex
state/oper-status
state/admin-status
state/last-change
state/counters/* (12 leaves)✗ (rejected with InvalidArgument)

ON_CHANGE event sources. Mutations come from the per-device flap scheduler (-if-flap-scenario) and the REST control plane (POST /api/v1/devices/{ip}/interfaces/{ifIndex}/{oper,admin}-status). Every transition fans out as a SubscribeResponse{update} to every matching subscriber within ~milliseconds. See interface state engine for the full picture.

Heartbeat. Per gNMI §3.5.1.5.2, heartbeat_interval lets a client request periodic re-emission of the current value even when nothing has changed. Set the field on an ON_CHANGE subscription to enable; sub-second values are clamped to 1 second. heartbeat_interval=0 (unset) means no heartbeat — emit only on actual state transitions.

Mixed-mode rejection. A single SubscribeRequest that mixes ON_CHANGE and SAMPLE subscriptions is rejected with InvalidArgument. The two paths have different emission models (event-driven vs ticker-driven); weaving them in one stream would inflate complexity for negligible value. Standard collectors (gnmic, OpenNMS Telemetryd) naturally issue two separate requests.

gnmic invocation examples

Boot the simulator with one device for these examples:

sudo ./nl6 -auto-start-ip 192.168.100.1 -auto-count 1

Then from the host:

# Capabilities — sanity check the target is reachable
gnmic -a 192.168.100.1:9339 --skip-verify capabilities

# Get all counters for one interface
gnmic -a 192.168.100.1:9339 --skip-verify get \
--path '/interfaces/interface[name=GigabitEthernet0/0]/state/counters'

# Get in-octets across every interface (wildcard)
gnmic -a 192.168.100.1:9339 --skip-verify get \
--path '/interfaces/interface[name=*]/state/counters/in-octets'

# Subscribe — stream every counter, every 5 seconds
gnmic -a 192.168.100.1:9339 --skip-verify subscribe \
--path '/interfaces/interface[name=*]/state/counters' \
--sample-interval 5s

# Subscribe ONCE — one snapshot, then exit
gnmic -a 192.168.100.1:9339 --skip-verify subscribe \
--path '/interfaces/interface[name=*]/state/counters/in-octets' \
--mode once

Quick validation with gnmic

A typical "is the gNMI surface working?" check takes about a minute. The sequence below walks capability discovery, one-shot Get, streaming Subscribe, and a counter cross-check against SNMP — useful as both a smoke test after a deployment and a regression check after touching the gNMI code path.

Install gnmic

gnmic ships from the OpenConfig project. The Go toolchain install is the most portable form:

go install github.com/openconfig/gnmic/cmd/gnmic@latest

Pre-built binaries are also published on the openconfig/gnmic GitHub releases page.

1. Boot the simulator with a small fleet

sudo ./nl6 -auto-start-ip 10.42.0.1 -auto-count 5

Five devices come up at 10.42.0.1 through 10.42.0.5, each listening on port 9339.

2. Capabilities — sanity-check the target is reachable

gnmic -a 10.42.0.1:9339 --skip-verify capabilities

Expected output:

gNMI version: 0.10.0
supported models:
- openconfig-interfaces, OpenConfig working group, 4.0.0
supported encodings:
- JSON_IETF
- PROTO

--skip-verify is required because every device presents the simulator's shared self-signed cert (see TLS above).

3. Get — confirm path resolution and counter shape

Single leaf:

gnmic -a 10.42.0.1:9339 --skip-verify get \
--path '/interfaces/interface[name=GigabitEthernet0/0]/state/counters/in-octets'

Expected output (timestamp + value will differ each call — the counter is a sine-wave function of time):

[
{
"source": "10.42.0.1:9339",
"time": "2026-05-09T10:00:30Z",
"updates": [
{
"Path": "interfaces/interface[name=GigabitEthernet0/0]/state/counters/in-octets",
"values": {
"interfaces/interface/state/counters/in-octets": "12345678"
}
}
]
}
]

Wildcard against every interface, full counter subtree:

gnmic -a 10.42.0.1:9339 --skip-verify get \
--path '/interfaces/interface[name=*]/state/counters'

Returns one notification per interface, each carrying all 12 counter leaves.

4. Subscribe — streaming telemetry

The high-value test: confirm SAMPLE-mode streaming works at the configured cadence.

gnmic -a 10.42.0.1:9339 --skip-verify subscribe \
--path '/interfaces/interface[name=*]/state/counters/in-octets' \
--sample-interval 5s

A line per interface streams every 5 seconds. Stop with Ctrl-C.

Multi-target subscribe across the whole fleet:

gnmic --skip-verify subscribe \
-a 10.42.0.1:9339,10.42.0.2:9339,10.42.0.3:9339,10.42.0.4:9339,10.42.0.5:9339 \
--path '/interfaces/interface[name=*]/state/counters/in-octets' \
--sample-interval 5s

gnmic opens parallel streams to each target; the source: field in each output line identifies the originating device.

Mixed-cadence streams in one Subscribe (post-D1 per-sub tickers):

gnmic -a 10.42.0.1:9339 --skip-verify subscribe \
--path '/interfaces/interface[name=*]/state/counters/in-octets' --sample-interval 1s \
--path '/interfaces/interface[name=*]/state/ifindex' --sample-interval 30s

The in-octets path streams every second, the ifindex path every 30 seconds — each subscription has its own ticker.

ONCE mode (snapshot, no streaming):

gnmic -a 10.42.0.1:9339 --skip-verify subscribe \
--path '/interfaces/interface[name=*]/state/counters' \
--mode once

5. Cross-check counter values against SNMP

The gNMI / SNMP / sFlow surfaces all read from the same IfCounterCycler.GetDynamicAt dispatcher, so values agree byte-for-byte at the same instant:

# Read ifHCInOctets.1 via SNMP
snmpget -v2c -c public 10.42.0.1 1.3.6.1.2.1.31.1.1.1.6.1

# Read /interfaces/interface[name=Gi0/0]/state/counters/in-octets via gNMI
gnmic -a 10.42.0.1:9339 --skip-verify get \
--path '/interfaces/interface[name=GigabitEthernet0/0]/state/counters/in-octets'

The two values should agree within the sub-second elapsed between the two commands. If they differ by more than the natural ramp rate of the sine wave at that instant, the resolver and the SNMP path have drifted — investigate gnmi_paths.go:resolveLeaf and snmp_handlers.go.

6. Confirm subsystem-level metrics

After running a Subscribe for ~30 seconds, check the simulator's accounting:

curl -s http://localhost:8080/api/v1/gnmi/status | jq
{
"subsystem_active": true,
"listeners": 5,
"active_subscriptions": 0,
"updates_sent": 30,
"updates_dropped": 0,
"tls_handshake_failures": 0
}

active_subscriptions is non-zero only while a Subscribe is live; updates_sent is monotonic across the simulator's lifetime. The exact updates_sent count depends on how many interfaces each device has and how long the stream ran.

updates_dropped > 0 means the send buffer overflowed — typically indicates a slow consumer or a sample interval too aggressive for the path coverage. tls_handshake_failures > 0 usually means clients connecting without --skip-verify (or with a wrong --tls-ca).

Troubleshooting

SymptomLikely cause
tls: failed to verify certificateAdd --skip-verify, or pass the simulator's cert via --tls-ca
connection refusedDevice IP not reachable from your shell — check routing into the opensim netns; the host route script is at GET /api/v1/devices/routes
code = InvalidArgument desc = unsupported encoding ASCIIOnly JSON_IETF and PROTO are advertised; gnmic defaults to JSON_IETF so this only triggers if you passed -e ASCII / -e BYTES
Subscribe drops after ~5 min idleHit the keepalive limit — server closes idle connections after 5 m by default (see Operational notes)
code = DeadlineExceeded desc = no SubscribeRequest received within 30sThe slowloris guard fired — your client opened a stream and didn't send the SubscribeRequest within 30 s
Get with --type config returns emptyExpected — the simulator exposes only the state subtree; no config tree exists
code = NotFound desc = origin "junos" not supportedThe simulator only serves OpenConfig; drop the origin field or set it to openconfig (or empty)
code = Unimplemented desc = POLL ... / Set ...Expected — see Subscribe semantics for the supported RPC surface

Status endpoint

curl -s http://localhost:8080/api/v1/gnmi/status | jq
{
"subsystem_active": true,
"listeners": 1,
"active_subscriptions": 0,
"updates_sent": 0,
"updates_dropped": 0
}

subsystem_active is false when -gnmi-disable is set. listeners equals the device count when active. The three counters are aggregates across the simulator's lifetime; updates_dropped increments per backpressure-discard event.

Known limitations

  • Read-only. Set returns Unimplemented. The simulator's deterministic-state guarantee is incompatible with mutation.
  • No ON_CHANGE. All counter values are continuously changing; oper-status is static. ON_CHANGE has no useful semantic in v1.
  • No POLL. Returns Unimplemented. STREAM/SAMPLE and ONCE cover the common cases.
  • Static oper-status and admin-status. Always UP. Will become dynamic when link-state simulation is added.
  • Single shared certificate. All devices present the same self-signed cert. Real fleets have per-device PKI; the simulator does not.
  • No client-cert auth. The simulator runs in lab contexts; mutual-TLS is out of scope.
  • No subinterfaces, no config tree, no /system, no LLDP, no platform/components. Only the interfaces leaves listed above.

Operational notes

  • Port surface. Each device adds one TCP listener on port 9339 (or whatever -gnmi-port says). Per-listener cost is ~10 KiB RSS + 1 fd + 1 goroutine. At 30,000 devices the total is ~320 MiB RSS.
  • Per-device source IP. Listeners bind inside the opensim netns so the source IP for accepted connections matches the device IP. Same model as SNMP / SSH / HTTPS REST.
  • Collector-side rp_filter. If your collector rejects packets from the 10.42.0.0/16 (or whatever device subnet) range, set net.ipv4.conf.*.rp_filter=0 or 2. Same caveat already documented for flow / trap / syslog.
  • Slowloris hardening. Per-device gRPC servers cap concurrent streams at 16 (lowered from 64 by the add-interface-state change — see §D9 for rationale), reap idle connections after 5 minutes, and ping clients every 30s with a 10s ack timeout. The Subscribe handler enforces a 30-second deadline on the initial SubscribeRequest — clients that open a stream and never send the subscription_list are rejected with DeadlineExceeded. The 17th concurrent stream on a single TCP connection is queued at the HTTP/2 SETTINGS_MAX_CONCURRENT_STREAMS layer until a slot frees — gRPC does not surface a status code in this case; the client's Subscribe.Recv simply blocks until a slot frees. Pre-v0.8.0 collectors that previously held >16 streams on one connection will see the 17th hang silently. To service >16 parallel streams, open a second grpc.ClientConn (multiple TCP connections each get their own quota).
  • Soft-breaking change vs v0.7.0: the stream cap drop (64 → 16) is observable to clients that previously opened more than 16 parallel streams per device-connection. The realistic ceiling is 2–3 (one primary collector + maybe a debug session); 16 is conservative.
  • ON_CHANGE on subtree paths is rejected. The /interfaces/interface[name=*]/state subtree includes 12 counter leaves; ON_CHANGE on a subtree that touches a counter leaf is rejected with InvalidArgument (the error names the offending leaf and recommends SAMPLE). Subscribers wanting ON_CHANGE coverage of the static leaves should enumerate them explicitly: e.g., one sub per state/oper-status, state/admin-status, state/last-change. The whole-/state subtree is incompatible with ON_CHANGE under the analytical counter engine because counter values change continuously.

See also

  • SNMP reference — the IF-MIB counter source, including the analytical sine-wave model.
  • Architecture — where the gNMI target sits in the simulator's component map.
  • CLI flags — the canonical flag catalog.