Skip to content

peer: refuse to start if launch_cfg lacks abuse-handling rules#484

Open
myleshorton wants to merge 2 commits into
fisk/peer-localbackendfrom
fisk/peer-validate-abuse-rules
Open

peer: refuse to start if launch_cfg lacks abuse-handling rules#484
myleshorton wants to merge 2 commits into
fisk/peer-localbackendfrom
fisk/peer-validate-abuse-rules

Conversation

@myleshorton
Copy link
Copy Markdown
Contributor

Summary

Defence-in-depth for Share My Connection abuse handling. Refuses to start the peer sing-box if the server-supplied launch_cfg is missing the expected abuse rule_set / reject rules.

Why this matters: Phase 1 SmC abuse handling lives entirely in the sing-box JSON that lantern-cloud sends back from /v1/peer/register (see pcfg/samizdat.goabuseRejectRules + abuseRemoteRuleSets + peerEgressBlockRules). The peer client currently hands that JSON straight to libbox after one VPN-bypass tweak. A regression in samizdat.go that ever shipped a launch_cfg without those rules would turn every newly-registered peer into an open residential proxy until someone noticed.

How the check works

sequenceDiagram
    autonumber
    participant LC as lantern-cloud<br/>pcfg/samizdat.go
    participant P  as peer.Client.Start<br/>radiance/peer/peer.go:187
    participant V  as validateAbuseRules<br/>radiance/peer/validate.go ✨
    participant LB as libbox.NewServiceWithContext

    P->>LC: POST /v1/peer/register
    LC-->>P: launch_cfg JSON (with abuse rules)
    P->>V: peer/peer.go:202<br/>validateAbuseRules(regResp.ServerConfig) ✨
    Note over V: parse route.rule_set + route.rules<br/>assert 4× abuse tags present in both<br/>assert RFC1918 + SMTP canary rejects
    rect rgba(180, 230, 200, 0.3)
        alt Happy path — rules intact
            V-->>P: nil
            P->>LB: build + start sing-box
        else Server-side regression — rules missing
            V-->>P: errors.Join(missing tags, missing canaries)
            Note over P: Start returns error,<br/>peer refuses to share
        end
    end
Loading

The check is purely structural and permissive about sing-box's JSON shape (it marshals "default" rules in both inlined and {"type":"default","default":{...}} forms — TestValidateAbuseRules_NestedDefaultForm covers both).

What it asserts

  1. Abuse rule_set tags present in route.rule_set: geosite-malware, geoip-malware, geosite-phishing, geosite-cryptominers. The canonical list mirrors abuseTags in lantern-cloud/cmd/api/pcfg/samizdat.go.
  2. Matching reject rule for each tag in route.rules. A rule_set without a reject rule is a no-op — sing-box downloads the list and never enforces it.
  3. Static reject canaries: one entry from each block in peerEgressBlockRules10.0.0.0/8 (RFC1918 canary) and port 25 (SMTP canary). Spot-check rather than full enumeration so legitimate additions in samizdat.go don't break this check.

