llro 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llro-1.0.0/LICENSE +21 -0
- llro-1.0.0/PKG-INFO +262 -0
- llro-1.0.0/README.md +240 -0
- llro-1.0.0/pyproject.toml +92 -0
- llro-1.0.0/setup.cfg +4 -0
- llro-1.0.0/src/llro.egg-info/PKG-INFO +262 -0
- llro-1.0.0/src/llro.egg-info/SOURCES.txt +15 -0
- llro-1.0.0/src/llro.egg-info/dependency_links.txt +1 -0
- llro-1.0.0/src/llro.egg-info/entry_points.txt +3 -0
- llro-1.0.0/src/llro.egg-info/requires.txt +7 -0
- llro-1.0.0/src/llro.egg-info/top_level.txt +2 -0
- llro-1.0.0/src/llro.py +720 -0
- llro-1.0.0/src/llro_cli.py +159 -0
- llro-1.0.0/tests/test_import.py +5 -0
- llro-1.0.0/tests/test_integration_compose.py +266 -0
- llro-1.0.0/tests/test_llro.py +705 -0
- llro-1.0.0/tests/test_llro_cli.py +118 -0
llro-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Stefan Meinecke
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
llro-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: llro
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Service to get lowest latency route to targets and set static routes.
|
|
5
|
+
Author: LLRO contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/greensec/LowestLatencyRoutesOptimizer
|
|
8
|
+
Project-URL: Repository, https://github.com/greensec/LowestLatencyRoutesOptimizer
|
|
9
|
+
Project-URL: Issues, https://github.com/greensec/LowestLatencyRoutesOptimizer/issues
|
|
10
|
+
Keywords: network,routing,latency,icmp
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
13
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
14
|
+
Classifier: Topic :: System :: Networking
|
|
15
|
+
Requires-Python: >=3.7
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: icmplib==3.0.4
|
|
19
|
+
Requires-Dist: PyYAML==6.0.1; python_version < "3.8"
|
|
20
|
+
Requires-Dist: PyYAML==6.0.3; python_version >= "3.8"
|
|
21
|
+
Dynamic: license-file
|
|
22
|
+
|
|
23
|
+
# LLRO (Lowest Latency Routes Optimizer)
|
|
24
|
+
|
|
25
|
+
Service to measure ICMP latency from multiple uplinks and keep per-host `/32` routes pinned to the best path.
|
|
26
|
+
|
|
27
|
+
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.
|
|
28
|
+
|
|
29
|
+
For a deeper technical walkthrough, see [HOW_IT_WORKS.md](/home/stefan/github/LowestLatencyRoutesOptimizer/HOW_IT_WORKS.md).
|
|
30
|
+
|
|
31
|
+
## Runtime requirements
|
|
32
|
+
|
|
33
|
+
- Linux with `iproute2` (`ip` command available, default path `/usr/sbin/ip`)
|
|
34
|
+
- Root privileges or equivalent capabilities (`CAP_NET_ADMIN` and raw ICMP capability)
|
|
35
|
+
- Python `>=3.7`
|
|
36
|
+
|
|
37
|
+
## Development setup
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
uv sync
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Run locally
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv run llro --config ./config.yml
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Configuration (recommended model)
|
|
50
|
+
|
|
51
|
+
Start from the example:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
cp config.example.yml config.yml
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Example:
|
|
58
|
+
|
|
59
|
+
```yaml
|
|
60
|
+
monitor:
|
|
61
|
+
- 1.1.1.1
|
|
62
|
+
- 8.8.8.8
|
|
63
|
+
|
|
64
|
+
routes:
|
|
65
|
+
- name: wan_fiber
|
|
66
|
+
device: eth0
|
|
67
|
+
probe_source: 192.168.0.8
|
|
68
|
+
gateway: 192.168.0.1
|
|
69
|
+
- name: wan_lte
|
|
70
|
+
device: wwan0
|
|
71
|
+
probe_source: 10.0.0.2
|
|
72
|
+
gateway: 10.0.0.1
|
|
73
|
+
|
|
74
|
+
also_route:
|
|
75
|
+
1.1.1.1:
|
|
76
|
+
- 1.0.0.1
|
|
77
|
+
8.8.8.8:
|
|
78
|
+
- 8.8.4.4
|
|
79
|
+
|
|
80
|
+
fallback_routes:
|
|
81
|
+
1.1.1.1: wan_fiber
|
|
82
|
+
8.8.8.8: wan_lte
|
|
83
|
+
|
|
84
|
+
rtt_threshold: 20
|
|
85
|
+
packet_loss_threshold: 2
|
|
86
|
+
test_count: 5
|
|
87
|
+
test_interval: 1
|
|
88
|
+
scan_interval: 30
|
|
89
|
+
delete_preadded_routes: true
|
|
90
|
+
# ip_bin: /usr/sbin/ip
|
|
91
|
+
# admin_socket_path: /run/llro/admin.sock
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Key fields
|
|
95
|
+
|
|
96
|
+
- `monitor`: host IPs to probe and route.
|
|
97
|
+
- `routes`: route candidates.
|
|
98
|
+
- `routes[].name`: unique route identifier.
|
|
99
|
+
- `routes[].device`: network device used for route installation.
|
|
100
|
+
- `routes[].probe_source`: source IP used for probing and route `src`.
|
|
101
|
+
- `routes[].gateway`: next-hop gateway for the host route.
|
|
102
|
+
- `also_route`: optional extra IPs that should follow a monitored host route.
|
|
103
|
+
- `fallback_routes`: optional fallback route name per monitored host.
|
|
104
|
+
- `rtt_threshold`: minimum RTT improvement (ms) required before switching.
|
|
105
|
+
- `packet_loss_threshold`: packet-loss threshold (%) that can force switching.
|
|
106
|
+
- `test_count`: number of probe rounds aggregated before routing decisions.
|
|
107
|
+
- `test_interval`: interval between ping packets in a probe run.
|
|
108
|
+
- `scan_interval`: delay between scan cycles.
|
|
109
|
+
- `delete_preadded_routes`: remove existing static `/32` routes for monitored hosts on startup.
|
|
110
|
+
- `ip_bin`: optional `ip` binary path override.
|
|
111
|
+
- `admin_socket_path`: Unix socket path used by `llro-cli` for admin/monitoring.
|
|
112
|
+
|
|
113
|
+
## Legacy config compatibility
|
|
114
|
+
|
|
115
|
+
The old `interfaces` model is still accepted for now:
|
|
116
|
+
|
|
117
|
+
```yaml
|
|
118
|
+
interfaces:
|
|
119
|
+
eth0:
|
|
120
|
+
- 192.168.0.8
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Compatibility mode maps each `interfaces.<device>.<source>` entry to a generated route candidate:
|
|
124
|
+
|
|
125
|
+
- `name: "<device>:<source>"`
|
|
126
|
+
- `probe_source: <source>`
|
|
127
|
+
- `gateway: <source>` (legacy behavior)
|
|
128
|
+
|
|
129
|
+
`fallback_routes` may reference either route names (new) or legacy source IPs (old).
|
|
130
|
+
|
|
131
|
+
## Tooling (Make + uv)
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
make validate # format check + lint + typecheck + dead-code scan
|
|
135
|
+
make test # pytest
|
|
136
|
+
make integration-test # dockerized route mutation integration test
|
|
137
|
+
make build # build sdist/wheel + twine metadata check
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Auto-fix formatting/lint issues:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
make fix
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Run integration tests directly:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
RUN_DOCKER_INTEGRATION=1 uv run pytest -m integration
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The compose integration scenario spins up multiple containers, blocks ICMP on one path, and verifies LLRO switches the monitored host route to the remaining healthy path.
|
|
153
|
+
|
|
154
|
+
## Install as CLI
|
|
155
|
+
|
|
156
|
+
From local checkout:
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
uv pip install .
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
From wheel:
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
uv pip install dist/*.whl
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Then run:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
llro --config /etc/llro.yml
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Admin commands (against running daemon):
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
llro-cli status
|
|
178
|
+
llro-cli override --host 1.1.1.1 --route wan_fiber
|
|
179
|
+
llro-cli disable-switching --all
|
|
180
|
+
llro-cli reset-auto --host 1.1.1.1
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Example output:
|
|
184
|
+
|
|
185
|
+
```text
|
|
186
|
+
$ llro-cli status
|
|
187
|
+
Host 1.1.1.1 | mode=auto | switching=yes | current=wan_fiber | override=-
|
|
188
|
+
wan_fiber: rtt=14.2 ms, loss=0%, alive=yes
|
|
189
|
+
wan_lte: rtt=35.8 ms, loss=0%, alive=yes
|
|
190
|
+
Host 8.8.8.8 | mode=frozen | switching=no | current=wan_lte | override=-
|
|
191
|
+
wan_fiber: rtt=48.1 ms, loss=0%, alive=yes
|
|
192
|
+
wan_lte: rtt=31.6 ms, loss=0%, alive=yes
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
```text
|
|
196
|
+
$ llro-cli status --json
|
|
197
|
+
{
|
|
198
|
+
"hosts": [
|
|
199
|
+
{
|
|
200
|
+
"current_route": "wan_fiber",
|
|
201
|
+
"host": "1.1.1.1",
|
|
202
|
+
"mode": "auto",
|
|
203
|
+
"override_route": null,
|
|
204
|
+
"routes": {
|
|
205
|
+
"wan_fiber": {
|
|
206
|
+
"avg_loss": 0,
|
|
207
|
+
"avg_rtt": 14.2,
|
|
208
|
+
"is_alive": true
|
|
209
|
+
},
|
|
210
|
+
"wan_lte": {
|
|
211
|
+
"avg_loss": 0,
|
|
212
|
+
"avg_rtt": 35.8,
|
|
213
|
+
"is_alive": true
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
"switching_enabled": true
|
|
217
|
+
}
|
|
218
|
+
]
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
```text
|
|
223
|
+
$ llro-cli override --host 1.1.1.1 --route wan_lte
|
|
224
|
+
{"host": "1.1.1.1", "mode": "override", "route": "wan_lte"}
|
|
225
|
+
|
|
226
|
+
$ llro-cli disable-switching --all
|
|
227
|
+
{"hosts": ["1.1.1.1", "8.8.8.8"], "mode": "frozen"}
|
|
228
|
+
|
|
229
|
+
$ llro-cli reset-auto --host 1.1.1.1
|
|
230
|
+
{"hosts": ["1.1.1.1"], "mode": "auto"}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## systemd service
|
|
234
|
+
|
|
235
|
+
Use the provided unit as a base and verify the executable path in your environment:
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
which llro
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Install:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
sudo cp llro.service /etc/systemd/system/llro.service
|
|
245
|
+
sudo systemctl daemon-reload
|
|
246
|
+
sudo systemctl enable --now llro
|
|
247
|
+
sudo systemctl status llro
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## PyPI release flow
|
|
251
|
+
|
|
252
|
+
- Local dry-run build: `make build`
|
|
253
|
+
- Publish manually with Twine:
|
|
254
|
+
- `make publish-testpypi`
|
|
255
|
+
- `make publish-pypi`
|
|
256
|
+
- GitHub Actions publish:
|
|
257
|
+
- Push tag `v*` to trigger `.github/workflows/publish-to-pypi.yml`
|
|
258
|
+
- Workflow uses trusted publishing (`id-token`) for PyPI
|
|
259
|
+
|
|
260
|
+
## Contributing
|
|
261
|
+
|
|
262
|
+
Inspired by <https://malaty.net/linux-lowest-latency-routes-optimizer/>
|
llro-1.0.0/README.md
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# LLRO (Lowest Latency Routes Optimizer)
|
|
2
|
+
|
|
3
|
+
Service to measure ICMP latency from multiple uplinks and keep per-host `/32` routes pinned to the best path.
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
For a deeper technical walkthrough, see [HOW_IT_WORKS.md](/home/stefan/github/LowestLatencyRoutesOptimizer/HOW_IT_WORKS.md).
|
|
8
|
+
|
|
9
|
+
## Runtime requirements
|
|
10
|
+
|
|
11
|
+
- Linux with `iproute2` (`ip` command available, default path `/usr/sbin/ip`)
|
|
12
|
+
- Root privileges or equivalent capabilities (`CAP_NET_ADMIN` and raw ICMP capability)
|
|
13
|
+
- Python `>=3.7`
|
|
14
|
+
|
|
15
|
+
## Development setup
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv sync
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Run locally
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv run llro --config ./config.yml
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Configuration (recommended model)
|
|
28
|
+
|
|
29
|
+
Start from the example:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
cp config.example.yml config.yml
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
|
|
37
|
+
```yaml
|
|
38
|
+
monitor:
|
|
39
|
+
- 1.1.1.1
|
|
40
|
+
- 8.8.8.8
|
|
41
|
+
|
|
42
|
+
routes:
|
|
43
|
+
- name: wan_fiber
|
|
44
|
+
device: eth0
|
|
45
|
+
probe_source: 192.168.0.8
|
|
46
|
+
gateway: 192.168.0.1
|
|
47
|
+
- name: wan_lte
|
|
48
|
+
device: wwan0
|
|
49
|
+
probe_source: 10.0.0.2
|
|
50
|
+
gateway: 10.0.0.1
|
|
51
|
+
|
|
52
|
+
also_route:
|
|
53
|
+
1.1.1.1:
|
|
54
|
+
- 1.0.0.1
|
|
55
|
+
8.8.8.8:
|
|
56
|
+
- 8.8.4.4
|
|
57
|
+
|
|
58
|
+
fallback_routes:
|
|
59
|
+
1.1.1.1: wan_fiber
|
|
60
|
+
8.8.8.8: wan_lte
|
|
61
|
+
|
|
62
|
+
rtt_threshold: 20
|
|
63
|
+
packet_loss_threshold: 2
|
|
64
|
+
test_count: 5
|
|
65
|
+
test_interval: 1
|
|
66
|
+
scan_interval: 30
|
|
67
|
+
delete_preadded_routes: true
|
|
68
|
+
# ip_bin: /usr/sbin/ip
|
|
69
|
+
# admin_socket_path: /run/llro/admin.sock
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Key fields
|
|
73
|
+
|
|
74
|
+
- `monitor`: host IPs to probe and route.
|
|
75
|
+
- `routes`: route candidates.
|
|
76
|
+
- `routes[].name`: unique route identifier.
|
|
77
|
+
- `routes[].device`: network device used for route installation.
|
|
78
|
+
- `routes[].probe_source`: source IP used for probing and route `src`.
|
|
79
|
+
- `routes[].gateway`: next-hop gateway for the host route.
|
|
80
|
+
- `also_route`: optional extra IPs that should follow a monitored host route.
|
|
81
|
+
- `fallback_routes`: optional fallback route name per monitored host.
|
|
82
|
+
- `rtt_threshold`: minimum RTT improvement (ms) required before switching.
|
|
83
|
+
- `packet_loss_threshold`: packet-loss threshold (%) that can force switching.
|
|
84
|
+
- `test_count`: number of probe rounds aggregated before routing decisions.
|
|
85
|
+
- `test_interval`: interval between ping packets in a probe run.
|
|
86
|
+
- `scan_interval`: delay between scan cycles.
|
|
87
|
+
- `delete_preadded_routes`: remove existing static `/32` routes for monitored hosts on startup.
|
|
88
|
+
- `ip_bin`: optional `ip` binary path override.
|
|
89
|
+
- `admin_socket_path`: Unix socket path used by `llro-cli` for admin/monitoring.
|
|
90
|
+
|
|
91
|
+
## Legacy config compatibility
|
|
92
|
+
|
|
93
|
+
The old `interfaces` model is still accepted for now:
|
|
94
|
+
|
|
95
|
+
```yaml
|
|
96
|
+
interfaces:
|
|
97
|
+
eth0:
|
|
98
|
+
- 192.168.0.8
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Compatibility mode maps each `interfaces.<device>.<source>` entry to a generated route candidate:
|
|
102
|
+
|
|
103
|
+
- `name: "<device>:<source>"`
|
|
104
|
+
- `probe_source: <source>`
|
|
105
|
+
- `gateway: <source>` (legacy behavior)
|
|
106
|
+
|
|
107
|
+
`fallback_routes` may reference either route names (new) or legacy source IPs (old).
|
|
108
|
+
|
|
109
|
+
## Tooling (Make + uv)
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
make validate # format check + lint + typecheck + dead-code scan
|
|
113
|
+
make test # pytest
|
|
114
|
+
make integration-test # dockerized route mutation integration test
|
|
115
|
+
make build # build sdist/wheel + twine metadata check
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Auto-fix formatting/lint issues:
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
make fix
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Run integration tests directly:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
RUN_DOCKER_INTEGRATION=1 uv run pytest -m integration
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The compose integration scenario spins up multiple containers, blocks ICMP on one path, and verifies LLRO switches the monitored host route to the remaining healthy path.
|
|
131
|
+
|
|
132
|
+
## Install as CLI
|
|
133
|
+
|
|
134
|
+
From local checkout:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
uv pip install .
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
From wheel:
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
uv pip install dist/*.whl
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Then run:
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
llro --config /etc/llro.yml
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Admin commands (against running daemon):
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
llro-cli status
|
|
156
|
+
llro-cli override --host 1.1.1.1 --route wan_fiber
|
|
157
|
+
llro-cli disable-switching --all
|
|
158
|
+
llro-cli reset-auto --host 1.1.1.1
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Example output:
|
|
162
|
+
|
|
163
|
+
```text
|
|
164
|
+
$ llro-cli status
|
|
165
|
+
Host 1.1.1.1 | mode=auto | switching=yes | current=wan_fiber | override=-
|
|
166
|
+
wan_fiber: rtt=14.2 ms, loss=0%, alive=yes
|
|
167
|
+
wan_lte: rtt=35.8 ms, loss=0%, alive=yes
|
|
168
|
+
Host 8.8.8.8 | mode=frozen | switching=no | current=wan_lte | override=-
|
|
169
|
+
wan_fiber: rtt=48.1 ms, loss=0%, alive=yes
|
|
170
|
+
wan_lte: rtt=31.6 ms, loss=0%, alive=yes
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
```text
|
|
174
|
+
$ llro-cli status --json
|
|
175
|
+
{
|
|
176
|
+
"hosts": [
|
|
177
|
+
{
|
|
178
|
+
"current_route": "wan_fiber",
|
|
179
|
+
"host": "1.1.1.1",
|
|
180
|
+
"mode": "auto",
|
|
181
|
+
"override_route": null,
|
|
182
|
+
"routes": {
|
|
183
|
+
"wan_fiber": {
|
|
184
|
+
"avg_loss": 0,
|
|
185
|
+
"avg_rtt": 14.2,
|
|
186
|
+
"is_alive": true
|
|
187
|
+
},
|
|
188
|
+
"wan_lte": {
|
|
189
|
+
"avg_loss": 0,
|
|
190
|
+
"avg_rtt": 35.8,
|
|
191
|
+
"is_alive": true
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
"switching_enabled": true
|
|
195
|
+
}
|
|
196
|
+
]
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
```text
|
|
201
|
+
$ llro-cli override --host 1.1.1.1 --route wan_lte
|
|
202
|
+
{"host": "1.1.1.1", "mode": "override", "route": "wan_lte"}
|
|
203
|
+
|
|
204
|
+
$ llro-cli disable-switching --all
|
|
205
|
+
{"hosts": ["1.1.1.1", "8.8.8.8"], "mode": "frozen"}
|
|
206
|
+
|
|
207
|
+
$ llro-cli reset-auto --host 1.1.1.1
|
|
208
|
+
{"hosts": ["1.1.1.1"], "mode": "auto"}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## systemd service
|
|
212
|
+
|
|
213
|
+
Use the provided unit as a base and verify the executable path in your environment:
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
which llro
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Install:
|
|
220
|
+
|
|
221
|
+
```bash
|
|
222
|
+
sudo cp llro.service /etc/systemd/system/llro.service
|
|
223
|
+
sudo systemctl daemon-reload
|
|
224
|
+
sudo systemctl enable --now llro
|
|
225
|
+
sudo systemctl status llro
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## PyPI release flow
|
|
229
|
+
|
|
230
|
+
- Local dry-run build: `make build`
|
|
231
|
+
- Publish manually with Twine:
|
|
232
|
+
- `make publish-testpypi`
|
|
233
|
+
- `make publish-pypi`
|
|
234
|
+
- GitHub Actions publish:
|
|
235
|
+
- Push tag `v*` to trigger `.github/workflows/publish-to-pypi.yml`
|
|
236
|
+
- Workflow uses trusted publishing (`id-token`) for PyPI
|
|
237
|
+
|
|
238
|
+
## Contributing
|
|
239
|
+
|
|
240
|
+
Inspired by <https://malaty.net/linux-lowest-latency-routes-optimizer/>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=65,<83", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "llro"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Service to get lowest latency route to targets and set static routes."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.7"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "LLRO contributors" }]
|
|
13
|
+
keywords = ["network", "routing", "latency", "icmp"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
17
|
+
"Operating System :: POSIX :: Linux",
|
|
18
|
+
"Topic :: System :: Networking",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"icmplib==3.0.4",
|
|
22
|
+
"PyYAML==6.0.1; python_version < '3.8'",
|
|
23
|
+
"PyYAML==6.0.3; python_version >= '3.8'",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/greensec/LowestLatencyRoutesOptimizer"
|
|
28
|
+
Repository = "https://github.com/greensec/LowestLatencyRoutesOptimizer"
|
|
29
|
+
Issues = "https://github.com/greensec/LowestLatencyRoutesOptimizer/issues"
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
llro = "llro:main"
|
|
33
|
+
llro-cli = "llro_cli:main"
|
|
34
|
+
|
|
35
|
+
[tool.setuptools]
|
|
36
|
+
py-modules = ["llro", "llro_cli"]
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.package-dir]
|
|
39
|
+
"" = "src"
|
|
40
|
+
|
|
41
|
+
[dependency-groups]
|
|
42
|
+
dev = [
|
|
43
|
+
"build>=1.2.2; python_version >= '3.8'",
|
|
44
|
+
"pyright>=1.1.408; python_version >= '3.8'",
|
|
45
|
+
"pytest>=7.4,<8; python_version < '3.8'",
|
|
46
|
+
"pytest>=8.3; python_version >= '3.8'",
|
|
47
|
+
"pytest-cov>=4.1,<5; python_version < '3.8'",
|
|
48
|
+
"pytest-cov>=5; python_version >= '3.8'",
|
|
49
|
+
"ruff>=0.15.8",
|
|
50
|
+
"ruff>=0.12.0; python_version >= '3.8'",
|
|
51
|
+
"twine>=6.2.0; python_version >= '3.10'",
|
|
52
|
+
"vulture>=2.14; python_version >= '3.8'",
|
|
53
|
+
"xenon>=0.9.3; python_version >= '3.10'",
|
|
54
|
+
"vulture>=2.9.1",
|
|
55
|
+
"pyright>=1.1.408",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[tool.ruff]
|
|
59
|
+
exclude = ["dist", "build", ".venv"]
|
|
60
|
+
line-length = 120
|
|
61
|
+
indent-width = 4
|
|
62
|
+
|
|
63
|
+
[tool.ruff.format]
|
|
64
|
+
quote-style = "double"
|
|
65
|
+
indent-style = "space"
|
|
66
|
+
line-ending = "lf"
|
|
67
|
+
|
|
68
|
+
[tool.ruff.lint]
|
|
69
|
+
select = ["E", "F", "I"]
|
|
70
|
+
ignore = ["E501"]
|
|
71
|
+
|
|
72
|
+
[tool.pytest.ini_options]
|
|
73
|
+
addopts = [
|
|
74
|
+
"-q",
|
|
75
|
+
"--cov=llro",
|
|
76
|
+
"--cov-report=term-missing",
|
|
77
|
+
]
|
|
78
|
+
pythonpath = ["src"]
|
|
79
|
+
markers = [
|
|
80
|
+
"integration: real integration tests that require external services/tools",
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
[tool.pyright]
|
|
84
|
+
pythonVersion = "3.7"
|
|
85
|
+
include = ["src", "tests"]
|
|
86
|
+
exclude = ["**/__pycache__", "**/node_modules", "**/dist", "**/build", "**/.venv"]
|
|
87
|
+
|
|
88
|
+
[tool.vulture]
|
|
89
|
+
exclude = ["dist", "build", "venv", ".venv", "tests"]
|
|
90
|
+
min_confidence = 80
|
|
91
|
+
paths = ["src"]
|
|
92
|
+
sort_by_size = true
|
llro-1.0.0/setup.cfg
ADDED