feat(diag): recent problems and a redacted export bundle - #55
Open
Behnam-RK wants to merge 1 commit into
Open
Conversation
Two surfaces over the same idea: dezhban already knows what went wrong, and until now finding out meant knowing where a root-owned log lived and reading slog output by eye. internal/logread parses the daemon's own text-format log back into records. Parsing belongs in Go because that is where the format is written — a second parser in Swift would be a second thing to get wrong about slog's quoting, and could not be tested against the writer. It reads the rotated archives too: the interesting failure is often the one that pushed the file over its rotation threshold. A line it cannot parse is kept, not dropped; an unrecognised level sorts as INFO rather than being filtered away. internal/redact replaces network identifiers with STABLE placeholders, so the same server is the same token everywhere and "the rules pass ip-1 but the endpoint is ip-2" survives as a finding. Loopback, private, link-local and multicast addresses are kept — they identify nobody, and hiding them makes a ruleset unreadable. Hostname handling is an ALLOW-list, which is the whole safety property: an unanticipated name is redacted rather than leaked, where a deny-list would leak exactly the VPN provider this exists to hide. Disabled is a true pass-through through the same code path, so the full-fidelity case cannot drift down a less-tested route. `dezhban report` writes one zip and stops there. Nothing is transmitted, for the reason CLAUDE.md already denies `upgrade` its own firewall pass. A missing input is noted inside the bundle rather than failing the collection — a host that never ran dezhban has no state, and that must not be why a report cannot be collected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Behnam-RK
force-pushed
the
feat/diagnostics-firewall-rules
branch
from
August 22, 2026 07:11
b000516 to
0dd295f
Compare
Behnam-RK
force-pushed
the
feat/problems-and-export
branch
from
August 22, 2026 07:11
28e19f8 to
0115bc5
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
"I want to collect errors somehow" — two surfaces over the same idea. dezhban already knows what went wrong; until now finding out meant knowing where a root-owned log lived and reading slog output by eye.
Recent problems
Diagnostics gains a Recent problems section: warn-and-worse records from dezhban's own log, newest first, with the evidence dezhban logged beside each one.
Three states, kept deliberately distinct — "nothing logged as a warning or an error" is shown in green as the good answer it is, and never collapsed into "couldn't read the log". Collapsing them would make a healthy host look like a broken reader.
Backed by a new
dezhban logs(--level,--since,--limit,--json). No root: the log is0644by design, the same callstate.jsonmakes.Parsing lives in Go
internal/logreadparses slog's text format back into records. That format is written in this repo, so a second parser in Swift would be a second thing to get wrong about quoting —msg="rules missing, re-applied" repairs=2splits into a truncated message and two garbage attrs under a naivestrings.Split— and it could not be tested against the writer.Two properties it holds to:
Attrs are carried as ordered pairs all the way to the SwiftUI row — the order dezhban wrote them in reads as a sentence, and a dictionary anywhere on that path would shuffle it.
The bundle
dezhban report(and Diagnostics → Export…) writes one zip: config,state.json,learned.json,armed.json, the ruleset dezhban last applied,doctor's findings, what each posture would apply, and recent log records — plus a README explaining each.A file that is missing is noted inside the bundle rather than failing the collection. A host that never ran dezhban has no state, and that must not be the reason you cannot collect a report.
Nothing is sent anywhere. It is a local file; sharing it is the operator's decision. Not a gap to close later — this is a tool whose job is that traffic does not leave the machine, and CLAUDE.md already denies
dezhban upgradeits own firewall pass on the same reasoning.Redaction, and why it is shaped this way
internal/redactreplaces addresses and hostnames with stable placeholders, not[redacted]. The same address is the same token everywhere it appears, so "the rules passip-1but the endpoint isip-2" survives as a finding — flattening everything to one token would hide exactly the class of bug a bundle is collected to diagnose.Three decisions worth reviewing:
pass on lo0 to 127.0.0.1intopass on lo0 to ip-4, which hides that the rule is loopback.The bundle's README legend reports counts ("23 distinct IP addresses → ip-1 … ip-23"), never originals — it ships inside the bundle, so listing them would undo the whole exercise.
The opt-out is loud in three places: the checkbox label says what the bundle will contain rather than describing the mechanism, the CLI warns on stderr, and the bundle's own README says NONE under Redaction.
Verification
go build,go vet(plusGOOS=linuxandGOOS=windows),go test— pass.internal/logread: quoted messages with spaces stay one message, escaped quotes survive, list attrs both forms, unparseable lines are kept, unknown levels are not dropped, level filtering,--limitkeeps the most recent, archives read oldest-first, missing log is empty not an error,--since.internal/redact: a public address does not survive; the same address always gets the same placeholder; structural addresses and prefix lengths are kept; an unknown hostname is redacted while shipped providers and filenames are not; version strings are left alone; disabled is a pass-through; the legend never contains the originals; and a real pf ruleset loses every identifier while keepingutun4,lo0,port 51820andblock drop out all.swift test— 210 tests;LogRecordsTestscovers Go's fractional timestamps, attr ordering, and that two byte-identical records from a retry loop stay distinct rows.dezhban reportagainst this machine's real config and grepped the unpacked bundle for identifiers. Only10.0.0.0,169.254.0.0,172.16.0.0,192.168.0.0,224.0.0.0and239.0.0.0survived — the deliberately-kept ranges. No real endpoint, provider hostname, or exit IP appeared.The redaction check is also in docs/contribute/testing.md as an on-host step, because it is the one that matters: a redactor that misses a field is worse than none, since it advertises a safety it did not deliver.
🤖 Generated with Claude Code