Skip to content

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

Open
myleshorton wants to merge 1 commit 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 1 commit 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.

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.
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 on lines +189 to +199
arr, ok := v.([]any)
if !ok {
return nil
}
out := make([]string, 0, len(arr))
for _, x := range arr {
if s, ok := x.(string); ok {
out = append(out, s)
}
}
return out
Comment thread peer/validate.go
Comment on lines +203 to +213
arr, ok := v.([]any)
if !ok {
return nil
}
out := make([]float64, 0, len(arr))
for _, x := range arr {
if f, ok := x.(float64); ok {
out = append(out, f)
}
}
return out
Comment thread peer/validate.go
Comment on lines +113 to +117
if action, _ := body["action"].(string); action != "reject" {
continue
}
for _, t := range asStringSlice(body["rule_set"]) {
rejectedTags[t] = true
Comment thread peer/validate.go
Comment on lines +146 to +155
if action, _ := body["action"].(string); action != "reject" {
continue
}
for _, cidr := range asStringSlice(body["ip_cidr"]) {
if cidr == rfc1918CanaryCIDR {
gotRFC1918 = true
}
}
for _, p := range asFloatSlice(body["port"]) {
if p == smtpCanaryPort {
Comment thread peer/peer.go
// would otherwise turn every peer in the field into one until the
// next deploy. The peer prefers failing to share over sharing
// unsafely. See validate.go for the exact checks.
if err := validateAbuseRules(regResp.ServerConfig); err != nil {
Comment thread peer/validate.go
Comment on lines +9 to +14
// abuseRuleSetTags is the canonical list of abuse rule_set tags that the
// peer launch_cfg MUST carry. Mirrors abuseTags in
// lantern-cloud/cmd/api/pcfg/samizdat.go. If samizdat.go grows or
// renames a tag, this list grows with it — the test in
// lantern-cloud asserts the server side; this list asserts the client
// side sees the same shape after registration.
Comment thread peer/validate.go
Comment on lines +36 to +37
// RFC1918 CIDRs, and abuse-prone ports. Those rules live in
// lantern-cloud/cmd/api/pcfg/samizdat.go.
Comment thread peer/validate.go
Comment on lines +49 to +50
// or that the URLs themselves are trustworthy; those are separate
// supply-chain concerns tracked in engineering#TODO.
Comment thread peer/peer.go
// server-side regression that silently shipped an open-proxy config
// would otherwise turn every peer in the field into one until the
// next deploy. The peer prefers failing to share over sharing
// unsafely. See validate.go for the exact checks.
Comment thread peer/validate_test.go
Comment on lines +12 to +13
// reject rules. Shared by peer_test.go's stubServer so the existing
// Start-path tests do not regress on the new check.
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