netevd runs your scripts the moment something changes on a Linux network interface — link up/down, a new IP, a route change — instead of you writing a NetworkManager dispatcher script, a systemd-networkd ExecStartPost hack, or a cron job that polls ip addr every few seconds.
It bridges systemd-networkd, NetworkManager, and dhclient into one event system, with sub-100ms netlink-driven latency, automatic policy routing for multi-homed hosts, a REST API, Prometheus metrics, and a defense-in-depth security model.
- Why netevd?
- Instead of...
- Quick Start
- How It Works
- Configuration
- Script Directories
- Automatic Policy Routing
- Security
- Performance
- REST API
- Enterprise
- License
| Problem | netevd solution |
|---|---|
| Need scripts to run when network state changes | Drop scripts in /etc/netevd/routable.d/ -- done |
| Multi-homed server with broken return-path routing | Automatic per-interface routing tables and policy rules |
| Want real-time network events, not polling | Netlink multicast: sub-100ms latency, zero polling |
| Need to support multiple network managers | One daemon handles networkd, NetworkManager, and dhclient |
| Security concerns with network daemons | Privilege separation, CAP_NET_ADMIN only, input validation |
| What people currently do | Why it's not enough |
|---|---|
NetworkManager dispatcher.d/ scripts |
NetworkManager only — nothing for systemd-networkd or dhclient hosts |
systemd-networkd unit ExecStartPost= |
Fires once at start, not on later link/address/route changes |
ifupdown /etc/network/if-up.d/ |
Debian/Ubuntu-only; ifupdown is legacy on most current distros |
Cron job polling ip addr / ip route |
Seconds of latency, wastes CPU, still needs you to write the diffing logic |
| Custom netlink code in your own daemon | You end up re-implementing debounce, backend detection, and safe script execution — the parts netevd already did |
netevd replaces all of these with one daemon: real netlink events (sub-100ms), one script contract across all three backends, and validated input so your scripts don't need to sanitize $LINK/$ADDRESSES themselves.
curl -LO https://github.com/zyvorai/netevd/releases/download/v0.4.0/netevd-0.4.0-linux-amd64.tar.gz
tar xzf netevd-*-linux-amd64.tar.gz && cd netevd-*-linux-amd64
sudo ./install.sh
sudo systemctl enable --now netevdgit clone https://github.com/zyvorai/netevd.git && cd netevd
cargo build --release
sudo install -Dm755 target/release/netevd /usr/bin/netevd
sudo install -Dm644 systemd/netevd.service /lib/systemd/system/netevd.service
sudo install -Dm644 config/netevd.example.yaml /etc/netevd/netevd.yaml
sudo systemctl enable --now netevdCreate your first script — this runs whenever an interface becomes fully routable:
cat <<'EOF' | sudo tee /etc/netevd/routable.d/01-notify.sh && sudo chmod +x /etc/netevd/routable.d/01-notify.sh
#!/bin/bash
logger -t netevd "Interface $LINK is routable: $ADDRESSES"
EOFSample configuration: config/netevd.example.yaml.
+------------------+
| Linux Kernel |
| Netlink events |
+--------+---------+
|
+-------------------+-------------------+
| | |
+-----------+ +-----------+ +-----------+
| Addresses | | Links | | Routes |
| watcher | | watcher | | watcher |
+-----+-----+ +-----+-----+ +-----+-----+
| | |
+-------------------+-------------------+
|
+--------+---------+
| NetworkState |
| (Arc<RwLock>) |
+--------+---------+
|
+--------------+--------------+
| | |
+-----+-----+ +----+----+ +------+------+
| Routing | | Script | | DBus |
| policy | | exec | | resolved/ |
| rules | | | | hostnamed |
+------------+ +---------+ +-------------+
Event sources -- netevd subscribes to kernel netlink multicast groups and listens for DBus signals from your chosen backend (systemd-networkd, NetworkManager) or watches dhclient lease files via inotify.
State management -- All state is held in a single NetworkState behind Arc<RwLock>, updated by concurrent Tokio tasks. Read locks for queries, write locks for mutations -- no races.
Actions -- On state changes, netevd configures routing policy rules, executes scripts from the matching event directory, and optionally pushes DNS/hostname updates via DBus.
# /etc/netevd/netevd.yaml
system:
log_level: "info"
backend: "systemd-networkd" # or "NetworkManager" or "dhclient"
monitoring:
interfaces: # empty = monitor all
- eth0
- eth1
match_patterns: # globs for address/link/mtu hooks; empty = all
- "eth*"
exclude: # globs to skip; empty = built-in virtual/CNI defaults
- "veth*"
hooks:
debounce_ms: 50 # coalesce netlink bursts per (link, event)
timeout_sec: 30 # per-script timeout
routing:
policy_rules: # auto-create per-interface routing tables
- eth1
backends:
systemd_networkd:
emit_json: true # pass full JSON to scripts via $JSON
dhclient:
use_dns: false
use_domain: false
use_hostname: false
networkmanager: {}Full template: config/netevd.example.yaml
Scripts are organized by the event that triggers them:
| Directory | Trigger | Backends |
|---|---|---|
carrier.d/ |
Cable connected | All |
no-carrier.d/ |
Cable disconnected | All |
configured.d/ |
Interface has IP | systemd-networkd |
degraded.d/ |
Partial configuration | systemd-networkd |
routable.d/ |
Full connectivity | systemd-networkd, dhclient |
activated.d/ |
Device activated | NetworkManager |
disconnected.d/ |
Device disconnected | NetworkManager |
manager.d/ |
Manager state change | All |
routes.d/ |
Routing table change | All |
address-added.d/ |
IP address added to an interface | All (netlink, backend-independent) |
address-removed.d/ |
IP address removed from an interface | All (netlink, backend-independent) |
link-added.d/ |
Interface appears (veth, tap, WireGuard, ...) | All (netlink, backend-independent) |
link-removed.d/ |
Interface disappears | All (netlink, backend-independent) |
mtu.d/ |
Interface MTU changes | All (netlink, backend-independent) |
Scripts run in alphabetical order. Use numeric prefixes (01-, 02-) to control ordering. Non-zero exit codes are logged but don't block other scripts.
The address-*, link-*, and mtu hooks fire per interface based on monitoring.match_patterns / monitoring.exclude (glob lists, defaulting to excluding lo/docker*/veth*/cni*/cilium*), independent of whether that interface is in routing.policy_rules. They're also debounced (hooks.debounce_ms, default 50ms) so a burst of netlink events collapses into one script run per (interface, event) pair. See docs/hooks-contract.md for the full JSON schema and config keys.
Every script receives:
| Variable | Example |
|---|---|
$LINK |
eth0 |
$LINKINDEX |
2 |
$STATE |
routable |
$BACKEND |
systemd-networkd |
$ADDRESSES |
192.168.1.100 10.0.0.5 |
systemd-networkd adds $JSON with full interface data (MTU, driver, DNS, routes).
dhclient adds $DHCP_ADDRESS, $DHCP_GATEWAY, $DHCP_DNS, $DHCP_DOMAIN, $DHCP_HOSTNAME.
The netlink-driven hooks (address-*, link-*, mtu, routes) always set $JSON to a versioned netevd.event.v1 payload ($BACKEND is netlink for these).
For multi-homed servers, netevd solves the classic "wrong interface" problem automatically. When you list an interface under routing.policy_rules, netevd:
- Creates a custom routing table (ID = 200 + interface index)
- Adds
from <ip> lookup <table>andto <ip> lookup <table>rules - Installs a default route via the interface's gateway in that table
- Cleans up automatically when addresses are removed
# After netevd configures eth1 (index 3, IP 192.168.1.100):
$ ip rule list
32765: from 192.168.1.100 lookup 203
32766: to 192.168.1.100 lookup 203
$ ip route show table 203
default via 192.168.1.1 dev eth1netevd follows a defense-in-depth model:
- Privilege separation -- Starts as root, immediately drops to the
netevduser viasetuid/setgid - Minimal capabilities -- Retains only
CAP_NET_ADMIN; child processes inherit nothing - Input validation -- All external data (interface names, IPs, hostnames) is validated; shell metacharacters are rejected
- No shell intermediary -- Scripts are executed directly, not via
sh -c - systemd hardening --
NoNewPrivileges,ProtectSystem=strict,PrivateTmp
Details: Security Policy
| Metric | Value |
|---|---|
| Memory (idle) | 3-5 MB RSS |
| CPU (idle) | < 1% |
| Event latency | < 100ms (netlink multicast) |
| Event-to-script | < 10ms |
| Throughput | 1000+ events/sec |
9 endpoints built on Axum for remote management and monitoring:
curl http://localhost:9090/api/v1/status # Daemon status
curl http://localhost:9090/api/v1/interfaces # List interfaces
curl http://localhost:9090/api/v1/routes # Routing table
curl http://localhost:9090/api/v1/events # Event history
curl http://localhost:9090/metrics # Prometheus metrics
curl http://localhost:9090/health # Health checkREST API and metrics use the daemon HTTP port (default 9090). Tune behavior in /etc/netevd/netevd.yaml — start from config/netevd.example.yaml.
cargo build && cargo test && cargo clippy -- -D warnings| Community Edition (this repo) | Enterprise (zyvor.dev) | |
|---|---|---|
| Support | GitHub Issues | SLA, sales@zyvor.dev, professional services |
| Scope | Self-hosted event hooks | Production rollouts, platform integration |
| Platform | netevd daemon | Full networking stack with netctl, cloud-netconfig, HyperSDK |
Next steps: Demo · ROI · Pricing · Contact · sales@zyvor.dev
Community Edition covers self-hosted event hooks and policy routing. Production SLAs, supported deployments, and the full HyperSDK platform → contact Zyvor (not GitHub Issues). Full detail: docs/enterprise.md.
netevd Community Edition is free and open source, maintained by Susant Sahani · Zyvor AI Labs
- Enterprise / production: zyvor.dev/contact · sales@zyvor.dev
- Community help: GitHub Issues · SECURITY.md
netevd is licensed under the Apache License, Version 2.0.
Copyright © 2026 Zyvor AI Labs Private Limited.
This repository contains only the netevd Community Edition source code.
Other Zyvor products, platforms, services, and commercial offerings are separate works and may be governed by different licenses and terms.
Enterprise: sales@zyvor.dev · General: info@zyvor.dev.
Related: netctl · cloud-netconfig