ms-split-router 0.1.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.
Files changed (36) hide show
  1. ms_split_router-0.1.0/.gitignore +12 -0
  2. ms_split_router-0.1.0/CHANGELOG.md +43 -0
  3. ms_split_router-0.1.0/LICENSE +21 -0
  4. ms_split_router-0.1.0/PKG-INFO +326 -0
  5. ms_split_router-0.1.0/README.md +298 -0
  6. ms_split_router-0.1.0/pyproject.toml +75 -0
  7. ms_split_router-0.1.0/src/ms_split_router/__init__.py +4 -0
  8. ms_split_router-0.1.0/src/ms_split_router/__main__.py +4 -0
  9. ms_split_router-0.1.0/src/ms_split_router/cli.py +192 -0
  10. ms_split_router-0.1.0/src/ms_split_router/config.py +611 -0
  11. ms_split_router-0.1.0/src/ms_split_router/data.py +152 -0
  12. ms_split_router-0.1.0/src/ms_split_router/discover.py +146 -0
  13. ms_split_router-0.1.0/src/ms_split_router/domains.py +117 -0
  14. ms_split_router-0.1.0/src/ms_split_router/example_config.yaml +81 -0
  15. ms_split_router-0.1.0/src/ms_split_router/generate.py +200 -0
  16. ms_split_router-0.1.0/src/ms_split_router/install.py +163 -0
  17. ms_split_router-0.1.0/src/ms_split_router/netutil.py +105 -0
  18. ms_split_router-0.1.0/src/ms_split_router/py.typed +0 -0
  19. ms_split_router-0.1.0/src/ms_split_router/render.py +221 -0
  20. ms_split_router-0.1.0/src/ms_split_router/select.py +111 -0
  21. ms_split_router-0.1.0/tests/conftest.py +108 -0
  22. ms_split_router-0.1.0/tests/fixtures/collector.json +113 -0
  23. ms_split_router-0.1.0/tests/fixtures/golden/dropin/etc/systemd/network/10-eth1.network.d/50-ms-split-router-corp-vpn.conf +15 -0
  24. ms_split_router-0.1.0/tests/fixtures/golden/dropin/etc/systemd/resolved.conf.d/10-ms-split-router.conf +5 -0
  25. ms_split_router-0.1.0/tests/fixtures/golden/full/etc/systemd/network/10-ms-split-corp-vpn.link +11 -0
  26. ms_split_router-0.1.0/tests/fixtures/golden/full/etc/systemd/network/10-ms-split-corp-vpn.network +69 -0
  27. ms_split_router-0.1.0/tests/fixtures/golden/full/etc/systemd/network/20-ms-default-internet.network +17 -0
  28. ms_split_router-0.1.0/tests/fixtures/golden/full/etc/systemd/networkd.conf.d/10-ms-split-router.conf +6 -0
  29. ms_split_router-0.1.0/tests/fixtures/golden/full/etc/systemd/resolved.conf.d/10-ms-split-router.conf +5 -0
  30. ms_split_router-0.1.0/tests/smoke-config.yaml +10 -0
  31. ms_split_router-0.1.0/tests/test_cli.py +225 -0
  32. ms_split_router-0.1.0/tests/test_config.py +154 -0
  33. ms_split_router-0.1.0/tests/test_data.py +119 -0
  34. ms_split_router-0.1.0/tests/test_generate.py +177 -0
  35. ms_split_router-0.1.0/tests/test_install.py +84 -0
  36. ms_split_router-0.1.0/tests/test_select_domains.py +147 -0
