LLRO (Lowest Latency Routes Optimizer)
Service to measure ICMP latency from multiple uplinks and keep per-host /32 routes pinned to the best path.
LLRO continuously probes each monitored destination through each configured uplink source, compares packet loss and latency, and installs host routes using Linux ip route so traffic for that destination follows the healthiest path. It can freeze or override routing decisions per host through a local admin socket, and it can fall back to predefined routes when probes fail. In short, it automates per-destination path selection based on live network conditions instead of static one-time routing choices.
For a deeper technical walkthrough, see HOW_IT_WORKS.md.
Runtime requirements
- Linux with
iproute2(ipcommand available, default path/usr/sbin/ip) - Root privileges or equivalent capabilities (
CAP_NET_ADMINand raw ICMP capability) - Python
>=3.11
Development setup
uv sync
Run locally
uv run llro --config ./config.yml
Configuration (recommended model)
Start from the example:
cp config.example.yml config.yml
Example:
monitor:
- 1.1.1.1
- 8.8.8.8
routes:
- name: wan_fiber
device: eth0
probe_source: 192.168.0.8
gateway: 192.168.0.1
- name: wan_lte
device: wwan0
probe_source: 10.0.0.2
gateway: 10.0.0.1
also_route:
1.1.1.1:
- 1.0.0.1
8.8.8.8:
- 8.8.4.4
fallback_routes:
1.1.1.1: wan_fiber
8.8.8.8: wan_lte
rtt_threshold: 20
packet_loss_threshold: 2
pings_per_probe: 5
decision_cycles: 5
test_interval: 1
scan_interval: 30
delete_preadded_routes: true
# probe_timeout: 2
# bind_to_device: false
# ewma_alpha: 0.4
# switch_cooldown: 60
# ip_bin: /usr/sbin/ip
# admin_socket_path: /run/llro/admin.sock
Key fields
monitor: host IPs to probe and route.routes: route candidates.routes[].name: unique route identifier.routes[].device: network device used for route installation.routes[].probe_source: source IP used for probing and routesrc.routes[].gateway: next-hop gateway for the host route.also_route: optional extra IPs that should follow a monitored host route.fallback_routes: optional fallback route name per monitored host.rtt_threshold: minimum RTT improvement (ms) required before switching.packet_loss_threshold: packet-loss threshold (%) that can force switching.pings_per_probe: ICMP echo requests sent per host per route in each probe cycle.decision_cycles: number of probe cycles aggregated before a routing decision.test_interval: interval between ping packets in a probe run.probe_timeout: seconds to wait for each ICMP reply (default2).payload_size: ICMP Echo Request payload size in bytes.scan_interval: delay between scan cycles.bind_to_device: bind probe sockets to each route'sdevice(SO_BINDTODEVICE) so probes egress that interface (defaultfalse; see "Probe path pinning").ewma_alpha: smoothing factor(0, 1]applied to per-route RTT/loss before decisions (default0.4; use1.0to disable smoothing).switch_cooldown: minimum seconds between RTT-improvement switches per host (default60;0disables; loss/dead failover is never delayed).test_count: deprecated; when set without the new keys it maps to bothpings_per_probeanddecision_cycles.delete_preadded_routes: remove existing static/32routes for monitored hosts on startup.ip_bin: optionalipbinary path override.admin_socket_path: Unix socket path used byllro-clifor admin/monitoring.
Probe path pinning
Probes are sent with each route's probe_source as their source address. Source binding alone does not fix the egress interface: once a <host>/32 route is installed, all probes for that host follow it in the forward direction, so per-uplink metrics reflect hybrid paths (forward via the current route, return via the probe's uplink) — and packets leaving one uplink with another uplink's source IP may be dropped by upstream anti-spoofing (uRPF), showing up as phantom loss.
Two ways to make probes actually traverse the intended uplink:
bind_to_device: true— LLRO setsSO_BINDTODEVICEon each probe socket, pinning egress to the route'sdevice. The bound interface still needs a usable route to the destination (e.g. its own default route in the main table); otherwise probes fail withENETUNREACH. RequiresCAP_NET_RAW(already required by LLRO).- Alternatively, keep
bind_to_deviceoff and configure per-source policy routing yourself, e.g.ip rule from <probe_source> table <uplink-table>plus a default route in that table. The probe's source binding then selects the right table.
Legacy config compatibility
The old interfaces model is still accepted for now:
interfaces:
eth0:
- 192.168.0.8
Compatibility mode maps each interfaces.<device>.<source> entry to a generated route candidate:
name: "<device>:<source>"probe_source: <source>gateway: <source>(legacy behavior)
fallback_routes may reference either route names (new) or legacy source IPs (old).
Tooling (Make + uv)
make validate # format check + lint + typecheck + dead-code/complexity/security scans
make test # unit tests (pytest -m "not integration")
make test-integration # dockerized integration tests (needs a docker daemon)
make build # build sdist/wheel + twine metadata check
Auto-fix formatting/lint issues:
make fix
Run integration tests directly:
uv run pytest tests/test_integration_compose.py -v -m integration
The compose integration scenario spins up multiple containers and verifies the daemon end-to-end: admin socket controls (status/override/disable-switching/reset-auto via llro-cli), route switchover when ICMP is blocked on the active path, and fallback_routes when all paths fail.
Install as CLI
From local checkout:
uv pip install .
From wheel:
uv pip install dist/*.whl
Then run:
llro --config /etc/llro.yml
Admin commands (against running daemon):
llro-cli status
llro-cli override --host 1.1.1.1 --route wan_fiber
llro-cli disable-switching --all
llro-cli reset-auto --host 1.1.1.1
Example output:
$ llro-cli status
Host 1.1.1.1 | mode=auto | switching=yes | current=wan_fiber | override=-
wan_fiber: rtt=14.2 ms, loss=0%, alive=yes
wan_lte: rtt=35.8 ms, loss=0%, alive=yes
Host 8.8.8.8 | mode=frozen | switching=no | current=wan_lte | override=-
wan_fiber: rtt=48.1 ms, loss=0%, alive=yes
wan_lte: rtt=31.6 ms, loss=0%, alive=yes
$ llro-cli status --json
{
"hosts": [
{
"current_route": "wan_fiber",
"host": "1.1.1.1",
"mode": "auto",
"override_route": null,
"routes": {
"wan_fiber": {
"avg_loss": 0,
"avg_rtt": 14.2,
"is_alive": true
},
"wan_lte": {
"avg_loss": 0,
"avg_rtt": 35.8,
"is_alive": true
}
},
"switching_enabled": true
}
]
}
$ llro-cli override --host 1.1.1.1 --route wan_lte
{"host": "1.1.1.1", "mode": "override", "route": "wan_lte", "route_applied": true}
$ llro-cli disable-switching --all
{"hosts": ["1.1.1.1", "8.8.8.8"], "mode": "frozen"}
$ llro-cli reset-auto --host 1.1.1.1
{"hosts": ["1.1.1.1"], "mode": "auto"}
systemd service
The provided unit runs LLRO under a dedicated unprivileged user with only the network capabilities it needs (CAP_NET_ADMIN and CAP_NET_RAW).
Prerequisites
Create the service user and verify the executable path:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin llro
which llro
# Update ExecStart in llro.service if the path differs from /usr/local/bin/llro
The unit uses RuntimeDirectory=llro so /run/llro is created automatically with correct ownership for the admin socket.
Install
sudo cp llro.service /etc/systemd/system/llro.service
sudo systemctl daemon-reload
sudo systemctl enable --now llro
sudo systemctl status llro
Verifying capabilities
On a hardened unit, LLRO must still be able to create raw ICMP sockets and modify routes. If the service fails to probe or install routes, check the journal for capability denials:
sudo journalctl -u llro -n 50
PyPI release flow
- Local dry-run build:
make build - Publish manually with Twine:
make publish-testpypimake publish-pypi- GitHub Actions publish:
- Push tag
v*.*.*to trigger.github/workflows/release.yml - Workflow verifies the tag matches the package version, attests build provenance, creates a GitHub release, and publishes to PyPI via trusted publishing (
id-token)
Contributing
Inspired by https://malaty.net/linux-lowest-latency-routes-optimizer/
Release files for llro 1.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| llro-1.1.0.tar.gz | 29.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| llro-1.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 47.9 kB
Release files / llro-1.1.0.tar.gz
| Download URL | llro-1.1.0.tar.gz |
|---|---|
| Size | 29.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bd8a94f9378209804e75e1ecc2dbec28e0c537e03fdcd8a21c0295938e5b8715
|
|
BLAKE2b-256 checksum How to use checksums |
dd4ac1f2d30aedc17a257bdfb1c5dec6f80761e53e0104503badc522a7c0de75
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency logRelease files / llro-1.1.0-py3-none-any.whl
| Download URL | llro-1.1.0-py3-none-any.whl |
|---|---|
| Size | 18.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
28963471ceada0fddc775366c2b66a08aa1720dde24ed3956309fab65bb0d424
|
|
BLAKE2b-256 checksum How to use checksums |
b823f93e1169e4bf835a2728b6676853bbbc4d4d92478b9bc2bfa8fd9de26c8b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency log