Skip to content

feat: report what the node's sb script can do as sb:<cap> on hello and heartbeat - #31

Merged
lr00rl merged 2 commits into
integrationfrom
feat/report-sb-caps
Oct 3, 2026
Merged

lr00rl merged 2 commits into
integrationfrom
feat/report-sb-caps

Conversation

@lr00rl

@lr00rl lr00rl commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

Summary

lattice-server (integration 05c5176, PR #135) removes a deleted VPN user from an adopted line by name only on nodes that report the capability sb:user-del-by-name, and it learns script capabilities only from the agent's capability list. This branch makes the agent report them.

  • With -singbox-discover on, the agent runs sb --json caps (the same -singbox-bin binary and bounded, shell-free runner as sb --json list) once at startup, before hello, and then at most every 10 minutes with a 5 s timeout.
  • Each cap goes into the capabilities of the hello and metrics bodies as sb:<cap>. The server replaces its per-node set from both, so both carry it. The task poll header keeps only the agent's own capabilities.
  • Only names from a fixed allowlist pass, the seven that lr00rl/sing-box v1.24.3-alpha.8 lists: user-del-by-name, user-park, user-parked-list, user-open-proxy-guard, user-match-counts, user-socks-add, user-lock (the last only on nodes with flock). Nothing else a script prints can reach the server.
  • alpha.7 and older have no caps verb (the word falls through to change, which answers {"ok":false,...} with exit 1). That, a timeout, any other non-zero exit, or output that is not one {"ok":true,"caps":[...]} object means no sb: caps, one debug line, and an unchanged heartbeat. A normal log line appears only when the reported set changes.
  • Second commit: runBoundedCommand now sets cmd.WaitDelay (1 s). Before, a child of sb that kept stdout open (jq, sleep, a flock wait) held the call past its deadline for as long as the child lived. This also bounds the existing sb --json list call.

capabilitiesFor, reportedCapabilities and the heartbeat goroutine are untouched, so this merges cleanly next to feat/control-plane-witness. No version bump and no changelog entry; the release cut adds those.

A probe failure on an alpha.8 node drops the sb: caps until the next probe, up to 10 minutes. That fails closed: the server keeps the old removal path for that node.

Test plan

  • gofmt -l . empty, go vet ./... clean
  • go test ./...: 21 packages ok
  • sh scripts/check-release-workflow.sh and sh scripts/test-install-integrity.sh ok
  • alpha.8 fixture is byte-identical to the fork's cmd_json_caps jq output; parse, no-flock, trim and dedupe, oversized list
  • alpha.7 failure modes: exit 1 with ok=false, ok=false with exit 0, human error text, empty output, missing caps, two JSON objects, binary missing, timeout
  • real shell scripts through the production runner: alpha.8, alpha.7 (exit 1), exec sleep 30, and a sleep 30 child (30.1 s before the WaitDelay fix, about 1 s after)
  • allowlist drops unknown, prefixed, case-changed and comma-joined names; probe runs at startup and again only after 10 minutes; nothing runs without -singbox-discover; a heartbeat carries the prefixed caps and still withholds the durable capability while recovery is blocked
  • On a node running alpha.8: the server's agent capability set shows sb:user-del-by-name after the agent restarts

lr00rl added 2 commits October 3, 2026 05:21
…d heartbeat

The server now removes a deleted VPN user from an adopted line by name only
on nodes that report sb:user-del-by-name, and it learns that from nothing but
the agent's capability list. lr00rl/sing-box v1.24.3-alpha.8 answers
`sb --json caps` with the list, so with -singbox-discover on the agent runs it
at startup, before hello, and then at most every 10 minutes with a 5 s
timeout, and appends each cap as "sb:<cap>" to the capabilities in the hello
and metrics bodies, the two places the server reads them.

Only names from a fixed allowlist pass: the seven alpha.8 lists
(user-del-by-name, user-park, user-parked-list, user-open-proxy-guard,
user-match-counts, user-socks-add, user-lock). A broken or hostile script
cannot put an arbitrary string into the capability set the server trusts,
and a later script's new cap needs an agent release to reach the server.

An alpha.7 or older script has no caps verb; it routes the word to change,
which refuses it with ok=false and exit 1. That, a timeout, any other
non-zero exit, or output that is not one {"ok":true,"caps":[...]} object
means no sb caps: one debug line and an unchanged heartbeat. The agent logs
at normal level only when the reported set changes.

The probe lives next to the existing `sb --json list` runner in
singboxdiscover and uses the same bounded, shell-free command. The task poll
header keeps only the agent's own capabilities, since the lease reads nothing
else from it. capabilitiesFor and the heartbeat goroutine are unchanged; the
work loop hands the cached caps to the heartbeat through its config, as it
already does for the discovered stats API address.

Tested: go test ./... (21 packages ok), gofmt -l . and go vet ./... clean,
scripts/check-release-workflow.sh and scripts/test-install-integrity.sh ok.
The alpha.8 fixture matches the fork's jq expression byte for byte; real
shell scripts cover the alpha.7 exit 1 and a hung script killed at the
timeout.
Not-tested: against a real node running the fork.
runBoundedCommand ran sb under a context deadline, but the deadline kills only
sb itself. A child it started (jq, a sleep, a flock wait) inherits stdout, and
cmd.Run waits for that pipe to close, so the call returned when the child
exited rather than at the deadline. A script whose shell runs `sleep 8` under
a 500 ms timeout held the caller for 8 s.

Both the `sb --json list` discovery call and the new `sb --json caps` probe
run on the work loop, so either could hold hello and the loop for as long as
such a child lived. cmd.WaitDelay now closes the pipes one second after the
kill and returns. The orphaned child is left to finish on its own; the agent
no longer waits for it.

Tested: a script that runs `sleep 30` as a child under a 100 ms timeout took
30.1 s before this change and returns in about a second after it. go test
./... (21 packages ok), gofmt, go vet, check-release-workflow.sh and
test-install-integrity.sh ok.
@lr00rl
lr00rl merged commit f1111e6 into integration Oct 3, 2026
1 check passed
@lr00rl
lr00rl deleted the feat/report-sb-caps branch October 3, 2026 13:23
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.

1 participant