Fix subnets validation
This commit is contained in:
@@ -3279,9 +3279,9 @@ Real IPv6 remains separately unverified by the previously accepted test scope.
|
||||
|
||||
### Milestone 10 — Visible styled keyboard quick reference in bottom panel
|
||||
|
||||
Status: in_progress (2026-10-26T20:52:01Z). The user retired M9 and explicitly
|
||||
Status: accepted (2026-10-26T21:07:24Z). The user retired M9 and explicitly
|
||||
directed development to continue. M1–M8 are accepted; M9 is no longer a
|
||||
prerequisite. M10 implementation, review, and validation are starting now.
|
||||
prerequisite. M10's scoped TUI checks and independent review passed.
|
||||
|
||||
#### Outcome and scope
|
||||
|
||||
@@ -3301,10 +3301,10 @@ paragraph cannot meet the requirement.
|
||||
|
||||
| Task | Owner | Scope and expected result | State |
|
||||
| --- | --- | --- | --- |
|
||||
| M10.1 | implementer | Inspect `src/tui/render.rs`, `state.rs`, `forms.rs` and `terminal.rs`; derive hints from actual supported keys and screen/form state. Implement the bottom panel with sufficient interior height, styled key/action groups and width-aware packing. | needs_changes: initial 7 render tests passed; independent R1/R2 found compact/form key-hint mismatches; targeted repair pending |
|
||||
| M10.2 | implementer | Cover resize, narrow/minimum terminals, list/details, forms, progress and QR disclosure with synthetic render/navigation checks. Update README shortcut guidance if needed. | pending |
|
||||
| M10.3 | reviewer / repairer | Independent review of actual layout, hints versus handlers, readability, clipping and state correctness; repair and re-review actionable findings. | pending |
|
||||
| M10.4 | coordinator | Run integrated gates, record source/artifact identity and actual results, maintain this plan and make the scoped acceptance decision. | pending |
|
||||
| M10.1 | implementer | Inspect `src/tui/render.rs`, `state.rs`, `forms.rs` and `terminal.rs`; derive hints from actual supported keys and screen/form state. Implement the bottom panel with sufficient interior height, styled key/action groups and width-aware packing. | complete for task scope: R1/R2 repaired and independently re-reviewed; render 9/9 and forms 7/7 focused tests passed on integrated HEAD |
|
||||
| M10.2 | implementer | Cover resize, narrow/minimum terminals, list/details, forms, progress and QR disclosure with synthetic render/navigation checks. Update README shortcut guidance if needed. | complete: render 13/13, TUI workflow 6/6 including PTY smoke; integrated review closed |
|
||||
| M10.3 | reviewer / repairer | Independent review of actual layout, hints versus handlers, readability, clipping and state correctness; repair and re-review actionable findings. | complete: R1 Clippy and R2 README precision repaired and independently re-reviewed with no remaining finding |
|
||||
| M10.4 | coordinator | Run integrated gates, record source/artifact identity and actual results, maintain this plan and make the scoped acceptance decision. | complete: integrated gates passed; source fingerprints and results in [M10 evidence](docs/evidence/milestone-10.md) |
|
||||
|
||||
List/details quick reference must include navigation (`Up/Down`, `Enter`, `Esc`
|
||||
as applicable), `?` Help and `q` Exit, plus the implemented management keys:
|
||||
@@ -3326,19 +3326,19 @@ Keep Help as the complete reference and preserve all existing operation semantic
|
||||
|
||||
#### Acceptance and validation
|
||||
|
||||
- [ ] M10-A1: the lowest bordered panel has visible interior text; at 80x24 all
|
||||
- [x] M10-A1: the lowest bordered panel has visible interior text; at 80x24 all
|
||||
applicable list actions are displayed in readable packed rows.
|
||||
- [ ] M10-A2: key labels are visually distinct with bold/color styling; action
|
||||
- [x] M10-A2: key labels are visually distinct with bold/color styling; action
|
||||
text is readable, and meaning remains understandable without color.
|
||||
- [ ] M10-A3: hints match actual key handlers, uppercase/lowercase and active
|
||||
- [x] M10-A3: hints match actual key handlers, uppercase/lowercase and active
|
||||
screen/form prerequisites; no hint implies a disabled operation can run.
|
||||
- [ ] M10-A4: resizing through 120x30, 80x24, 40x8 and the supported minimum
|
||||
- [x] M10-A4: resizing through 120x30, 80x24, 40x8 and the supported minimum
|
||||
34x8 causes no panic, overlapping borders or invisible footer text; compact
|
||||
layouts retain navigation/Help and indicate omitted commands. Below-minimum
|
||||
sizes retain a readable resize message.
|
||||
- [ ] M10-A5: list/detail navigation, forms, progress and QR remain usable;
|
||||
- [x] M10-A5: list/detail navigation, forms, progress and QR remain usable;
|
||||
footer rendering exposes no secrets and performs no application mutations.
|
||||
- [ ] M10-A6: focused synthetic rendering checks and all required Rust gates
|
||||
- [x] M10-A6: focused synthetic rendering checks and all required Rust gates
|
||||
pass; independent review closes actionable findings with recorded evidence.
|
||||
|
||||
Planned validation: `cargo test --locked tui::`,
|
||||
@@ -3346,37 +3346,131 @@ Planned validation: `cargo test --locked tui::`,
|
||||
`cargo clippy --locked --all-targets -- -D warnings`, `cargo test --locked`,
|
||||
`cargo build --locked --release` and `git diff --check`. Use synthetic fixtures
|
||||
and Ratatui test buffers; inspect styles as well as rendered text and geometry.
|
||||
No production or VM operations are needed for this presentation change. These
|
||||
checks have not been run for M10; no implementation or acceptance is claimed.
|
||||
No production or VM operations are needed for this presentation change. Actual
|
||||
integrated results and source identity are in [M10 evidence](docs/evidence/milestone-10.md).
|
||||
|
||||
#### Review, evidence and handoff
|
||||
|
||||
M10.1 initial implementation added visible styled footer rows and passed
|
||||
`cargo test --locked tui::render` (7/7), `cargo fmt --all -- --check`, and
|
||||
`git diff --check`. Independent review found **M10.1-R1 (P2, open)**: compact
|
||||
rows advertise list navigation/Help during forms, QR, and pending work;
|
||||
**M10.1-R2 (P2, open)**: generic form Enter is false for draft review and Tab
|
||||
is missing from tabbed forms. Targeted repair/re-review is next; no acceptance
|
||||
item is closed yet.
|
||||
`git diff --check`. Independent review found **M10.1-R1 (P2, closed)**: compact
|
||||
rows advertised list navigation/Help during forms, QR, and pending work; and
|
||||
**M10.1-R2 (P2, closed)**: generic form Enter was false for draft review and
|
||||
Tab was missing from tabbed forms. Targeted repair added secret-free form key
|
||||
capabilities and context-specific compact rows; independent re-review closed
|
||||
both findings on current integrated HEAD `e141c03`. Render tests passed 9/9,
|
||||
forms tests 7/7, format and diff checks passed. M10.2 coverage remains; no
|
||||
milestone acceptance is claimed yet.
|
||||
|
||||
The user removed M9 as
|
||||
a prerequisite without accepting its unchecked M9-A1–A9 criteria. The project
|
||||
configuration specifies GPT-6-Sol/medium for the coordinator and GPT-6-Luna/high
|
||||
for all four custom roles. Start with M10.1, one writer at a time. The
|
||||
coordinator owns this plan; record detailed evidence in
|
||||
`docs/evidence/milestone-10.md` only after actual checks exist. No new
|
||||
dependencies are planned.
|
||||
M10.2 added synthetic TestBackend coverage for 120×30, 80×24, 40×8, 34×8,
|
||||
below-minimum fallback, all applicable saved-client list hints, QR Esc/footer
|
||||
rendering, and key/action styles. README now describes contextual shortcuts.
|
||||
`cargo test --locked tui::render` passed 13/13 and
|
||||
`cargo test --locked --test tui_workflow` passed 6/6 including PTY smoke;
|
||||
`cargo fmt --all -- --check` and `git diff --check` passed. Integrated review
|
||||
subsequently closed its findings. No VM or production operations were run.
|
||||
|
||||
Fresh integrated review found **M10.3-R1 (P2, closed)**: Clippy failed on a
|
||||
collapsible `if`; repair preserved the action prerequisite and the gate passed.
|
||||
It also found **M10.3-R2 (P3, closed)**: README overstated narrow-screen Help
|
||||
availability; repair qualified the claim to list/details. Independent re-review
|
||||
closed both with no remaining actionable finding. A 34×8 QR assertion confirms
|
||||
Esc guidance and no fixture payload exposure; compact QR may be too large to
|
||||
display, but can be cleared. [M10 evidence](docs/evidence/milestone-10.md)
|
||||
records the integrated results and limitations.
|
||||
|
||||
The user removed M9 as a prerequisite without accepting its unchecked M9-A1–A9
|
||||
criteria. The project configuration specifies GPT-6-Sol/medium for the
|
||||
coordinator and GPT-6-Luna/high for all four custom roles. No new dependencies
|
||||
were added.
|
||||
|
||||
#### Outcome
|
||||
|
||||
Milestone in progress; no M10 acceptance is claimed. Next action: implement
|
||||
M10.1, then independent review/repair and M10.2–M10.4 checks. M9 stays retired.
|
||||
Milestone 10 accepted for scoped TUI presentation on the integrated source
|
||||
identified in [M10 evidence](docs/evidence/milestone-10.md). M9 stays retired
|
||||
and unaccepted. The user's subsequent AllowedIPs classification request is
|
||||
handled in M11 below.
|
||||
|
||||
### Milestone 11 — Valid subnet routes shown as a capability limitation
|
||||
|
||||
Status: accepted (2026-10-26T21:20:07Z). Prerequisite M10 accepted; M9 was
|
||||
retired by user direction and remains unaccepted. This is the subsequent
|
||||
user-requested behavior change. No VM work is in scope.
|
||||
|
||||
#### Outcome and scope
|
||||
|
||||
Keep a peer with valid extra subnet routes in the client list as `inspection
|
||||
only`, without treating those routes alone as a configuration anomaly or
|
||||
counting them on the issues screen. Details explain neutrally: “This peer
|
||||
contains subnet routes. Managing this configuration is not supported yet.”
|
||||
Malformed addresses, invalid keys, duplicate identities, document issues and
|
||||
other genuine errors remain visible. Preserve all source bytes and routes;
|
||||
do not expand the mutable client model, address allocation or restart
|
||||
verification, or remove safety checks.
|
||||
|
||||
#### Context and implementation steps
|
||||
|
||||
Read the parser, projection and TUI before editing. `ConfigDocument::parse`
|
||||
already distinguishes parse errors from valid `IpNet` values outside the
|
||||
client model. `Application::display_projection` retains all peer issues and
|
||||
marks projection failures inspection-only. The TUI currently treats every
|
||||
peer issue as an anomaly in list/details/`!` at the start of M11. Classify valid unsupported
|
||||
address shape separately at the presentation boundary, while retaining raw
|
||||
issues for edit/restart safety. A subnet-specific capability notice must be
|
||||
derived from parsed routes, not from parsing display strings. M2's historical
|
||||
“inspection issues” wording described visibility; this user-directed change
|
||||
refines their UI severity without rewriting M2's acceptance evidence.
|
||||
|
||||
| Task | Owner | Scope and expected result | State |
|
||||
| --- | --- | --- | --- |
|
||||
| M11.1 | implementer | Add explicit error-versus-capability classification and focused parser/projection/TUI regressions for valid subnet routes, normal client and genuine errors; preserve mutation guards. | complete: parser/projection/TUI regressions and safety preservation inspected; integrated review closed |
|
||||
| M11.2 | reviewer / repairer | Independently review behavior, safety boundaries and tests; repair and re-review actionable findings. | complete: R1 withdrawn after source-based re-evaluation; R2 repaired and independently re-reviewed with no remaining finding |
|
||||
| M11.3 | coordinator | Run focused and full Rust gates, record source/evidence and make acceptance decision. | complete: integrated gates passed; source fingerprints/results in [M11 evidence](docs/evidence/milestone-11.md) |
|
||||
|
||||
#### Acceptance and validation
|
||||
|
||||
- [x] M11-A1: a peer with `10.9.0.10/32, 192.168.88.0/24,
|
||||
192.168.8.0/24` is visible as `inspection only`, with original bytes/routes
|
||||
intact and the exact neutral details explanation.
|
||||
- [x] M11-A2: those valid subnet routes alone cause no `! anomaly`, error
|
||||
count or `UnsupportedAllowedIpShape` text in the list or issues screen.
|
||||
- [x] M11-A3: a normal supported peer remains mutable and unchanged; malformed
|
||||
addresses, invalid keys, duplicate identities and document issues remain
|
||||
visible as errors/ambiguities alongside any valid capability limitation.
|
||||
- [x] M11-A4: unsupported peers remain protected from TUI/application unsafe
|
||||
mutations; allocation and restart verification behavior are unchanged.
|
||||
- [x] M11-A5: focused regressions, independent review and all applicable Rust
|
||||
completion gates pass on the integrated result.
|
||||
|
||||
Planned checks: focused document, application, TUI and config-preservation
|
||||
tests; `cargo fmt --all -- --check`, `cargo clippy --locked --all-targets --
|
||||
-D warnings`, `cargo test --locked`, `cargo build --locked --release`, and
|
||||
`git diff --check`. Use synthetic fixtures only. No Alpine, VM or production
|
||||
checks are needed for this classification/presentation change.
|
||||
|
||||
#### Outcome
|
||||
|
||||
Accepted for the requested classification/presentation behavior. **M11-R1
|
||||
(withdrawn)**: review initially called a second
|
||||
distinct `/32` route a configuration error; reviewer re-evaluated against
|
||||
the WireGuard `wg(8)` format and confirmed it is another valid shape beyond
|
||||
the client model, with no actionable finding. **M11-R2 (coordinator, closed)**:
|
||||
without any subnet, valid additional host routes or IPv6-only shapes still
|
||||
appeared as anomalies. The repair classifies parsed-valid shape variants as
|
||||
capability limitations for display, retains raw issues for safety, and adds a
|
||||
generic detail notice for non-subnet cases. Independent re-review found no
|
||||
actionable issue. [M11 evidence](docs/evidence/milestone-11.md) records the
|
||||
source, focused checks and all Rust gates. An external workspace actor advanced
|
||||
HEAD to `18f28ee` while M11.1 was underway; the review covered committed and
|
||||
uncommitted M11 inputs. This team created no commit. No Alpine, VM or
|
||||
production behavior is claimed. Stop after this milestone.
|
||||
|
||||
## Current progress and resume point
|
||||
|
||||
- Bounded TUI maintenance (2026-10-26): repaired list arrows opening client details. Up/Down now only move the highlight; Enter opens details and Esc returns. Initial navigation follows the renderer's first-row highlight. Independent review closed NAV-001 with no remaining scoped findings; validation is recorded in [navigation repair evidence](docs/evidence/tui-navigation.md). This does not advance M9 or supply VM/Alpine acceptance evidence; prior artifact identities remain historical after this Rust change.
|
||||
- Milestones 1–8 are accepted; milestone 9 is retired by user direction and unaccepted. Its real systemd/D-Bus checks remain unverified and are not a prerequisite to M10.
|
||||
- Milestone 10 is in progress: implement the styled keyboard quick reference in the bottom bordered panel, then review and validate it.
|
||||
- Milestone 10 is accepted: the styled keyboard quick reference passed independent review, focused synthetic/PTY checks and Rust completion gates.
|
||||
- Milestone 11 is accepted for valid but unsupported AllowedIPs route presentation; focused and full Rust gates and independent re-review passed. Real systemd/D-Bus and IPv6 behavior remain unverified.
|
||||
- The approved dependency list is in [DEPENDENCIES.md](docs/DEPENDENCIES.md); container testing methodology is in [TESTING.md](docs/TESTING.md).
|
||||
- Milestone 1 is expanded and the Rust environment is pinned.
|
||||
- Milestone 2 delivered a lossless in-memory WireGuard configuration editor, versioned metadata, and behavioral preservation tests.
|
||||
@@ -3389,7 +3483,7 @@ M10.1, then independent review/repair and M10.2–M10.4 checks. M9 stays retired
|
||||
|
||||
- Milestone 8 delivered reviewed opt-in Alpine Testcontainers coverage for real IPv4 WireGuard peers, application lifecycle, saved/runtime separation, restoration, import/reissue, backup failures and the real Rustix reader. Host gates and three fresh real-container runs passed on one stable source fingerprint; M8-A1–A10 accepted. Real IPv6 behavior is unverified by user-authorized scope.
|
||||
- Milestone 9 is retired and unaccepted: the reviewed disposable VM preparation and guarded first start did not establish Debian/systemd acceptance. The exact VM was powered off after the user stopped work. M9.2–M9.3 were not started; M9-A1–A9 are unchecked.
|
||||
- Next action: execute M10.1. Do not restart the VM, access production, commit, or deploy.
|
||||
- Next action: stop after M11 and await user direction. Do not restart the VM, access production, commit, or deploy.
|
||||
|
||||
## Plan maintenance
|
||||
|
||||
|
||||
@@ -56,9 +56,16 @@ claim a completed systemd VM run or real IPv6 qualification.
|
||||
|
||||
## Terminal workflows
|
||||
|
||||
The client list supports arrow-key selection, Enter for details, `?` for help,
|
||||
and `q` to exit. Exit warns when a draft would be discarded; it never commits
|
||||
implicitly. From the list or details screen, the current shortcuts are:
|
||||
The bordered **Keys** panel at the bottom of the screen shows shortcuts for the
|
||||
current screen and form. On narrow list and details screens it prioritizes
|
||||
navigation and Help, and indicates when additional actions are omitted; resize
|
||||
the terminal to see more. Forms, QR disclosure and progress show only their
|
||||
applicable controls. Press `?` from the list or details screen to open Help,
|
||||
which contains the complete shortcut reference. The client list supports
|
||||
arrow-key selection, Enter for details, and `q` to exit. Exit warns when a draft
|
||||
would be discarded; it never commits implicitly. Uppercase shortcuts are
|
||||
distinct from lowercase shortcuts (`R` previews restart, while `r` renames;
|
||||
`B` creates a backup, while `b` lists backups):
|
||||
|
||||
| Key | Action |
|
||||
| --- | --- |
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# Milestone 10 acceptance evidence
|
||||
|
||||
Run: 2026-10-26T21:07:24Z, local development workspace. This report covers the
|
||||
styled TUI keyboard footer only. Milestone 9 remains retired and unaccepted.
|
||||
|
||||
## Source and environment
|
||||
|
||||
- HEAD: `e141c03c2c401b09ad0fa78557cb9403640ea80a`; working tree was dirty
|
||||
with `PLANS.md`, `README.md`, `src/delivery/qr.rs`, and `src/tui/render.rs`.
|
||||
The following SHA-256 values identify the relevant integrated inputs at the
|
||||
gates: `render.rs` `40e2abc891cc562a628eb661dcdcd3e052818c4332c25f3ae555e184d9adb65d`;
|
||||
`forms.rs` `8ecbd614c1ecc7f1ab4cbae9223f9820c5d8315c62a54cffbc6d5c2e640f6c31`;
|
||||
`qr.rs` `b8d30e6b24370fd1e3d666586481af74b12f279043cb1e0a7a1e61923588c4b8`;
|
||||
`README.md` `fa4ef797565895dbd78f920e92263df718720bdb84dba48da88e611020638741`;
|
||||
`Cargo.lock` `b8549df27f971dc550218cfc28e788f6630a1e13e7df538aded38e969ab713b6`.
|
||||
- `rustc 1.97.1 (8bab26f4f 2026-07-14)` and `cargo 1.97.1
|
||||
(c980f4866 2026-06-30)`. The release build used the locked local target;
|
||||
no container, VM, production service or VPN was accessed.
|
||||
|
||||
## Checks and scope
|
||||
|
||||
| Exact command | Result |
|
||||
| --- | --- |
|
||||
| `cargo test --locked tui::` | Exit 0; 38 TUI unit tests passed. |
|
||||
| `cargo test --locked --test tui_workflow` | Exit 0; 6 integration tests passed, including isolated PTY smoke. |
|
||||
| `cargo fmt --all -- --check` | Exit 0. |
|
||||
| `cargo clippy --locked --all-targets -- -D warnings` | Exit 0 after M10.3-R1 repair. |
|
||||
| `cargo test --locked` | Exit 0; 271 library units passed, 2 opt-in container drivers ignored; all integration suites and 2 compile-fail doctests passed. The Testcontainers target reported 25 passed and 1 ignored; no opt-in Alpine run occurred. |
|
||||
| `cargo build --locked --release` | Exit 0. |
|
||||
| `git diff --check` | Exit 0. |
|
||||
|
||||
The TestBackend checks cover 120×30, 80×24, 40×8, 34×8 and below-minimum
|
||||
rendering. They assert visible footer interior, styled key labels, applicable
|
||||
80×24 list actions, compact navigation/Help and overflow, context-specific
|
||||
forms/progress, and QR Esc guidance without fixture payload disclosure. These
|
||||
are synthetic buffer checks; no physical terminal QR scan or systemd behavior
|
||||
is claimed.
|
||||
|
||||
## Review and decision
|
||||
|
||||
Independent M10.1 review found R1/R2 context/form hint issues; repair and
|
||||
re-review closed both on the integrated implementation. A separate fresh
|
||||
integrated M10.3 review found R1 (Clippy failure) and R2 (README context
|
||||
overstatement). The repairer fixed both; the same independent reviewer
|
||||
re-reviewed the repaired state and reported no remaining actionable finding.
|
||||
The reviewer supported M10-A1–A5 within synthetic/PTY scope; the coordinator's
|
||||
full gates above satisfy M10-A6. Compact QR may show a too-large condition at
|
||||
34×8 while retaining Esc clearing guidance. M10 acceptance is scoped to TUI
|
||||
presentation and does not upgrade M9 systemd/IPv6 evidence.
|
||||
@@ -0,0 +1,62 @@
|
||||
# Milestone 11 acceptance evidence
|
||||
|
||||
Run: 2026-10-26T21:20:07Z, local development workspace. This report covers
|
||||
presentation of valid but unsupported peer `AllowedIPs` shapes. It does not
|
||||
establish runtime WireGuard, Alpine or systemd behavior.
|
||||
|
||||
## Source and environment
|
||||
|
||||
- HEAD `18f28eef02d6a9f94a1d34ad94e25079ebf125a2`; the working tree was
|
||||
dirty with `PLANS.md`, `src/app.rs`, `src/tui/render.rs`,
|
||||
`src/tui/terminal.rs`, and `tests/config_preservation.rs`. An external
|
||||
workspace actor created HEAD during implementation; this team created no
|
||||
commit. The integrated inputs at the gates have SHA-256 values:
|
||||
`app.rs` `75935d694d85cad11511ffbcff093cc7ebded08f02eb26589cb552fdbe1c7720`;
|
||||
`document.rs` `af4fed6c25fe4b801112d19fe4867ed809c6a05e68492c3a53cc4829ac3b766b`;
|
||||
`render.rs` `2f673febaba9e6f0ada1c274f33c0db02fe78fd2bca50310b1215d5a22dd8c9f`;
|
||||
`terminal.rs` `ed9a99932dddbaf508667c5f17886704bda749264c53cc60c7e29b889a56e074`;
|
||||
`config_preservation.rs` `0cdac39ac6ac370aaa85ea13610cb00f778a7efc559a39896212d96c6147f0b0`;
|
||||
`Cargo.lock` `b8549df27f971dc550218cfc28e788f6630a1e13e7df538aded38e969ab713b6`.
|
||||
- `rustc 1.97.1 (8bab26f4f 2026-07-14)`; `cargo 1.97.1
|
||||
(c980f4866 2026-06-30)`. Synthetic fixtures and Ratatui TestBackend only;
|
||||
no production, Docker, VM or live VPN action.
|
||||
|
||||
## Checks and scope
|
||||
|
||||
| Exact command | Result |
|
||||
| --- | --- |
|
||||
| `cargo test --locked valid_subnet_routes_are_classified_without_changing_source_or_mutation_safety` | Exit 0; focused config preservation test passed. |
|
||||
| `cargo test --locked tui::render::tests::client_rows_show_draft_state_and_escape_anomalous_unsupported_peers` | Exit 0; focused list/details test passed. |
|
||||
| `cargo test --locked --lib issues_screen_hides_valid_address_shape_limits_but_keeps_genuine_errors` | Exit 0; focused issue-screen test passed after repair. |
|
||||
| `cargo test --locked --test config_preservation` | Exit 0; 10 tests passed. |
|
||||
| `cargo fmt --all -- --check` | Exit 0. |
|
||||
| `cargo clippy --locked --all-targets -- -D warnings` | Exit 0. |
|
||||
| `cargo test --locked` | Exit 0; 272 library tests passed, 2 opt-in drivers ignored; integration and 2 compile-fail doctests passed. Testcontainers target reported 25 passed and 1 ignored without starting Alpine. |
|
||||
| `cargo build --locked --release` | Exit 0. |
|
||||
| `git diff --check` | Exit 0. |
|
||||
|
||||
The parser preserves the valid example's original source and all three routes.
|
||||
The application leaves its raw peer issues and unsupported projection intact;
|
||||
the TUI presents parsed-valid address-shape limitations separately from
|
||||
malformed input. The list and issue screen omit capability-only diagnostics;
|
||||
details show the exact subnet explanation. Tests also cover a normal supported
|
||||
client, extra host routes, IPv6-only shape, mixed malformed route, and existing
|
||||
broad-route deletion safety. Alias-only rename of an unprojectable peer retains
|
||||
its existing behavior and preserves routes. The TUI still blocks normal client
|
||||
mutations for unprojectable peers.
|
||||
|
||||
## Review and decision
|
||||
|
||||
Fresh independent review initially raised M11-R1, treating a second distinct
|
||||
`/32` host route as a configuration error. After checking the WireGuard
|
||||
[`wg(8)` AllowedIPs format](https://man7.org/linux/man-pages/man8/wg.8.html),
|
||||
the reviewer withdrew R1: a valid additional host route is another model
|
||||
capability limitation. The coordinator raised M11-R2 because non-subnet valid
|
||||
address shapes were still displayed as anomalies. A repairer classified the
|
||||
two parsed-valid shape variants consistently for presentation, kept all raw
|
||||
issues for safety, and added regressions. Independent re-review reported no
|
||||
remaining actionable finding. A1–A4 passed within source/fixture scope; the
|
||||
integrated gates above satisfy A5.
|
||||
|
||||
No real WireGuard or systemd behavior was exercised. Milestone 9 remains
|
||||
retired and unaccepted; real systemd/D-Bus and IPv6 capability are unverified.
|
||||
+20
@@ -46,10 +46,29 @@ pub struct ClientDisplay {
|
||||
pub identity: Option<PublicKey>,
|
||||
pub label: String,
|
||||
pub allowed_ips: Vec<String>,
|
||||
pub has_subnet_routes: bool,
|
||||
pub issues: Vec<crate::wireguard::PeerIssue>,
|
||||
pub mutable_identity: Option<PublicKey>,
|
||||
}
|
||||
|
||||
impl ClientDisplay {
|
||||
/// Whether a retained parser issue describes a valid address shape the
|
||||
/// client model cannot manage, rather than a malformed configuration.
|
||||
pub fn is_capability_limitation_issue(&self, issue: &crate::wireguard::PeerIssue) -> bool {
|
||||
matches!(
|
||||
issue,
|
||||
crate::wireguard::PeerIssue::UnsupportedAllowedIpShape { .. }
|
||||
| crate::wireguard::PeerIssue::MissingIpv4HostRoute
|
||||
)
|
||||
}
|
||||
|
||||
pub fn presented_issues(&self) -> impl Iterator<Item = &crate::wireguard::PeerIssue> {
|
||||
self.issues
|
||||
.iter()
|
||||
.filter(|issue| !self.is_capability_limitation_issue(issue))
|
||||
}
|
||||
}
|
||||
|
||||
/// Secret-free display projection of the current document and client state.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct SessionDisplay {
|
||||
@@ -553,6 +572,7 @@ impl Application {
|
||||
identity,
|
||||
label,
|
||||
allowed_ips: peer.allowed_ips.iter().map(ToString::to_string).collect(),
|
||||
has_subnet_routes: peer.has_subnet_routes,
|
||||
issues: peer.issues.clone(),
|
||||
mutable_identity: projected
|
||||
.filter(|client| self.states.draft().get(&client.identity).is_some())
|
||||
|
||||
@@ -84,6 +84,12 @@ fn encode_bytes(bytes: &[u8]) -> Result<QrCodeOutput, QrError> {
|
||||
Ok(QrCodeOutput { side, modules })
|
||||
}
|
||||
|
||||
/// Small, non-secret QR used only to exercise the terminal disclosure renderer.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn synthetic_test_output() -> QrCodeOutput {
|
||||
encode_bytes(b"synthetic QR disclosure fixture").expect("synthetic QR payload encodes")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
+229
-18
@@ -230,17 +230,16 @@ fn keyboard_hints(state: &UiState, forms: &FormController) -> Vec<Hint> {
|
||||
add("c", "Commit", HintKind::Action);
|
||||
add("x", "discard", HintKind::Destructive);
|
||||
add("R", "restart", HintKind::Destructive);
|
||||
if let Some(id) = state.selected_identity {
|
||||
if state
|
||||
if let Some(id) = state.selected_identity
|
||||
&& state
|
||||
.view
|
||||
.as_ref()
|
||||
.and_then(|view| view.client_states.get(&id))
|
||||
.is_some_and(|row| row.saved.is_some())
|
||||
&& selected
|
||||
{
|
||||
add("o", "first file", HintKind::Action);
|
||||
add("O", "password reissue", HintKind::Action);
|
||||
}
|
||||
&& selected
|
||||
{
|
||||
add("o", "first file", HintKind::Action);
|
||||
add("O", "password reissue", HintKind::Action);
|
||||
}
|
||||
add("b", "backups / restore", HintKind::Action);
|
||||
add("B", "backup", HintKind::Action);
|
||||
@@ -497,6 +496,7 @@ fn render_list(frame: &mut Frame<'_>, area: ratatui::layout::Rect, state: &UiSta
|
||||
.clients
|
||||
.iter()
|
||||
.map(|client| {
|
||||
let visible_issues: Vec<_> = client.presented_issues().collect();
|
||||
let identity = client
|
||||
.identity
|
||||
.map(|key| short_key(&key))
|
||||
@@ -520,15 +520,15 @@ fn render_list(frame: &mut Frame<'_>, area: ratatui::layout::Rect, state: &UiSta
|
||||
(None, None) => "State unavailable".to_owned(),
|
||||
},
|
||||
);
|
||||
let anomaly_marker = if client.issues.is_empty() {
|
||||
let anomaly_marker = if visible_issues.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
" ! anomaly".to_owned()
|
||||
};
|
||||
let anomaly_details = if client.issues.is_empty() {
|
||||
let anomaly_details = if visible_issues.is_empty() {
|
||||
String::new()
|
||||
} else {
|
||||
format!(" {}", safe_text(&format!("{:?}", client.issues)))
|
||||
format!(" {}", safe_text(&format!("{:?}", visible_issues)))
|
||||
};
|
||||
ListItem::new(Line::from(vec![
|
||||
Span::raw(format!("{row_state}{anomaly_marker} ")),
|
||||
@@ -652,6 +652,19 @@ fn detail_lines(client: &ClientDisplay, view: &SessionView) -> Vec<Line<'static>
|
||||
}
|
||||
)),
|
||||
];
|
||||
let has_capability_limitation = client
|
||||
.issues
|
||||
.iter()
|
||||
.any(|issue| client.is_capability_limitation_issue(issue));
|
||||
if client.has_subnet_routes {
|
||||
lines.push(Line::from(
|
||||
"This peer contains subnet routes. Managing this configuration is not supported yet.",
|
||||
));
|
||||
} else if has_capability_limitation {
|
||||
lines.push(Line::from(
|
||||
"This peer uses valid address routes that this client model cannot manage yet.",
|
||||
));
|
||||
}
|
||||
if let Some(identity) = client.identity {
|
||||
if let Some(row) = view.client_states.get(&identity) {
|
||||
lines.push(Line::from(format!(
|
||||
@@ -697,7 +710,7 @@ fn detail_lines(client: &ClientDisplay, view: &SessionView) -> Vec<Line<'static>
|
||||
}
|
||||
}
|
||||
lines.push(Line::from(format!("Applied state: {:?}", view.applied)));
|
||||
for issue in &client.issues {
|
||||
for issue in client.presented_issues() {
|
||||
lines.push(Line::from(format!(
|
||||
"Peer issue: {}",
|
||||
safe_text(&format!("{issue:?}"))
|
||||
@@ -833,6 +846,44 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn saved_client_view(identity: PublicKey) -> SessionView {
|
||||
let client = crate::domain::Client::new(
|
||||
identity,
|
||||
"synthetic client",
|
||||
crate::domain::ClientAddresses {
|
||||
ipv4: Ipv4Addr::new(10, 0, 0, 2),
|
||||
ipv6: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
SessionView {
|
||||
generation: 0,
|
||||
clients: vec![ClientDisplay {
|
||||
peer_index: 0,
|
||||
line: 1,
|
||||
identity: Some(identity),
|
||||
label: "synthetic client".to_owned(),
|
||||
allowed_ips: vec!["10.0.0.2/32".to_owned()],
|
||||
has_subnet_routes: false,
|
||||
issues: vec![],
|
||||
mutable_identity: Some(identity),
|
||||
}],
|
||||
client_states: BTreeMap::from([(
|
||||
identity,
|
||||
super::super::state::ClientStateRow {
|
||||
saved: Some(client.clone()),
|
||||
draft: Some(client),
|
||||
},
|
||||
)]),
|
||||
document_issues: Vec::new(),
|
||||
applied: super::super::state::AppliedState::Unknown,
|
||||
reserved_addresses: Default::default(),
|
||||
statistics: RuntimeObservation::unknown(),
|
||||
has_draft: false,
|
||||
vault_initialized: true,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bottom_keyboard_panel_has_visible_interior_and_styled_key_labels() {
|
||||
let state = UiState {
|
||||
@@ -869,6 +920,9 @@ mod tests {
|
||||
let style = buffer[key].style();
|
||||
assert!(style.add_modifier.contains(Modifier::BOLD));
|
||||
assert_eq!(style.fg, Some(Color::Green));
|
||||
let action = buffer[(key.0 + 1, key.1)].style();
|
||||
assert!(matches!(action.fg, None | Some(Color::Reset)));
|
||||
assert!(!action.add_modifier.contains(Modifier::BOLD));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -889,6 +943,111 @@ mod tests {
|
||||
assert!(text.lines().last().unwrap().contains('└'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requested_terminal_sizes_keep_borders_nonoverlapping_and_footer_inside_panel() {
|
||||
let state = UiState {
|
||||
view: Some(empty_view()),
|
||||
..UiState::default()
|
||||
};
|
||||
for (width, height) in [(120, 30), (80, 24), (40, 8), (34, 8)] {
|
||||
let mut terminal = Terminal::new(TestBackend::new(width, height)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| draw(frame, &state, &FormController::default()))
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
let text = rendered_text(&terminal);
|
||||
assert!(text.contains("Keys"), "missing footer at {width}x{height}");
|
||||
let bottom = height - 1;
|
||||
assert!(buffer[(0, bottom)].symbol().contains('└'));
|
||||
assert!(buffer[(width - 1, bottom)].symbol().contains('┘'));
|
||||
assert!(buffer[(1, bottom)].symbol().contains('─'));
|
||||
assert!(buffer[(0, bottom - 1)].symbol().contains('│'));
|
||||
assert!(
|
||||
text.lines()
|
||||
.any(|line| line.contains("select") || line.contains("new")),
|
||||
"footer has no visible interior hint at {width}x{height}: {text}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_at_80x24_shows_each_applicable_saved_client_action() {
|
||||
let identity = PublicKey::from_bytes([4; 32]);
|
||||
let state = UiState {
|
||||
selected_identity: Some(identity),
|
||||
view: Some(saved_client_view(identity)),
|
||||
..UiState::default()
|
||||
};
|
||||
let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| draw(frame, &state, &FormController::default()))
|
||||
.unwrap();
|
||||
let text = rendered_text(&terminal);
|
||||
for expected in [
|
||||
"↑/↓ select",
|
||||
"Enter details",
|
||||
"? Help",
|
||||
"q exit",
|
||||
"n new",
|
||||
"r rename",
|
||||
"d delete",
|
||||
"p preview",
|
||||
"c Commit",
|
||||
"x discard",
|
||||
"R restart",
|
||||
"o first file",
|
||||
"O password reissue",
|
||||
"b backups / restore",
|
||||
"B backup",
|
||||
"i import",
|
||||
"! issues",
|
||||
] {
|
||||
assert!(
|
||||
text.contains(expected),
|
||||
"missing {expected:?} in 80x24 render:\n{text}"
|
||||
);
|
||||
}
|
||||
assert!(text.contains("R restart") && text.contains("r rename"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn qr_disclosure_keeps_esc_clear_hint_and_never_renders_fixture_payload() {
|
||||
let mut forms = FormController::default();
|
||||
forms.open_disclosure(
|
||||
crate::delivery::qr::synthetic_test_output(),
|
||||
"Activation pending".to_owned(),
|
||||
);
|
||||
let mut terminal = Terminal::new(TestBackend::new(80, 24)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| draw(frame, &UiState::default(), &forms))
|
||||
.unwrap();
|
||||
let text = rendered_text(&terminal);
|
||||
assert!(text.contains("Secret disclosure"));
|
||||
assert!(text.contains("Esc clears QR") || text.contains("Esc clear QR"));
|
||||
assert!(text.contains("clear QR"));
|
||||
assert!(!text.contains("synthetic QR disclosure fixture"));
|
||||
|
||||
let mut compact = Terminal::new(TestBackend::new(34, 8)).unwrap();
|
||||
compact
|
||||
.draw(|frame| draw(frame, &UiState::default(), &forms))
|
||||
.unwrap();
|
||||
let compact_text = rendered_text(&compact);
|
||||
assert!(compact_text.contains("clear QR"));
|
||||
assert!(!compact_text.contains("synthetic QR disclosure fixture"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn below_minimum_terminal_shows_resize_message_without_drawing_footer() {
|
||||
let mut terminal = Terminal::new(TestBackend::new(33, 7)).unwrap();
|
||||
terminal
|
||||
.draw(|frame| draw(frame, &UiState::default(), &FormController::default()))
|
||||
.unwrap();
|
||||
let text = rendered_text(&terminal);
|
||||
assert!(text.contains("Terminal too small"));
|
||||
assert!(text.contains("34 × 8"));
|
||||
assert!(!text.contains("Keys"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compact_form_hints_follow_the_active_form_without_leaking_input() {
|
||||
let state = UiState::default();
|
||||
@@ -945,6 +1104,7 @@ mod tests {
|
||||
identity: Some(identity),
|
||||
label: "client".to_owned(),
|
||||
allowed_ips: vec![],
|
||||
has_subnet_routes: false,
|
||||
issues: vec![],
|
||||
mutable_identity: Some(identity),
|
||||
}],
|
||||
@@ -1040,6 +1200,7 @@ mod tests {
|
||||
identity: Some(identity),
|
||||
label: "new alias".to_owned(),
|
||||
allowed_ips: vec!["10.20.0.2/32".to_owned()],
|
||||
has_subnet_routes: false,
|
||||
issues: vec![],
|
||||
mutable_identity: Some(identity),
|
||||
},
|
||||
@@ -1049,10 +1210,14 @@ mod tests {
|
||||
identity: Some(unsupported_key),
|
||||
label: "odd\u{1b}[31m peer".to_owned(),
|
||||
allowed_ips: vec!["bad\nroute".to_owned()],
|
||||
issues: vec![crate::wireguard::PeerIssue::UnsupportedAllowedIpShape {
|
||||
field: 0,
|
||||
item: 1,
|
||||
}],
|
||||
has_subnet_routes: true,
|
||||
issues: vec![
|
||||
crate::wireguard::PeerIssue::UnsupportedAllowedIpShape {
|
||||
field: 0,
|
||||
item: 1,
|
||||
},
|
||||
crate::wireguard::PeerIssue::MalformedAllowedIp { field: 0, item: 2 },
|
||||
],
|
||||
mutable_identity: None,
|
||||
},
|
||||
],
|
||||
@@ -1079,7 +1244,7 @@ mod tests {
|
||||
has_draft: true,
|
||||
vault_initialized: true,
|
||||
};
|
||||
let state = UiState {
|
||||
let mut state = UiState {
|
||||
view: Some(view),
|
||||
..UiState::default()
|
||||
};
|
||||
@@ -1100,9 +1265,54 @@ mod tests {
|
||||
assert!(rendered.contains("inspection only"));
|
||||
assert!(rendered.contains("removed alias"));
|
||||
assert!(rendered.contains("Draft removed"));
|
||||
assert!(rendered.contains("UnsupportedAllowedIpShape"));
|
||||
assert!(rendered.contains("MalformedAllowedIp"));
|
||||
assert!(!rendered.contains("UnsupportedAllowedIpShape"));
|
||||
assert!(
|
||||
rendered.contains("! anomaly"),
|
||||
"mixed genuine error stays visible"
|
||||
);
|
||||
let subnet_only = ClientDisplay {
|
||||
has_subnet_routes: true,
|
||||
issues: vec![crate::wireguard::PeerIssue::UnsupportedAllowedIpShape {
|
||||
field: 0,
|
||||
item: 1,
|
||||
}],
|
||||
..state.view.as_ref().unwrap().clients[1].clone()
|
||||
};
|
||||
assert!(
|
||||
subnet_only.issues.len() == 1,
|
||||
"raw safety issue is retained"
|
||||
);
|
||||
assert!(subnet_only.presented_issues().next().is_none());
|
||||
assert!(detail_lines(&state.view.as_ref().unwrap().clients[1], state.view.as_ref().unwrap())
|
||||
.iter()
|
||||
.any(|line| line.to_string().contains(
|
||||
"This peer contains subnet routes. Managing this configuration is not supported yet."
|
||||
)));
|
||||
let additional_host = ClientDisplay {
|
||||
has_subnet_routes: false,
|
||||
issues: vec![crate::wireguard::PeerIssue::UnsupportedAllowedIpShape {
|
||||
field: 0,
|
||||
item: 1,
|
||||
}],
|
||||
..state.view.as_ref().unwrap().clients[1].clone()
|
||||
};
|
||||
let additional_host_details = detail_lines(&additional_host, state.view.as_ref().unwrap());
|
||||
assert!(
|
||||
additional_host_details
|
||||
.iter()
|
||||
.any(|line| line.to_string().contains(
|
||||
"This peer uses valid address routes that this client model cannot manage yet."
|
||||
))
|
||||
);
|
||||
assert!(!additional_host_details.iter().any(|line| {
|
||||
line.to_string()
|
||||
.contains("Peer issue: UnsupportedAllowedIpShape")
|
||||
}));
|
||||
assert!(rendered.contains("odd\\u{1b}[31m peer"));
|
||||
assert!(rendered.contains("bad\\nroute"));
|
||||
state.view.as_mut().unwrap().clients[1].issues =
|
||||
vec![crate::wireguard::PeerIssue::UnsupportedAllowedIpShape { field: 0, item: 1 }];
|
||||
|
||||
let mut narrow = Terminal::new(TestBackend::new(34, 12)).unwrap();
|
||||
narrow
|
||||
@@ -1118,7 +1328,8 @@ mod tests {
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
assert!(narrow_rendered.contains("Draft changed"));
|
||||
assert!(narrow_rendered.contains("! anomaly"));
|
||||
assert!(!narrow_rendered.contains("! anomaly"));
|
||||
assert!(!narrow_rendered.contains("UnsupportedAllowedIpShape"));
|
||||
assert!(narrow_rendered.contains("Draft removed"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,6 +315,7 @@ mod tests {
|
||||
identity: Some(identity),
|
||||
label: label.to_owned(),
|
||||
allowed_ips: vec!["10.0.0.2/32".to_owned()],
|
||||
has_subnet_routes: false,
|
||||
issues: if mutable {
|
||||
Vec::new()
|
||||
} else {
|
||||
|
||||
+55
-17
@@ -19,7 +19,7 @@ use super::events::{
|
||||
};
|
||||
use super::forms::{FormAction, FormController};
|
||||
use super::render;
|
||||
use super::state::{Screen, UiState};
|
||||
use super::state::{Screen, SessionView, UiState};
|
||||
use crate::app::Application;
|
||||
|
||||
const MIN_WIDTH: u16 = 34;
|
||||
@@ -765,22 +765,7 @@ fn handle_key(
|
||||
state.screen = Screen::Message;
|
||||
return;
|
||||
};
|
||||
let mut issues = Vec::new();
|
||||
issues.extend(
|
||||
view.document_issues
|
||||
.iter()
|
||||
.map(|issue| format!("Document issue: {issue:?}")),
|
||||
);
|
||||
for client in &view.clients {
|
||||
for issue in &client.issues {
|
||||
issues.push(format!("Peer at line {}: {issue:?}", client.line));
|
||||
}
|
||||
}
|
||||
state.status_message = Some(if issues.is_empty() {
|
||||
"No document or peer issues were reported.".to_owned()
|
||||
} else {
|
||||
issues.join("\n")
|
||||
});
|
||||
state.status_message = Some(issue_summary(view));
|
||||
state.screen = Screen::Message;
|
||||
state.scroll_offset = 0;
|
||||
}
|
||||
@@ -802,6 +787,25 @@ fn handle_key(
|
||||
}
|
||||
}
|
||||
|
||||
fn issue_summary(view: &SessionView) -> String {
|
||||
let mut issues = Vec::new();
|
||||
issues.extend(
|
||||
view.document_issues
|
||||
.iter()
|
||||
.map(|issue| format!("Document issue: {issue:?}")),
|
||||
);
|
||||
for client in &view.clients {
|
||||
for issue in client.presented_issues() {
|
||||
issues.push(format!("Peer at line {}: {issue:?}", client.line));
|
||||
}
|
||||
}
|
||||
if issues.is_empty() {
|
||||
"No document or peer issues were reported.".to_owned()
|
||||
} else {
|
||||
issues.join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle list/detail navigation and return whether the key was consumed.
|
||||
/// Keeping this path independent of worker actions makes its state transitions
|
||||
/// directly testable without starting an application worker.
|
||||
@@ -921,6 +925,7 @@ mod tests {
|
||||
identity: None,
|
||||
label: "unsupported peer".to_owned(),
|
||||
allowed_ips: Vec::new(),
|
||||
has_subnet_routes: false,
|
||||
issues: vec![PeerIssue::MissingIpv4HostRoute],
|
||||
mutable_identity: None,
|
||||
};
|
||||
@@ -933,6 +938,7 @@ mod tests {
|
||||
identity: Some(first),
|
||||
label: "first".to_owned(),
|
||||
allowed_ips: vec!["10.0.0.2/32".to_owned()],
|
||||
has_subnet_routes: false,
|
||||
issues: Vec::new(),
|
||||
mutable_identity: Some(first),
|
||||
},
|
||||
@@ -1007,6 +1013,38 @@ mod tests {
|
||||
assert!(!state.selected_client_mutable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn issues_screen_hides_valid_address_shape_limits_but_keeps_genuine_errors() {
|
||||
let mut view = navigation_view();
|
||||
view.clients.truncate(1);
|
||||
view.clients[0].issues = vec![PeerIssue::UnsupportedAllowedIpShape { field: 0, item: 1 }];
|
||||
view.clients[0].has_subnet_routes = false;
|
||||
let additional_host_only = issue_summary(&view);
|
||||
assert_eq!(
|
||||
additional_host_only,
|
||||
"No document or peer issues were reported."
|
||||
);
|
||||
assert_eq!(
|
||||
view.clients[0].issues,
|
||||
vec![PeerIssue::UnsupportedAllowedIpShape { field: 0, item: 1 }],
|
||||
"presentation must retain the parser issue for safety checks"
|
||||
);
|
||||
|
||||
view.clients[0].issues = vec![PeerIssue::MissingIpv4HostRoute];
|
||||
assert_eq!(
|
||||
issue_summary(&view),
|
||||
"No document or peer issues were reported.",
|
||||
"valid IPv6-only address shape is a capability limit"
|
||||
);
|
||||
|
||||
view.clients[0]
|
||||
.issues
|
||||
.push(PeerIssue::MalformedAllowedIp { field: 0, item: 2 });
|
||||
let mixed = issue_summary(&view);
|
||||
assert!(mixed.contains("MalformedAllowedIp"));
|
||||
assert!(!mixed.contains("UnsupportedAllowedIpShape"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restart_results_render_cancel_success_and_uncertain_failure_honestly() {
|
||||
use crate::app::{RestartFailureKind, RestartReport, RestartStage};
|
||||
|
||||
@@ -113,6 +113,10 @@ pub struct PeerInspection {
|
||||
pub public_key_fields: Vec<DirectiveSpan>,
|
||||
/// Parsed route values. Unparseable items are represented by `PeerIssue` entries.
|
||||
pub allowed_ips: Vec<IpNet>,
|
||||
/// True when a valid parsed route is a subnet rather than a host route.
|
||||
/// This is a capability limitation for the current client model, not a
|
||||
/// malformed configuration issue.
|
||||
pub has_subnet_routes: bool,
|
||||
pub allowed_ips_fields: Vec<DirectiveSpan>,
|
||||
/// Parsed alias and safe owned block span, or an explicit metadata issue.
|
||||
pub metadata: MetadataAttachment,
|
||||
@@ -346,11 +350,16 @@ impl ConfigDocument {
|
||||
let mut allowed_ips_fields = Vec::new();
|
||||
let mut has_ipv4_host_route = false;
|
||||
let mut has_ipv6_host_route = false;
|
||||
let mut has_subnet_routes = false;
|
||||
for (field, (directive, items)) in allowed_fields.into_iter().enumerate() {
|
||||
allowed_ips_fields.push(directive);
|
||||
for (item, result) in items {
|
||||
match result {
|
||||
Ok(net) => {
|
||||
has_subnet_routes |= match net {
|
||||
IpNet::V4(network) => network.prefix_len() < 32,
|
||||
IpNet::V6(network) => network.prefix_len() < 128,
|
||||
};
|
||||
let supported = match net {
|
||||
IpNet::V4(network) if network.prefix_len() == 32 => {
|
||||
let first = !has_ipv4_host_route;
|
||||
@@ -385,6 +394,7 @@ impl ConfigDocument {
|
||||
public_key_candidates: parsed_keys.into_iter().flatten().collect(),
|
||||
public_key_fields,
|
||||
allowed_ips,
|
||||
has_subnet_routes,
|
||||
allowed_ips_fields,
|
||||
metadata: MetadataAttachment::Absent,
|
||||
issues: peer_issues,
|
||||
|
||||
@@ -372,6 +372,45 @@ fn unsupported_routes_multiple_addresses_and_ipv6_only_round_trip_but_projection
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn valid_subnet_routes_are_classified_without_changing_source_or_mutation_safety() {
|
||||
let source = format!(
|
||||
"# wg-admin:begin\n# wg-admin:version=1\n# wg-admin:alias=Y2xpZW50\n# wg-admin:end\n[Peer]\nPublicKey = {KEY_A}\nAllowedIPs = 10.9.0.10/32, 192.168.88.0/24, 192.168.8.0/24\n"
|
||||
);
|
||||
let document = ConfigDocument::parse(&source).unwrap();
|
||||
let peer = &document.peers()[0];
|
||||
assert!(peer.has_subnet_routes);
|
||||
assert_eq!(peer.allowed_ips.len(), 3);
|
||||
assert!(peer.issues.iter().any(|issue| matches!(
|
||||
issue,
|
||||
PeerIssue::UnsupportedAllowedIpShape { field: 0, item: 1 }
|
||||
)));
|
||||
assert_eq!(document.render(), source.as_bytes());
|
||||
assert_eq!(
|
||||
document.project_client(&key(KEY_A)),
|
||||
Err(EditError::UnsupportedAddresses)
|
||||
);
|
||||
let renamed = document
|
||||
.rename_client(&key(KEY_A), "alias-only edit remains byte-preserving")
|
||||
.expect("safe alias-only edit remains supported");
|
||||
assert!(
|
||||
String::from_utf8_lossy(renamed.render())
|
||||
.contains("AllowedIPs = 10.9.0.10/32, 192.168.88.0/24, 192.168.8.0/24")
|
||||
);
|
||||
|
||||
let malformed = format!(
|
||||
"[Peer]\nPublicKey = {KEY_A}\nAllowedIPs = 10.9.0.10/32, 192.168.88.0/24, not-an-address\n"
|
||||
);
|
||||
let mixed = ConfigDocument::parse(&malformed).unwrap();
|
||||
assert!(mixed.peers()[0].has_subnet_routes);
|
||||
assert!(
|
||||
mixed.peers()[0]
|
||||
.issues
|
||||
.iter()
|
||||
.any(|issue| matches!(issue, PeerIssue::MalformedAllowedIp { .. }))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_errors_debug_and_failed_edits_never_expose_document_secrets() {
|
||||
let document = ConfigDocument::parse(FIXTURE).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user