Upstream defect: acme.go is behind with_acme but acme_logger.go was not,
so go.uber.org/zap linked into every build even with ACME disabled. Only
acme.go references ACMELogWriter/ACMEEncoderConfig, so the twin gate is
behaviour-preserving; a with_acme build still compiles.
Marked lx:acme_logger_gate; upstream-PR candidate (drop the lx block on
rebase once merged). -94 KB on the router shaterd link.
LogEntry.Device was always '' — but the client address IS on the DNS
resolution context. dnstrack.QueryEvent gains Client netip.Addr, populated at
all three dns/client_log.go emit sites from adapter.ContextFrom(ctx).Source.Addr
(same context processInfoFromContext already reads). stats deviceLabel: LAN
source (a.lanNets.isLAN) -> DHCP hostname or IP; loopback/non-LAN/unknown ->
'router' (the appliance's own urltest/sub/DoH lookups). Insights DNS log now
shows device -> domain · resolver · action (mirrors the Connections log), with
a dimmed 'router' chip for router-originated lookups; falls back to '—' on
older data.
Verified: root build (dns tree + box, router tags)/vet 0, go test ok
(deviceLabel: LAN+lease/LAN+IP/loopback->router), panel tsc/build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLthkP2S8WAfxu7fcYbPfE
In-process stats fed by the engine's DNS-query event stream + nft counters.
- engine: DNSQueryManager() accessor; engine.New pre-registers a stable
*dnstrack.Manager into e.ctx (box.New only creates one when an api/
clash_api observable is present, which the router config has none of, so
the manager would be nil — pre-registering keeps the DNS stream alive).
- shater/stats: Aggregator subscribes to dnstrack QueryEvents and maintains
bounded top-domains, allowed-vs-blocked (blocked = NXDOMAIN / 0.0.0.0 /
failed), a 60-min timeline, a 200-entry live query-log ring, per-server
counts; polls netplane.ListClients/ListCounters for per-device + per-rule
traffic (client IP -> DHCP hostname). Snapshot()/RecentQueries(); re-subs
on box swap; resilient when the engine is down.
- daemon: creates+starts the aggregator, Resubscribe() after each reconcile,
Close on SIGTERM; control-socket 'stats' verb returns the real snapshot.
- panel: GET /api/stats (snapshot) + GET /api/stats/log?n= (live log),
session-gated; Stats type + getStatsLog() in api.ts; Overview QueryLog now
polls the live log, plus a DNS-filtering module + blocked SegMeter + top-
blocked list. Honest empty states, no fabricated data.
- upstream (minimal, marked // lx/D15): dnstrack SourceFiltered +
emitFilteredResponse at the two DNS-filter predefined-block sites in
dns/router.go — filter blocks now feed the query stream (were invisible).
Verified: stats+panel unit tests; panel tsc+build; VM E2E — DNS traffic
from a netns client produced /api/stats totals (queries 16, blocked 6),
top_domains[blocked-ad.example blocked 6], per-device row, and /api/stats/log
rows with correct block/allow; stream survived a box swap (SIGHUP). Overview
screenshot shows the live query log + blocked stats.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LLthkP2S8WAfxu7fcYbPfE
Merges 14 upstream commits including L3-forwarding support (which bumped
wireguard-go v0.0.3->v0.0.5, already re-grafted in the prior commit),
snell protocol, bridge outbound, flow-tracking/sniff improvements, and
DNS/dialer fixes.
lx conflict resolutions:
- protocol/wireguard/endpoint.go: took upstream's new flow API
(PreMatchFlow/PortAddresses/PortMTU/AttachReturn/DetachReturn/JudgeFlow),
dropped our old PrepareConnection/NewDirectRouteConnection. SPEC 020
idle-suspend wake guard (resumeOnDial) moved to WritePackets — the single
point every L3-forwarded packet transits, incl. established flows that
bypass DialContext.
- adapter/outbound.go: kept lx IdleSuspendable/ReachabilityInvalidator,
restored 'time' import dropped by auto-merge.
- go.mod/go.sum + test/: took upstream dependency bumps (tailscale, sing,
sing-tun); wireguard-go stays v0.0.5 with local submodule replace.
Green: full sing-box CLI with LX_TAGS (Go 1.24.7), libbox, wireguard/
adapter/dns/daemon packages, transport+protocol/wireguard tests, AWG
config validation.
Two nits surfaced by the lx-vs-upstream cleanliness audit (no runtime impact):
- box.go: the dnstrack registration comment said "service.FromContext" — the
§180 dead-stream signature. The actual readers use PtrFromContext (pairs with
MustRegisterPtr). Fixed the comment + noted why FromContext[*T] returns nil,
so a future debugger doesn't "fix" the readers back into §180.
- common/dnstrack/manager.go: removed the unused SourceRejected constant —
rejected resolutions are folded into SourceFailed at the emit site, so
"rejected" never reaches the wire. Replaced with a comment to prevent re-adding
an unreachable client case.
Audit verdict: code clean — no concurrency/wire/behaviour issues; dns/client.go
byte-identical to upstream, emits additive and subscriber-gated.
LxBox feedback: DnsQuery lacked which DNS server / outbound channel the query went
through. A DNS rule selects a server (matchDNS by action.Server), not an outbound;
the channel is the server's own detour, fixed at config time. Add to DnsQueryEvent:
- dnsServer/dnsServerType = transport.Tag()/Type() (transport is the Exchange param,
so available on all emit paths incl. failures);
- outbound = the server's detour tag (TransportAdapter.OutboundTag() from
DialerOptions.Detour), with a selector expanded to its live node via Now()
server-side (like Connection.Detour), empty on cached/optimistic.
Also gate event construction on HasSubscribers(): with no profiler attached the DNS
hot path builds nothing (no event/answers/outbound lookup) — previously every
resolution built an event just to be dropped for lack of a listener. The Now()
resolution therefore never touches the hot path.
Wire: additive proto fields + OutboundTag() on DNSTransport (embedded adapter
satisfies it). libbox DnsQuery.DNSServer/DNSServerType/Outbound(). Changelog rc.10.
Hijacked DNS (the norm on an Android VPN) is answered before a connection becomes
a traffic tracker, so DNS queries never reach the connections stream — the only
egress was the text log, which carries no app attribution. Add common/dnstrack
(a Subscriber[QueryEvent] mirror of trafficcontrol) emitting one event per
resolution from dns/client.go, attributed via adapter.ContextFrom(ctx).ProcessInfo
(same ctx on cache-hit and miss, so cached queries are attributed too).
Failures are first-class: timeout/loopback/rejected-cached/SERVFAIL-reject emit
failed=true + error + rcode=-1 (no response) — without this the stream is blind to
DNS failures, the primary throttling signal. CNAME chains preserved: with
includeAnswers, each event carries the full response.Answer in wire order (CNAME
hops + final A/AAAA, not filtered to IPs).
Wire: rpc SubscribeDNSQueries(SubscribeDNSQueriesRequest) returns (stream
DnsQueryEvent) + DnsAnswer; event-driven server stream (no ticker); libbox
SubscribeDNSQueries(includeAnswers, handler). Tag-less core -> Unimplemented.
Detour/Chain and other streams unchanged.
Docs: SPECS/018, lx-changelog rc.7.
chain omits the final outbound's own detour by design (upstream loop only
unwinds OutboundGroup via Now() and breaks on the first non-group), so a node
detouring through e.g. WARP never shows in the routing chain. Add Detour
[]string to TrackerMetadata, unwound from the final outbound's Dependencies()
(= its detour for a non-group outbound), descending into groups via Now()
against the same atomic snapshot, with a seen-guard against cycles.
Wire: additive 'repeated string detourList = 23' on the Connection proto
message (hand-applied to keep the generated diff minimal — no toolchain churn),
mapped in connectionToProto, surfaced on libbox Connection as Detour()
StringIterator. Chain / Clash-API unchanged.
Docs: SPECS/017, lx-changelog rc.6.
The URL test history update hook and the Clash mode update hook were
single-slot: the API service's attached service overwrote the hook set
by the daemon, so clients stopped receiving group updates. Replace both
with multicast hook lists.
Also share a single URL test history storage via context: Clash API
looked it up under a key nobody registered and fell back to its own
empty storage, so dashboards showed no delay once an API service was
configured. Selector changes now notify through the shared storage,
covering selections made from any API surface.
Serialize probe rounds in startProber to eliminate unbounded fan-out of
fire-and-forget probe goroutines (up to 100/sec per direction), and close
HTTP/3 transports via transport.Close() in addition to CloseIdleConnections.
The URL test history update hook and the Clash mode update hook were
single-slot: the API service's attached service overwrote the hook set
by the daemon, so clients stopped receiving group updates. Replace both
with multicast hook lists.
Also share a single URL test history storage via context: Clash API
looked it up under a key nobody registered and fell back to its own
empty storage, so dashboards showed no delay once an API service was
configured. Selector changes now notify through the shared storage,
covering selections made from any API surface.