What it does NOT cover

  • The .srs files at the rule_set URLs (could be corrupted/tampered)
  • The trustworthiness of the rule_set URLs themselves (currently Chocolate4U on GitHub raw — single point of trust, tracked separately)
  • Behaviour while the rule_set is being downloaded for the first time (sing-box fails open during that window — separate concern, see audit notes)
  • Volumetric / rate-limit abuse (explicitly deferred to engineering#3443)

These are flagged in the audit notes but are out of scope for a structural-validation PR.

Errors are joined, not first-failure

errors.Join means a thoroughly broken config (e.g. someone accidentally deleted half of samizdat.go's route block) surfaces every missing piece in one error report. Whoever is triaging "why won't my peer start?" doesn't have to fix-one-thing-find-the-next.

launch_cfg failed abuse-rule sanity check:
  route.rule_set is missing abuse tags: [geosite-malware geoip-malware]
  route.rules has no reject action for abuse tags: [geosite-malware geoip-malware]
  route.rules is missing static abuse blocks: [RFC1918 reject (canary 10.0.0.0/8) SMTP-port reject (canary :25)]

Keeping the validator in sync with the server

abuseRuleSetTags in this PR mirrors abuseTags in lantern-cloud/cmd/api/pcfg/samizdat.go. If samizdat.go grows or renames a tag, this list grows with it. A future test that exercises an actual lantern-cloud-generated launch_cfg through validateAbuseRules would close the drift gap entirely — left as follow-up since it spans two modules.

Test plan

  • go test ./peer/ -count=1 — full peer test suite green
  • go test ./peer/ -run TestValidateAbuseRules -v — 10/10 validator unit tests pass
    • happy path
    • nested-default JSON form
    • missing route block
    • missing abuse rule_set tag
    • missing abuse reject rule
    • missing RFC1918 canary
    • missing SMTP canary
    • non-reject action ignored (e.g. route instead of reject)
    • all errors joined when multiple checks fail
    • malformed JSON
  • Existing stubServer fixture updated to minimalValidLaunchCfg — no peer test regressions.
  • (Follow-up) Cross-module test: feed an actual pcfg.GenerateSamizdat output through validateAbuseRules to catch drift between the two files.

Copilot AI review requested due to automatic review settings May 16, 2026 03:40
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a peer-side safety gate that validates the server-supplied sing-box config before starting Share My Connection, so peers refuse configs missing abuse-blocking structure.

Changes:

  • Adds validateAbuseRules and unit tests for required abuse rule sets, reject rules, and static canaries.
  • Calls the validator during Client.Start before patching and starting sing-box.
  • Updates peer test fixtures to use a config that passes the new validation.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 10 comments.

File Description
peer/validate.go New structural validator for peer launch config abuse-blocking rules.
peer/validate_test.go Unit tests and shared minimal valid launch config fixture.
peer/peer.go Invokes abuse-rule validation during peer startup.
peer/peer_test.go Updates stub registration response to include valid abuse-rule config.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread peer/validate.go
Comment thread peer/validate.go
Comment thread peer/validate.go Outdated
Comment thread peer/validate.go Outdated
Comment thread peer/peer.go
Comment thread peer/validate.go Outdated
Comment thread peer/validate.go Outdated
Comment thread peer/validate.go Outdated
Comment thread peer/peer.go Outdated
Comment thread peer/validate_test.go Outdated
@myleshorton myleshorton force-pushed the fisk/peer-localbackend branch from 3b19ec0 to 8cb3713 Compare May 28, 2026 23:57
@myleshorton myleshorton force-pushed the fisk/peer-validate-abuse-rules branch from 6348606 to 6f95dc2 Compare May 29, 2026 01:58
@myleshorton myleshorton force-pushed the fisk/peer-localbackend branch from 8cb3713 to 5959bbe Compare May 29, 2026 19:51
@myleshorton myleshorton force-pushed the fisk/peer-validate-abuse-rules branch 3 times, most recently from 2351fa8 to 57e7cd4 Compare May 29, 2026 20:17
@myleshorton myleshorton force-pushed the fisk/peer-localbackend branch from be86ee9 to 7d616d6 Compare May 29, 2026 20:43
Phase 1 Share My Connection abuse-handling lives entirely in the
sing-box options that lantern-cloud sends back from
/v1/peer/register. The peer client trusts that JSON and hands it
straight to libbox. If a future regression in
lantern-cloud/cmd/api/pcfg/samizdat.go silently shipped a
launch_cfg without those rules, every newly-registered peer would
become an open residential proxy until someone noticed — and the
class of bug that triggers it is one missing function call in a
file most reviewers don't routinely audit.

validateAbuseRules adds defence-in-depth on the client side. After
Register returns, before BuildBoxService is called, parse the
JSON and assert:

  - route.rule_set declares all four abuse tags (geosite-malware,
    geoip-malware, geosite-phishing, geosite-cryptominers).
  - route.rules has a matching reject action for each tag —
    otherwise sing-box downloads the rule_set but never enforces it.
  - route.rules contains an RFC1918 reject (canary 10.0.0.0/8) and
    an SMTP-port reject (canary :25). One sentinel per static block
    in samizdat.go's peerEgressBlockRules, picked to detect "whole
    block was dropped" rather than fail on legitimate additions.

The check is structural-only and permissive about JSON shape (sing-
box marshals "default" rules in both inlined and nested forms;
TestValidateAbuseRules_NestedDefaultForm asserts both work). It
does NOT verify the .srs files at the rule_set URLs or that the
URLs themselves are trustworthy — those are separate supply-chain
concerns to track.

errors.Join means a thoroughly-broken config surfaces every missing
piece in one report so the deployer triaging "why won't my peer
start?" doesn't have to fix-one-thing-find-the-next.

Existing peer_test.go uses a minimal `{"inbounds":[…]}` fixture
that would now fail the check. Migrated it to minimalValidLaunchCfg
(shared with validate_test.go) — same shape as a real samizdat
launch_cfg as far as the routing layer is concerned.
@myleshorton myleshorton force-pushed the fisk/peer-validate-abuse-rules branch from 57e7cd4 to cdd286d Compare May 29, 2026 20:44
Ten findings: 5 substantive validator bugs, 5 doc-lint violations.

Substantive:

1. asStringSlice / asFloatSlice now accept scalar OR array. sing-box
   route rules encode single-element matches as a scalar (e.g.
   '"rule_set": "sr-direct"' or '"port": 25'), and treating only
   the array form as valid would false-positive a launch_cfg that
   uses the scalar form. Mirrors how sing-box itself decodes these
   fields.

2. Same for asFloatSlice — scalar number support.

3. New isUnconditionalReject helper. The old check counted any reject
   rule that mentioned an abuse tag, which would let through:
   - {action:reject, rule_set:[tag], invert:true} — inverted match
     rejects EVERYTHING EXCEPT the tag, not the tag.
   - {action:reject, rule_set:[tag], port:80} — narrows the reject
     to port-80 traffic only; abuse traffic on other ports passes.
   isUnconditionalReject(body, matchKey) requires action=reject,
   invert!=true, and no body keys outside {action, invert, matchKey}.
   Explicit invert=false is allowed (treated as the canonical no-op).

4. Same predicate-narrowing concern applies to the static-canary
   reject rules (RFC1918, SMTP). Updated validateStaticRejectCanaries
   to check each canary against isUnconditionalReject scoped to its
   own match field (ip_cidr or port).

5. New TestClient_Start_AbuseRuleValidationFailureUnwinds integration
   test mirrors the existing *FailureUnwinds tests: stub a launch_cfg
   without a route block, assert Start returns 'abuse-rule sanity
   check' error, c.IsActive() is false, the port forward is unmapped,
   the box was never started, and the route was deregistered.

Doc-lint (AGENTS.md:13-17 forbids code-location and ticket refs):

6. validate.go:14 — dropped the explicit reference to lantern-cloud's
   server-side file; replaced with 'the server-side abuseTags list'.

7. validate.go:37 — same; 'the server-side static peer-egress-block
   list' replaces the path reference.

8. validate.go:50 — dropped 'engineering#TODO' placeholder. The
   supply-chain concern is real but unactionable as written; replaced
   with 'separate supply-chain concerns and are not in scope for this
   gate.'

9. peer.go:215 — dropped 'See validate.go for the exact checks';
   replaced with 'the validator's docstring enumerates the exact
   rule shapes it requires.'

10. validate_test.go:13 — dropped peer_test.go reference; reworded as
    'the stub server used in Start-path tests'.

Six new tests added:
- TestValidateAbuseRules_AcceptsScalarRuleSet — scalar matches valid
- TestValidateAbuseRules_RejectsInverted — invert=true not credited
- TestValidateAbuseRules_RejectsExtraConstraint — narrowed reject not credited
- TestValidateAbuseRules_RejectsStaticCanaryWithExtraConstraint — same for canaries
- TestValidateAbuseRules_AcceptsExplicitInvertFalse — explicit false credited
- TestClient_Start_AbuseRuleValidationFailureUnwinds — Start-path unwind coverage

All tests pass under -race -count=1.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants