A collection of reusable Go packages for network telemetry, peer monitoring, generic data structures, and Cisco IOS XR / NX-OS tooling.
Module: github.com/sbezverk/tools
Go version: 1.25+
- Building
- Root package —
tools - Package
peer - Package
sort - Package
store - Package
telemetry_feeder - Tool
xr_getproto
Both standard Go tooling and Bazel are supported.
go build ./...
go test ./...bazel build //...
bazel test //...Cross-platform targets:
bazel build --config=linux_amd64 //...
bazel build --config=darwin_amd64 //...
bazel build --config=darwin_arm64 //...import "github.com/sbezverk/tools"Formats a byte slice as a human-readable hex string suitable for logging.
s := tools.MessageHex([]byte{0xDE, 0xAD, 0xBE, 0xEF})
// s == "[ 0xDE, 0xAD, 0xBE, 0xEF ]"Returns the two-character uppercase hex representation of a single byte.
s := tools.ConvertToHex(0x0F)
// s == "0F"Validates that addr is a well-formed host:port string — splits with
net.SplitHostPort, performs a DNS lookup on the host, and checks that the
port is in the range 1–65535.
if err := tools.HostAddrValidator("router1.example.com:57400"); err != nil {
log.Fatalf("invalid address: %v", err)
}Parses a full URL and then calls HostAddrValidator on the host portion.
if err := tools.URLAddrValidation("grpc://10.0.0.1:57400"); err != nil {
log.Fatalf("invalid URL: %v", err)
}import "github.com/sbezverk/tools"Returns a channel that is closed when the process receives its first SIGINT
(Ctrl-C). A second SIGINT calls os.Exit(1) immediately. Only one call per
process is allowed; a second call panics.
stopCh := tools.SetupSignalHandler()
// block until Ctrl-C
<-stopCh
fmt.Println("shutting down")import "github.com/sbezverk/tools/peer"Implements a UDP keepalive peer-monitoring system. Local and remote endpoints
exchange small binary heartbeat messages. A Monitor aggregates multiple peers
and emits events whenever any peer transitions between UP and DOWN.
A Peer manages a single local ↔ remote UDP keepalive session.
type Peer interface {
Start()
Stop()
GetSessionStateChangeCh() chan *SessionState
GetRemoteAddr() string
Alive() bool
}Creating a peer:
stateCh := make(chan *peer.SessionState, 10)
p, err := peer.NewPeer(
"192.168.1.1", // local address (without port)
9000, // UDP port used on both ends
"192.168.1.2", // remote address
stateCh,
)
if err != nil {
log.Fatal(err)
}
p.Start()
defer p.Stop()
for s := range stateCh {
fmt.Printf("peer %s is now %s\n", s.RemotePeer, s.PeerState)
}Keepalive message (KeepaliveMessageLen = 9 bytes):
| Field | Type | Description |
|---|---|---|
Priority |
uint8 | Peer priority |
Interval |
uint32 | Keepalive interval in milliseconds (default 1000) |
DeadInterval |
uint32 | Dead interval in milliseconds (default 3000) |
The peer is declared DOWN after DeadInterval / Interval consecutive missed
keepalive intervals.
Monitor wraps multiple peers behind a single event channel. All peer state
changes are multiplexed onto one chan *MonitorMessage.
type Monitor interface {
GetMonitorCh() chan *MonitorMessage
Stop()
}mon, err := peer.SetupMonitorForRemotePeer(
"192.168.1.1", // local address
9000, // port
"192.168.1.2", // one or more remote peers
"192.168.1.3",
)
if err != nil {
log.Fatal(err)
}
defer mon.Stop()
for msg := range mon.GetMonitorCh() {
fmt.Printf("remote %s → %s\n", msg.RemotePeer, msg.PeerState)
}Constraints:
- At least one remote peer must be provided.
- Port must be < 65535.
- All addresses must belong to the same IP family (no mixing of IPv4 and IPv6).
PeerState values:
| Constant | Value | Meaning |
|---|---|---|
PeerUp |
1 | Peer is reachable |
PeerDown |
2 | Peer is unreachable |
import "github.com/sbezverk/tools/sort"Generic merge sort for slices of any type that satisfies
golang.org/x/exp/constraints.Ordered (integers, floats, strings, …).
Sorts s in-place using merge sort and returns the same slice. Safe to call
on nil or empty slices.
import "github.com/sbezverk/tools/sort"
nums := []int{5, 2, 8, 1, 9, 3}
sorted := sort.SortMergeComparableSlice(nums)
// sorted == [1, 2, 3, 5, 8, 9]
words := []string{"banana", "apple", "cherry"}
sort.SortMergeComparableSlice(words)
// words == ["apple", "banana", "cherry"]import "github.com/sbezverk/tools/store"An actor-model, goroutine-safe, in-memory key-value store. All mutations and reads are serialised through a single manager goroutine — no mutexes required in calling code.
Any value that implements Storable can be stored:
type Storable interface {
Key() string
}The store is accessed through the Manager interface:
type Manager interface {
Add(Storable) error // ErrAlreadyExist if key is taken
Remove(string) error // ErrNotFound if key is absent
List() []Storable // snapshot of all values
Get(string) Storable // nil if not found
Stop() // shut down the manager goroutine
}type Route struct {
Prefix string
NextHop string
}
func (r *Route) Key() string { return r.Prefix }
// ---
s := store.NewStore()
defer s.Stop()
r := &Route{Prefix: "10.0.0.0/8", NextHop: "192.168.0.1"}
if err := s.Add(r); err != nil {
log.Printf("add failed: %v", err) // store.ErrAlreadyExist
}
entry := s.Get("10.0.0.0/8")
if entry != nil {
fmt.Println(entry.(*Route).NextHop)
}
all := s.List()
fmt.Printf("%d routes stored\n", len(all))
if err := s.Remove("10.0.0.0/8"); err != nil {
log.Printf("remove failed: %v", err) // store.ErrNotFound
}import "github.com/sbezverk/tools/telemetry_feeder"Defines the common interface consumed by all three transport implementations (gRPC, UDP, offline). Callers depend only on this interface and are transport-agnostic.
const (
TransportGRPC Transport = "grpc"
TransportUDP Transport = "udp"
TransportTCP Transport = "tcp"
EncodingGPB PayloadEncoding = "gpb"
EncodingJSON PayloadEncoding = "json"
FramingNone Framing = "none"
FramingCiscoXRST Framing = "cisco-xr-st"
FramingCiscoNXOSUDP Framing = "cisco-nxos-udp"
)
// A single telemetry event delivered to the caller.
type Feed struct {
ProducerAddr net.Addr // source address of the sender
TelemetryMsg []byte // raw telemetry payload (encoding depends on Encoding)
Err error // non-nil on transport or decode error
Transport Transport // grpc or udp
Encoding PayloadEncoding // gpb or json
Framing Framing // none, cisco-xr-st, or cisco-nxos-udp
}
type Feeder interface {
GetStatsJson() ([]byte, error)
GetFeed() chan *Feed
Stop()
}GetStatsJson returns a transport-normalized stats snapshot. UDP and gRPC
use the same JSON counter names, with transport set to udp or grpc.
Sentinel errors:
| Error | Meaning |
|---|---|
ErrUnmarshalTelemetryMsg |
Failed to deserialise the telemetry message |
ErrReceiveTelemetryMsg |
Transport receive error |
import "github.com/sbezverk/tools/telemetry_feeder/grpc_feeder"Starts a gRPC server that accepts Cisco MDT dial-out connections
(gRPCMdtDialout service). Each inbound MdtDialoutArgs.data payload is
emitted as a *Feed.
f, err := grpc_feeder.New("0.0.0.0:57500")
if err != nil {
log.Fatal(err)
}
defer f.Stop()
for feed := range f.GetFeed() {
if feed.Err != nil {
log.Printf("grpc error: %v", feed.Err)
continue
}
// decode feed.TelemetryMsg as *telemetry.Telemetry
msg := &telemetry.Telemetry{}
if err := proto.Unmarshal(feed.TelemetryMsg, msg); err != nil {
log.Printf("unmarshal: %v", err)
continue
}
fmt.Printf("path=%s ts=%d\n", msg.EncodingPath, msg.MsgTimestamp)
}Server configuration defaults:
| Setting | Value |
|---|---|
| Max receive msg size | 4 MB |
| Keepalive time | 30 s |
| Keepalive timeout | 10 s |
The router side (IOS XR / NX-OS) must be configured with destination-group
pointing at the listener address and encoding self-describing-gpb or
encoding gpb.
import "github.com/sbezverk/tools/telemetry_feeder/udp_feeder"Listens on a UDP socket and emits each received datagram as a *Feed. The
feed channel is internally buffered at 100 messages.
f, err := udp_feeder.New("0.0.0.0:57500")
if err != nil {
log.Fatal(err)
}
defer f.Stop()
for feed := range f.GetFeed() {
if feed.Err != nil {
log.Printf("udp error: %v", feed.Err)
continue
}
fmt.Printf("received %d bytes from %s\n",
len(feed.TelemetryMsg), feed.ProducerAddr)
}Maximum datagram size: 4 MB.
import "github.com/sbezverk/tools/telemetry_feeder/offline_feeder"Replays telemetry from a binary capture file at a rate of one message per second. The file format is a simple length-prefixed stream:
[ 4-byte big-endian uint32 length ][ <length> bytes payload ]
[ 4-byte big-endian uint32 length ][ <length> bytes payload ]
...
f, err := offline_feeder.New("/path/to/capture.bin")
if err != nil {
log.Fatal(err)
}
defer f.Stop()
for feed := range f.GetFeed() {
if feed.Err != nil {
break // EOF or read error
}
msg := &telemetry.Telemetry{}
proto.Unmarshal(feed.TelemetryMsg, msg)
// process msg ...
}The channel is closed when EOF is reached or Stop() is called.
All protobuf-generated Go packages live under telemetry_feeder/proto/.
| Import path | Description |
|---|---|
.../proto/telemetry |
Cisco MDT envelope (Telemetry, TelemetryField, TelemetryGPBTable) |
.../proto/mdtdialout |
gRPCMdtDialout service — used internally by grpc_feeder |
.../proto/adjacency |
NX-OS adjacency add/delete/update events |
.../proto/mac_all |
NX-OS MAC table events |
.../proto/urib |
NX-OS Unicast RIB (L3 route / next-hop) events |
.../proto/ios-xr-rib/... |
IOS XR RIB YANG-generated schemas (44 message types) |
Decoding a self-describing GPB-KV telemetry message:
import (
"github.com/sbezverk/tools/telemetry_feeder/proto/telemetry"
"google.golang.org/protobuf/proto"
)
msg := &telemetry.Telemetry{}
if err := proto.Unmarshal(feed.TelemetryMsg, msg); err != nil {
return err
}
fmt.Printf("node: %s\n", msg.GetNodeIdStr())
fmt.Printf("subscription: %s\n", msg.GetSubscriptionIdStr())
fmt.Printf("path: %s\n", msg.EncodingPath)
fmt.Printf("collection: %d\n", msg.CollectionId)
// GPB-KV rows
for _, field := range msg.DataGpbkv {
fmt.Printf(" field: %s\n", field.Name)
for _, child := range field.Fields {
fmt.Printf(" %s = %v\n", child.Name, child.ValueByType)
}
}Decoding NX-OS adjacency events:
import (
adj "github.com/sbezverk/tools/telemetry_feeder/proto/adjacency"
)
a := &adj.NxAdjacencyProto{}
if err := proto.Unmarshal(row.Content, a); err != nil {
return err
}
fmt.Printf("adj %s/%s event=%s\n",
a.IpAddress, a.MacAddress, a.EventType)Decoding NX-OS URIB (L3 route) events:
import (
urib "github.com/sbezverk/tools/telemetry_feeder/proto/urib"
)
route := &urib.NxL3RouteProto{}
if err := proto.Unmarshal(row.Content, route); err != nil {
return err
}
fmt.Printf("route %s/%d vrf=%s event=%s\n",
route.Address, route.MaskLen, route.VrfName, route.EventType)
for _, nh := range route.NextHop {
fmt.Printf(" nexthop %s via %s\n", nh.Address, nh.OutInterface)
}A command-line utility that connects to a live Cisco IOS XR router and
retrieves .proto schema files for arbitrary YANG paths via the
GetProtoFile gRPC RPC.
# Go
go build -o xr_getproto ./xr_getproto
# Bazel
bazel build //xr_getproto:xr_getprotoxr_getproto [flags]
Flags:
-server_addr string Router address (host:port) [required]
-yang_path string YANG path to fetch proto for [required]
-out string Output file path (default: stdout)
-req_id int Request ID (default: 1)
-tls Enable TLS
-ca_file string CA certificate file (for TLS)
-server_name string Server name override (for TLS)
-username string gRPC metadata username
-password string gRPC metadata password
-timeout int Request timeout in seconds (default: 30)
Fetch the BGP RIB proto and print to stdout:
xr_getproto \
-server_addr 10.0.0.1:57400 \
-yang_path "Cisco-IOS-XR-ip-rib-oper:rib/vrfs/vrf/afs/af/safs/saf/ip-rib-route-table-names/ip-rib-route-table-name/routes/route" \
-username admin \
-password secretSave to a file with TLS:
xr_getproto \
-server_addr router.example.com:57400 \
-yang_path "Cisco-IOS-XR-ip-rib-oper:rib" \
-tls \
-ca_file /etc/tls/ca.pem \
-server_name router.example.com \
-username admin \
-password secret \
-out xr_rib.protoNote: If an HTTP proxy is configured in the environment (
HTTP_PROXY,HTTPS_PROXY) it may intercept the gRPC connection. Either unset those variables or add the router toNO_PROXYbefore running the tool.
Credentials are sent as per-RPC gRPC metadata (not TLS client certificates).
This means they work over plaintext gRPC connections as well as TLS-encrypted
ones. Use -tls when operating over untrusted networks.
See LICENSE.