@@ -0,0 +1,12 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ build/
6
+ dist/
7
+ .venv/
8
+ .pytest_cache/
9
+ .ruff_cache/
10
+
11
+ # Generated output
12
+ output/
@@ -0,0 +1,43 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to
6
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ### Added
11
+
12
+ - `--print-example-config` prints the annotated YAML bundled in the installed package without fetching data,
13
+ inspecting links or modifying system configuration.
14
+ - `--ipv6`/`--no-ipv6` options that override `routes.ipv6` on every split link. With `--no-ipv6`, fetched data is
15
+ also collected with `--no-ipv6`.
16
+
17
+ ### Changed
18
+
19
+ - The canonical example moved from `examples/config.yaml` to `src/ms_split_router/example_config.yaml` so it is
20
+ available in wheels and editable installations. It defaults to live fetching rather than a repository-relative
21
+ data file; use `--print-example-config` to create a configuration outside a checkout.
22
+ - `routes.ipv6: true` (and `--ipv6`) also fails when the collector data was gathered with `--no-ipv6`
23
+ (`metadata.options.ipv6: false`), instead of writing no Microsoft IPv6 routes. With `auto`, that case is logged
24
+ at info level.
25
+
26
+ ## [0.1.0]
27
+
28
+ ### Added
29
+
30
+ - `ms-split-router` command (also `python -m ms_split_router`) that renders per-link split-tunnel configuration for
31
+ WSL2 mirrored networking from [ms-cloud-endpoints](https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter.ms-cloud-endpoints) data.
32
+ - YAML configuration with any number of split links and default links, each matched by MAC address, alternative
33
+ name or interface name, rendered as a full `.network` file or a `.network.d` drop-in.
34
+ - Route selection from Azure Service Tags (globs), Microsoft 365 endpoint sets (instance, service area, category,
35
+ required, ExpressRoute) and the "other endpoints" page, plus extra and excluded prefixes. Prefixes are collapsed.
36
+ - Routing-domain derivation from Microsoft 365 URLs, Private Endpoint DNS zones and the "other endpoints" page, with
37
+ root-domain collapsing, a built-in third-party (CA/CDN) denylist and extra/excluded domains.
38
+ - Gateway and on-link auto-detection from `ip -json`; IPv6 routes only when the link has an IPv6 gateway (`auto`).
39
+ - Optional `.link` file that pins a stable alternative name to the split adapter's MAC address.
40
+ - `networkd.conf.d` (`ManageForeignRoutes=no`, `ManageForeignRoutingPolicyRules=no`) and `resolved.conf.d`
41
+ (`DNSSEC=no`, extra keys) drop-ins.
42
+ - Data from a collector file or by running `ms-cloud-endpoints` with arguments narrowed to the configuration.
43
+ - `--output`, `--install`/`--root`, `--diff`, `--check`, `--prune`, `--reload` and `--show-links`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Radux Technologies
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.
@@ -0,0 +1,326 @@
1
+ Metadata-Version: 2.5
2
+ Name: ms-split-router
3
+ Version: 0.1.0
4
+ Summary: Generate systemd-networkd and systemd-resolved per-link split-tunnel configuration for Microsoft 365, Entra and Azure from ms-cloud-endpoints data.
5
+ Project-URL: Homepage, https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter
6
+ Project-URL: Source, https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter.ms-split-router
7
+ Project-URL: Documentation, https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter.ms-split-router/blob/main/README.md
8
+ Project-URL: Issues, https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter.ms-split-router/issues
9
+ Project-URL: Changelog, https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter.ms-split-router/blob/main/CHANGELOG.md
10
+ Author-email: Jayonics <jameshopwood1998@gmail.com>
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: azure,entra,microsoft-365,office-365,routing,split-tunnel,systemd-networkd,systemd-resolved,wsl
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Environment :: Console
16
+ Classifier: Intended Audience :: System Administrators
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Topic :: System :: Networking
24
+ Classifier: Typing :: Typed
25
+ Requires-Python: >=3.12
26
+ Requires-Dist: pyyaml>=6.0
27
+ Description-Content-Type: text/markdown
28
+
29
+ # ms-split-router
30
+
31
+ Generate **systemd-networkd** and **systemd-resolved** per-link split-tunnel configuration for **Microsoft 365,
32
+ Microsoft Entra and Azure** on a WSL2 VM that uses
33
+ [mirrored mode networking](https://learn.microsoft.com/en-us/windows/wsl/networking#mirrored-mode-networking).
34
+
35
+ It is the rendering stage of
36
+ [Azure-O365-DNS-UDR-SplitRouter](https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter). It reads the
37
+ document written by [`ms-cloud-endpoints`](https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter.ms-cloud-endpoints) (or runs that command for you) and writes:
38
+
39
+ | File | Purpose |
40
+ |------|---------|
41
+ | `/etc/systemd/network/10-ms-split-<id>.network` (or a `.network.d` drop-in) | Split link: `Domains=~…` routing-only domains, `DNS=`, `DNSDefaultRoute=no` and one `[Route]` per Microsoft prefix |
42
+ | `/etc/systemd/network/10-ms-split-<id>.link` (optional) | Gives the split adapter a stable alternative name, matched by MAC address |
43
+ | `/etc/systemd/network/20-ms-default-<id>.network` (or a drop-in) | Default links: `Domains=~.`, `DNSDefaultRoute=yes`, `DNS=` |
44
+ | `/etc/systemd/networkd.conf.d/10-ms-split-router.conf` | `ManageForeignRoutes=no`, `ManageForeignRoutingPolicyRules=no` so networkd keeps WSL's routes |
45
+ | `/etc/systemd/resolved.conf.d/10-ms-split-router.conf` | Global resolved settings (`DNSSEC=no` by default, plus any extra keys) |
46
+
47
+ - [Features](#features)
48
+ - [Requirements](#requirements)
49
+ - [Installation](#installation)
50
+ - [Quick start](#quick-start)
51
+ - [Configuration](#configuration)
52
+ - [Options](#options)
53
+ - [How the output is built](#how-the-output-is-built)
54
+ - [Notes and limitations](#notes-and-limitations)
55
+ - [Development](#development)
56
+ - [License](#license)
57
+
58
+ ## Features
59
+
60
+ - Any number of **split links**, each with its own routes, domains and DNS servers (for example Microsoft 365 over a
61
+ VPN adapter and Azure over an ExpressRoute adapter). Overlapping prefixes or domains between links are allowed.
62
+ - Links are matched by **MAC address**, **alternative name** or interface name. An optional `.link` file pins an
63
+ alternative name (for example `msvpn`) to the adapter's MAC.
64
+ - **Full `.network` files or `.network.d` drop-ins** on top of a `.network` file you already own.
65
+ - **Routes** from Azure Service Tags (case-insensitive globs, default `AzureCloud`), Microsoft 365 endpoint sets
66
+ (instance, service area, category, required, ExpressRoute), the "other endpoints" page, extra prefixes, and
67
+ excluded prefixes. Prefixes are widened (IPv6 to `/48` by default) and collapsed so they stay within networkd's
68
+ limit.
69
+ - **Routing domains** derived from the Microsoft 365 URLs and the Private Endpoint DNS zones, reduced to their root
70
+ domains. Certificate authority and CDN domains (`digicert.com`, `akamaihd.net`, …) are dropped by default.
71
+ - The **gateway and on-link flag are auto-detected** from the link's current default route (`ip -json route`).
72
+ IPv6 routes are only written when the link has an IPv6 gateway. `--ipv6`/`--no-ipv6` require them or leave them
73
+ out on every split link.
74
+ - Output is **deterministic** (no timestamps), so `--check`, `--diff` and only-if-changed installs are reliable.
75
+ Every file carries a marker header, which lets `--prune` remove stale files safely.
76
+ - The only runtime dependency is [PyYAML](https://pypi.org/project/PyYAML/). It doesn't import `ms-cloud-endpoints`.
77
+
78
+ ## Requirements
79
+
80
+ - Python 3.12 or later (tested on 3.12, 3.13 and 3.14)
81
+ - Linux with iproute2 (`ip -json`) for auto-detection, `--show-links` and `--reload`
82
+ - On the target VM: systemd ≥ 249 with `systemd-networkd` and `systemd-resolved`, and WSL mirrored networking with
83
+ `dnsTunneling=false` (see [docs/configuration.md](https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter/blob/main/docs/configuration.md#prerequisites))
84
+ - To fetch data live: the `ms-cloud-endpoints` command on `PATH`
85
+
86
+ ## Installation
87
+
88
+ From a clone of the [parent project](https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter) with submodules
89
+ (`git clone --recurse-submodules`), at its root:
90
+
91
+ ```bash
92
+ pipx install ./ms-cloud-endpoints ./ms-split-router # both commands, isolated
93
+ # or
94
+ pip install ./ms-split-router
95
+ ```
96
+
97
+ Directly from GitHub:
98
+
99
+ ```bash
100
+ pipx install "git+https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter.ms-split-router.git"
101
+ ```
102
+
103
+ Both install the `ms-split-router` command. `python -m ms_split_router` works too.
104
+
105
+ For a version-pinned deployment once both releases exist on PyPI (pipx >= 1.5):
106
+
107
+ ```bash
108
+ sudo pipx install --global "ms-cloud-endpoints==0.1.0"
109
+ sudo pipx install --global "ms-split-router==0.1.0"
110
+ ms-cloud-endpoints --version
111
+ ms-split-router --version
112
+ ```
113
+
114
+ The collector is a separate executable, not an automatically installed Python dependency. Install both for
115
+ live fetching; the router alone can read an existing data file. The annotated example is bundled in the installed
116
+ package: print it with `ms-split-router --print-example-config`. For older pipx and explicit upgrade/rollback
117
+ commands, see the parent's
118
+ [pinned deployment guide](https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter/blob/main/USAGE.md#version-pinned-deployment-from-pypi).
119
+ Avoid `pipx upgrade`/`upgrade-all` for pinned deployments; application pins do not lock transitive dependencies.
120
+
121
+ ## Quick start
122
+
123
+ ```bash
124
+ # 1. Find the split adapter: name, MAC, altnames, addresses and default gateways of every link
125
+ ms-split-router --show-links
126
+
127
+ # 2. Print the bundled example, then set the split adapter's MAC and DNS servers
128
+ ms-split-router --print-example-config > split-router.yaml
129
+ $EDITOR split-router.yaml
130
+
131
+ # 3. Preview: print every file to stdout, or render into a directory tree
132
+ ms-split-router -c split-router.yaml
133
+ ms-split-router -c split-router.yaml -o output/
134
+
135
+ # 4. Install into /etc, remove stale generated files, and reload what changed
136
+ sudo ms-split-router -c split-router.yaml --install --prune --reload
137
+
138
+ # Later, from cron or a timer: refresh only when the data or gateway changed
139
+ sudo ms-split-router -c split-router.yaml --fetch --install --prune --reload
140
+ ```
141
+
142
+ Use `--diff` to see the changes against `/` (or `--root`) first. `--check` exits with code 4 when the installed
143
+ files are out of date.
144
+
145
+ `--print-example-config` works without a configuration file, collector, root access or network connection.
146
+ It prints and exits without inspecting links or installing/reloading anything. Shell redirection (`>`) replaces
147
+ an existing file, so create the example only on first setup or redirect it to a new filename.
148
+
149
+ ## Configuration
150
+
151
+ A YAML file. The [bundled example](src/ms_split_router/example_config.yaml) lists every key with its default.
152
+ It defaults to fetching live data; uncomment `data.file` to use an existing document. The full reference
153
+ with explanations is in [docs/configuration.md](https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter/blob/main/docs/configuration.md#generator-configuration-ms-split-router).
154
+
155
+ ```yaml
156
+ data:
157
+ file: output/ms-cloud-endpoints.json # omit to run ms-cloud-endpoints instead
158
+
159
+ split_links:
160
+ - id: corp-vpn
161
+ match: {mac: "04:92:26:8a:8f:98"} # or {altname: msvpn} / {name: eth1}
162
+ altname_link: {name: msvpn} # optional .link file
163
+ dns: [10.10.0.4, 10.10.0.5]
164
+ routes:
165
+ azure: {service_tags: [AzureCloud]}
166
+ m365: {service_areas: [Common, Exchange, SharePoint, Skype]}
167
+ extra_prefixes: [10.20.0.0/16] # e.g. the vNet that holds your Private Endpoints
168
+
169
+ default_links:
170
+ - id: internet
171
+ match: {name: eth0}
172
+ dns: [1.1.1.1, 1.0.0.1]
173
+ ```
174
+
175
+ Errors name the offending key, for example `split_links[0].routes.m365.service_areas[0]: unknown value 'Teams'`.
176
+
177
+ ## Options
178
+
179
+ | Option | Default | Meaning |
180
+ |--------|---------|---------|
181
+ | `-c`, `--config FILE` | required for generation | Generator configuration |
182
+ | `--data FILE` | `data.file` | Collector JSON/YAML, overriding `data.file` |
183
+ | `--fetch` | off | Run `ms-cloud-endpoints` even when `data.file` is set |
184
+ | `--allow-partial` | off | Continue when the collector reports failed sources |
185
+ | `--ipv6`, `--no-ipv6` | `routes.ipv6` | Override `routes.ipv6` on every split link. `--ipv6` fails if a link has no IPv6 gateway or the data was collected with `--no-ipv6`. `--no-ipv6` also fetches without IPv6 |
186
+ | `-o`, `--output DIR` | | Write the files as a tree under `DIR` (`DIR/etc/systemd/...`) |
187
+ | `--install` | off | Write the files under `--root` |
188
+ | `--root DIR` | `/` | Target root for `--install`, `--diff` and `--check` |
189
+ | `--prune` | off | Also remove generated files (marker header) that are no longer rendered |
190
+ | `--reload` | off | After `--install` to `/`, run the reload commands for what changed (see below) |
191
+ | `--diff` | off | Print a unified diff against the target |
192
+ | `--check` | off | Write nothing. Exit 4 if the target is out of date |
193
+ | `--show-links` | | List links (name, state, MAC, altnames, addresses, gateways) and exit |
194
+ | `--print-example-config` | | Print the bundled annotated YAML and exit; mutually exclusive with `--show-links` |
195
+ | `-v`, `-vv` | warnings | Info (per-link route and domain counts, excluded domains) or debug logging on stderr |
196
+ | `--version` | | Print the version |
197
+
198
+ Without `-o`, `--install`, `--diff` or `--check`, the files are printed to stdout.
199
+
200
+ Only changed files are written, atomically (temporary file + rename, mode `0644`). `--reload` runs:
201
+
202
+ | Changed | Command(s) |
203
+ |---------|------------|
204
+ | `.link` | `udevadm control --reload`, `udevadm trigger --action=add --subsystem-match=net --attr-match=address=<mac>`, `udevadm settle` |
205
+ | `networkd.conf.d` | `systemctl restart systemd-networkd` |
206
+ | `.network` / drop-ins (only) | `networkctl reload` |
207
+ | `resolved.conf.d` | `systemctl try-reload-or-restart systemd-resolved` |
208
+
209
+ ### Exit codes
210
+
211
+ | Code | Meaning |
212
+ |------|---------|
213
+ | `0` | Success (or `--check`: up to date) |
214
+ | `1` | Error: data, discovery, rendering, writing or a reload command failed |
215
+ | `2` | Usage or configuration error |
216
+ | `3` | The collector reported failed sources (use `--allow-partial` to continue). Nothing was written |
217
+ | `4` | `--check`: the target is out of date |
218
+
219
+ ## How the output is built
220
+
221
+ **Routes.** For each split link the selected prefixes are gathered, widened to `widen_to` (IPv6 `/48` by default),
222
+ reduced by `exclude_prefixes`, and collapsed. Each prefix becomes one `[Route]` with the link's gateway, plus
223
+ `GatewayOnLink=yes` when the detected default route is on-link (as it is on WSL mirrored links). Generation fails
224
+ if a link would exceed **8192 routes**, because systemd-networkd silently ignores `[Route]` sections beyond that per
225
+ `.network` file. With the defaults, `AzureCloud` plus Microsoft 365 renders roughly 1,000 IPv4 and 400 IPv6 routes.
226
+
227
+ **Domains.** From the selected Microsoft 365 endpoint sets (the link's `routes.m365` filters, or all sets if
228
+ `routes.m365` is off), the Private Endpoint zones (`privatelink.` stripped, plus their public forwarders) and,
229
+ optionally, the "other endpoints" page:
230
+
231
+ 1. Wildcards and `{placeholder}` labels are cut to the suffix after the last wildcard (`*.blob.core.windows.net` →
232
+ `blob.core.windows.net`).
233
+ 2. With `collapse_to_root` (default) every name is reduced to its root domain (`windows.net`). Single-label results
234
+ are dropped.
235
+ 3. With `exclude_third_party` (default) CA/OCSP/CDN domains are dropped. `-v` lists them.
236
+ 4. `extra` is added, `exclude` removed, and names already covered by a shorter domain are dropped.
237
+
238
+ They are written as routing-only domains (`~office.com`) across several `Domains=` lines. Without `dns` servers
239
+ the DNS layer is skipped (with a warning), because the domains would have nowhere to go.
240
+
241
+ **Gateways.** `gateway4`/`gateway6: auto` takes the lowest-metric default route of the matched link at render time.
242
+ If no IPv4 default route exists, generation fails unless `gateway4` is set explicitly or to `none`. `onlink: auto`
243
+ copies the `onlink` flag of the detected route when it uses the same gateway.
244
+
245
+ **IPv6.** `routes.ipv6: auto` (default) writes IPv6 routes only when the link has an IPv6 gateway. `true` (or
246
+ `--ipv6`) makes them mandatory: generation fails when the link has no IPv6 gateway, or when the data was collected
247
+ with `ms-cloud-endpoints --no-ipv6` (recorded in `metadata.options.ipv6`). `false` (or `--no-ipv6`) leaves them out.
248
+ When the data is fetched and no split link wants IPv6 routes, the collector runs with `--no-ipv6`.
249
+
250
+ ## Notes and limitations
251
+
252
+ - **Render-time detection.** Routes carry a fixed gateway. Re-run the generator (for example with `--check` or from a
253
+ timer) when the Windows adapter's gateway changes.
254
+ - **First match wins.** networkd uses the first `.network` file, in lexical order, whose `[Match]` fits. Your own
255
+ `10-eth1.network` sorts before `10-ms-split-…`. Either remove it, or use `style: dropin` with `base:` set to it.
256
+ - **`.link` files** only apply on udev "add" events. `--reload` triggers one, but the most reliable way is
257
+ `wsl --shutdown`. The generated file keeps the naming and alternative-name policies of systemd's `99-default.link`,
258
+ which it supersedes for that device, and sets `MACAddressPolicy=none` so the mirrored MAC is never changed.
259
+ - **Third-party denylist.** It is built in (`domains.THIRD_PARTY_DOMAINS`). To route one of those domains anyway,
260
+ set `exclude_third_party: false` or add it to `extra`.
261
+ - **Root-domain collapsing** routes whole Microsoft domains (for example `~live.com`, `~bing.com`) to the split
262
+ link. Set `collapse_to_root: false` for the exact suffixes instead (longer list).
263
+ - **Validated with systemd 259.** The generated files were loaded by systemd-networkd 259 against a link set up
264
+ like a WSL mirrored link: all routes installed, foreign addresses and routes kept, DNS and domains handed to
265
+ resolved.
266
+
267
+ ## Development
268
+
269
+ ```bash
270
+ python -m venv .venv && . .venv/bin/activate
271
+ pip install -e . --group dev # pip >= 25.1
272
+
273
+ pytest # offline: fixtures/collector.json, fake `ip -json` output, golden files
274
+ ruff check . && ruff format --check .
275
+ python -m build
276
+ ```
277
+
278
+ Golden files in `tests/fixtures/golden/` hold the exact expected output. Regenerate them deliberately when the
279
+ rendering changes and review the diff.
280
+
281
+ ### Releases
282
+
283
+ The existing workflow also runs on `v*` tag pushes. Stable tags such as `v0.1.0` must exactly match the version
284
+ in the built wheel (sourced from `src/ms_split_router/__init__.py`); prerelease or mismatched tags fail.
285
+ After lint, tests and build checks pass, the `pypi` environment gates PyPI Trusted Publishing. A subsequent
286
+ job creates a GitHub Release with the same wheel and source distribution. Pull requests, branch pushes and
287
+ manual runs never publish.
288
+
289
+ Before tagging, configure this repository's `pypi` GitHub environment to allow deployments only from `v*`
290
+ tags (optionally with required reviewers, where the GitHub plan supports them). Register a PyPI Trusted Publisher for project `ms-split-router`,
291
+ owner `Jayonics`, repository `Azure-O365-DNS-UDR-SplitRouter.ms-split-router`, workflow
292
+ `ms-split-router.yml`, environment `pypi`. No PyPI API token is required. Optionally restrict release-tag
293
+ creation and mutation with a tag ruleset.
294
+
295
+ Commit the version and changelog first, then release the reviewed commit:
296
+
297
+ ```bash
298
+ git tag -a v0.1.0 -m "Release 0.1.0"
299
+ git push origin main
300
+ git push origin v0.1.0
301
+ ```
302
+
303
+ Pushing the tag publishes automatically once checks pass (or after approval, if reviewers are required). Never move a published tag or reuse an uploaded PyPI
304
+ version. If GitHub release creation fails after PyPI succeeds, re-run only failed jobs, not the whole workflow.
305
+ See the parent's [release procedure](https://github.com/Jayonics/Azure-O365-DNS-UDR-SplitRouter/blob/main/USAGE.md#publishing-releases)
306
+ for the complete setup and coordinated deployment process.
307
+
308
+ ### Layout
309
+
310
+ ```
311
+ src/ms_split_router/
312
+ cli.py command line, exit codes
313
+ config.py YAML -> validated dataclasses
314
+ data.py collector document loading / ms-cloud-endpoints subprocess
315
+ select.py per-link prefix selection
316
+ domains.py routing-domain derivation, third-party denylist
317
+ discover.py `ip -json` link and gateway discovery
318
+ generate.py combines the above into rendered files, route-limit check
319
+ render.py INI rendering of every file type
320
+ install.py plan/diff/atomic write/prune/reload
321
+ netutil.py prefix and domain helpers
322
+ ```
323
+
324
+ ## License
325
+
326
+ [MIT](LICENSE) © Radux Technologies