Files
shater/docs-shater/PORTING.md
T
omarandClaude Fable 5 693db39642 fix(schedule+profiles): evaluate windows at a captured UTC offset; stop claiming schedules are ignored
The schedule evaluator called time.LoadLocation, but the router binary
embeds no tzdata and OpenWrt ships none — so LoadLocation always failed
and windows silently ran in UTC while the panel promised local time.

- Windows now anchor to SchedUTCOffset (minutes east of UTC), which the
  panel captures from the editing browser on every schedule save; the
  daemon evaluates now.UTC()+offset with no location database. This
  sidesteps the weekly-recurring day-shift that a full local<->UTC
  conversion cannot express in one window. SchedTZ is deleted (documented
  in the removed-options list; old configs parse and drain it). DST is a
  stated limitation (followed on re-save). generate/schedule.go collapses
  from a second copy of the evaluator to a thin adapter over the model one.
- The iface-profile schedule was honored by the WAN watcher since
  08d5d6cc, but generate warned "the watcher does not look at the schedule"
  and the panel muted the editor with "the router ignores the schedule" —
  both false. Warning and lie removed; the editor is live and labelled
  "applies together with the uplink match".
- Stale fictions: FEATURES.md nftset/FakeIP-mode MVP line and the shipped
  conffile's dead `option dns_mode 'nftset'` corrected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 22:30:21 +03:00

28 KiB

Phase 2 porting spec — v0.1 control-plane → v0.2 (sing-box, in-process)

This is the authoritative map for the Phase 2 control-plane port. Every Phase 2 agent should read it. It records what ports near-verbatim vs. what is rewritten, the v0.2 package layout, and the exact v0.1 internals + sing-box option surface.

Source: a full read-only survey of the v0.1 branch (xrayctl/, shater-core/) and the sing-box option surface on main. Decisions: DECISIONS.md D11/D12.

v0.2 package layout (overlay under shater/ and openwrt/)

shater/
  cmd/shaterd/         # THE binary: embeds engine (box.New) + control-plane (+ panel later)
  cmd/shater-proto/    # Phase-1 embedding prototype (kept as reference)
  model/               # UCI desired-state structs (port model.go) + uci reader (uci.go) + migrate
  parse/               # share-link + subscription parsers -> model.Node/URI (port sharelink/subformat/sub/subfilter/wireguard)
  generate/            # model -> option.Options (NEW; replaces xray-JSON generate.go) + typed DNS
  engine/              # box.New lifecycle: build/validate/apply(=close+new)/close; config-hash gate
  netplane/            # nft `inet shater` + policy routing + tproxy + kill-switch + mgmt-bypass + dnsnat (port egress/nftstats/apply-nft)
  apply/               # orchestration: snapshot, validate, apply engine+netplane, commit-confirm rollback, reconcile under flock
  subs/                # subscription fetch/update/reconcile/cache (port sub.go)  [Phase 2b]
  ruleset/ preset/ schedule/ profile/ backup/ migrate/   # ported subsystems  [Phase 2b]
  stats/               # lx libbox command-client consumer (SubscribeDNSQueries/URLTest/traffic)  [Phase 5]
  api/                 # command surface the LuCI/rpcd/panel calls (verbs)
openwrt/
  shater-core/         # procd init, uci-defaults, hotplug, sysctl, Makefile (port shater-core; deps: shaterd, not xray)
  luci-app-shater/     # thin LuCI launcher  [Phase 3]

Build order / waves (parallel agents must own DISJOINT dirs, build only their own package, and never edit go.mod):

  • Wave 1 (contract): model/ (+ uci reader + migrate). Everything imports it.
  • Wave 2 (parallel): parse/, netplane/, generate/+engine/.
  • Wave 3 (integrate): apply/ + cmd/shaterd + openwrt/shater-core, then the VM gate.

Phase-2 GATE: a real client on the VM is proxied E2E through a sing-box outbound, DNS anti-leak holds, kill-switch is honest. Subscriptions/rulesets/ presets/schedules/profiles/stats are Phase 2b / later — not required for the gate.

Wave 3 daemon contract (shater/apply + cmd/shaterd)

The engine runs IN-PROCESS, so the daemon that holds the box IS shaterd run — a single long-lived process. It owns the engine; other CLI verbs SIGNAL it (they must not start a second engine). Design:

  • shaterd run (procd-supervised, the daemon): read UCI (model.ReadUCI); if Globals.Enabled → apply.Apply (generate → engine.Apply swap → netplane render+apply nft+routing); else stay inert. Then block. Signals: SIGTERM → apply.Teardown (engine.Close + netplane teardown) + exit; SIGHUP → apply.Reconcile (re-read UCI, idempotent re-apply). Optionally a small unix control socket for richer verbs later.
  • shaterd reconcile (from hotplug/cron/LuCI): find the running daemon and send SIGHUP (or via control socket). Idempotent; does NOT itself build an engine. Falls back to no-op if the daemon isn't running (fork-storm guard, like v0.1 allowColdStart=false).
  • shaterd apply|confirm|rollback: commit-confirm flow (arm a detached watcher that rolls back after Globals.ConfirmTimeout unless confirmed). apply.Rollback = engine.Rollback + netplane restore of last-good nft/routing.
  • shaterd status|nodes|stats (read-side, JSON for LuCI/panel) — MVP can stub these; full versions in Phase 5 (stats) via the lx command server.
  • shater/apply ties it together: Snapshot, Apply(m), Confirm, Rollback, Reconcile, Teardown, all under a cross-process flock (port flock_unix.go; lock e.g. /var/lock/shater.lock). It imports model, parse, generate, engine, netplane. ACTIVE_FLAG=/var/run/shater.active gates hotplug/cron (set on successful enabled apply, cleared on teardown).

MVP for the gate: shaterd run (read UCI → apply engine+netplane → block → SIGTERM teardown / SIGHUP reconcile) + shater/apply.Apply/Reconcile/Teardown under flock. Commit-confirm/rollback and read verbs can follow, but Teardown must be honest (fail-closed stays blocked until an explicit disable).

openwrt/shater-core (port from v0.1, swap deps to +shaterd +kmod-nft-tproxy +kmod-nft-socket +ip-full, drop dnsmasq since the engine does DNS): procd init.d/shater supervises /usr/bin/shaterd run (respawn, START=99, no procd_set_param file watch); reload_service→start/stop; service_triggers reload-trigger "shater"; stop sends SIGTERM (honest teardown). init.d/shater-cron (sub/ruleset/schedule due + reconcile). uci-defaults/30_shater-core (seed rt_tables 8192, enable inits, seed disabled presets, model.Migrate, sysctl from netplane.SysctlConf). hotplug.d/iface/99-shater (debounced shaterd reconcile). sysctl.d/99-shater.conf (from netplane). Default /etc/config/shater conffile.


What ports near-verbatim (engine-agnostic) vs. rewrites (engine-specific)

Port near-verbatim (not part of the engine config):

  • The entire nft / policy-routing / DNS-hijack data plane: egress.go, nftstats.go, the nft-render + routing parts of apply.go, sysctl, dnsnat, kill-switch, management-bypass. Only the tproxy port + loop-guard mark wiring is re-pointed.
  • UCI model + reader (model.go, uci.go), migrate.go, backup.go.
  • Share-link / subscription parsers (sharelink.go, subformat*.go, sub.go, subfilter.go, wireguard.go) — reuse the parsing logic; change only the OUTPUT target (xray map[string]any → typed option.*Options).
  • schedule.go, ruleset.go, preset.go, profile*.go — mostly engine-agnostic.
  • shater-core/ procd init, uci-defaults, hotplug, sysctl — port; swap deps (drop xray-core/xrayctl, add shaterd; keep kmod-nft-tproxy/socket, ip-full).

Rewrite (engine-specific):

  • generate.go → new generator emitting option.Options.
  • dns.go → typed option.DNSOptions (v1.14 removed flat servers + top-level fakeip).
  • Engine lifecycle: box has no Reload — apply = Close old box + New/Start; gate on a config hash so unchanged applies don't churn the tunnel.
  • observatory.go / stats / conns.go → lx libbox command server (SubscribeDNSQueries / GetRules / URLTestOutbound / traffic) instead of xray API.
  • compat.go → obsolete (build-tag/feature detection of the embedded engine).

sing-box SIMPLIFIES three v0.1 hacks — take the native path:

  • Chains (multi-hop): drop the inverted localhost-socks scaffold (emitChain); use native per-outbound DialerOptions.Detour chaining.
  • Groups/balancer: drop xray balancer+observatory; use native urltest (least_test) + lx round_robin balancer. fallbackTag (fail-closed→block) has no native equivalent → express as a trailing route rule.
  • resolver.detour: each sing-box DNS server has its own Detour — maps cleanly.

PART A — v0.1 control-plane (xrayctl/, branch v0.1)

File map (read the file on the v0.1 branch: git show v0.1:xrayctl/<file>)

  • model.go — typed desired-state *Model (see structs below); everything consumes it, nothing downstream touches raw UCI. Helpers parseBool/parseUint32/parseInt/splitTarget.
  • main.go — CLI dispatch (verbs LuCI/rpcd call): gen test selftest apply confirm rollback reconcile sub node chain geodata schedule backup restore profile migrate compat wanmode ruleset status conns nodes stats explain. flock on mutating verbs.
  • generate.go — Model → xray JSON. BuildConfig, BuildConfigFromLinks, builder, Outbound. (rewrite target)
  • sharelink.go — ParseShareLink, parseVLESS/VMess/Trojan/SS/Wireguard, buildStream, normalizeTransport.
  • subformat.go / subformat_parsers.go — DetectSubFormat, ParseSubBody, parseClashProxies/parseXrayOutbounds/parseSingboxOutbounds, synthLink/vmessLink/ssLink, streamOpts (converge all formats to share-link URIs).
  • sub.go — Fingerprint, FetchSubscriptionFull, SubUpdate, ReconcileSub, SubCache, CachedNode, SubUserinfo, ImportNodes (HAPP fetch, userinfo, fingerprint reconcile, cache).
  • subfilter.go — FilterSpec, subApplyFilters, nodeCountry, flagToISO, parseCountryFilter.
  • wireguard.go — parseWireguard, parseWGConf, wgConfigToURI, wgConfig, wgAWGSupported (WG/AWG link + .conf → model).
  • egress.go — buildEgresses, resolveEgressTag, egEgressMark, egEgressOutboundTag, genClassifySrc, genSrcClasses (egress binding + src classification; contract with nft plane).
  • nftstats.go — nftSrcFrags, nftRuleCounter/nftInCounter, nftListClients/nftListCounters, nftTableExists; consts nftTable="inet shater", nftClient4="clients".
  • apply.go — Apply, Confirm, Rollback, Reconcile, RenderNft, applyNft/applyRouting, TestConfig, GenerateConfig, ifaceDevice (atomic apply + nft + routing + rollback).
  • backup.go — Backup/BackupTo, Restore, backupManifest, untarInto (gzip-tar of /etc/config/shater + caches; validate-before-swap).
  • uci.go — ReadUCI, ParseUCIExport, parseSections, uciSection (uci -q export shater → Model).
  • dns.go — dnsRender, dnsResolverAddress, dnsQueryStrategy, dnsFakePool (Model → xray dns). (rewrite target)
  • compat.go — CompatJSON, xrayVersion, requiredFeatures, featureMinVer. (obsolete)
  • conns.go — ConnsJSON, parseConntrack (live flows, proxied/direct).
  • flock_unix.go — real blocking cross-process flock; lockPath=/var/lock/xrayctl.lock.
  • geodata.go — geoAssetPresent, geoStrip, GeodataStatus/Download/Remove (strip geo matchers when dat absent).
  • migrate.go — Migrate, CurrentSchemaVersion=1, uciRunner (UCI schema migration; refuses newer).
  • nodeops.go — NodeSetEnabled/NodeDelete/NodeAssignGroup/NodeQR.
  • observatory.go — read xray live state via gRPC API inbound (127.0.0.1:10853). (rewrite → lx command server)
  • preset.go — presetRules, presetDef, builtinPresets (curated rule packs → synthetic Rules).
  • probe.go — NodeHTTPProbe/All/ChainHTTPProbe, ProbeResult, runEphemeralXray, socks5Dial.
  • profile.go — named config snapshots (tar.gz). profile_wanmode.go — WAN-mode conditional overrides.
  • ruleset.go — LoadRuleset, rsResolve, RulesetUpdate (inline/file/url; plain/clash/geosite).
  • schedule.go — ruleActiveNow, scheduleSignature, ScheduleDue, timeNow (CP re-applies at window boundaries; engine has no time match).
  • status.go — StatusJSON/NodesJSON/StatsJSON/ExplainJSON, NodeTest, sortedRules, matchSrc/matchDst/domainMatch.

model.go — the data model (parsers PRODUCE this; the generator CONSUMES it)

type Model struct {
	Globals   Globals
	Inbounds  []Inbound
	Subscriptions []Subscription
	Nodes     []Node   // manual + subscription-cache (Node.FromSub != "")
	Groups    []Group
	Chains    []Chain
	Egresses  []Egress
	Rulesets  []Ruleset
	Rules     []Rule
	Presets   []Preset
	Profiles  []Profile
	Resolvers []Resolver
	DNSRules  []DNSRule
}
type Globals struct {
	Enabled bool; LogLevel string; KillSwitch string // closed|open
	DNSMode string // nftset|fakeip
	IPv6 bool; FwmarkBase uint32; TableBase uint32 // both default 0x2000
	ConfirmTimeout int; ResolverDefault string; ResolverFallback string
	ProbeURL string; ProbeInterval string; SchemaVersion int; ActiveProfile string
}
type Inbound struct { // Type default "" => tproxy
	Name string; Enabled bool; Type string // tproxy|socks|http|dokodemo
	Network string; TproxyPort int
	Listen string; Port int
	Auth,User,Pass string
	TargetAddr string; TargetPort int; TargetNetwork string
	TCP,UDP,Sniff bool
}
type Node struct {
	Name string; Enabled bool; URI string; FromSub string // "" = manual
	Fingerprint string; Stale bool
	Mux bool; MuxConcurrency int; XUDPConcurrency int; XUDPProxyUDP443 string
	Mark uint32; TCPFastOpen string; TCPKeepAliveIdle int
}
type Group struct {
	Name string; Source string // subscription|manual
	Subscription string; Nodes []string
	Strategy string // leastping|random|roundrobin|failover|single
	Include,Exclude,FilterProto,FilterCountry []string; Dedup bool
	ProbeURL,ProbeInterval string
}
type Chain struct { Name string; Hops []string } // "group:<n>" | "node:<n>", L1..Ln (Ln=exit)
type Egress struct { Name,Type,Interface,Target string } // interface|proxy|direct|block
type Rule struct {
	Name string; Enabled bool; Order int
	Src []string; DstDomain,DstRuleset,DstIP []string; DstPort,Proto string
	Target string // chain:|group:|node:|direct|block
	Egress,Kill string
	SchedEnabled bool; SchedDays []string; SchedStart,SchedEnd string; SchedUTCOffset int
}
type Resolver struct { Name,Type,Address,Detour,Pool string } // doh|dot|plain|local|fakeip
type DNSRule struct { Order int; MatchDomain,MatchSrc []string; Resolver string }
type Profile struct { // WAN-mode override
	Name string; Enabled bool; Priority int
	MatchIface []string; ProbeURL,ProbeMode string; SchedDays []string; SchedStart,SchedEnd string; SchedUTCOffset int
	EnableRules,DisableRules []string; DefaultTarget,DefaultEgress string
}

generate.go — node→xray mapping (rewrite reference)

Emits {log, dns, inbounds[], outbounds[], routing{rules,balancers}, observatory, api/stats/policy}.

  • loopMark = 255 (0xff) stamped on every real outbound + every tproxy inbound (withMark). API gRPC on apiInboundPort=10853.
  • baseline outbounds: direct (freedom, UseIP) + block (blackhole). Kill-switch closed ⇒ trailing catch-all → block (fail-closed).
  • inbounds: tproxy = dokodemo-door + followRedirect:true + sockopt{tproxy:"tproxy",mark:255} + sniff [http,tls,quic]; socks/http = plain listener; dokodemo = fixed target.
  • protocol emit (via ParseShareLink): vless settings.vnext[0].users[0]{id,encryption:none,flow?}; vmess users[0]{id,alterId,security}; trojan servers[0]{address,port,password}; ss servers[0]{address,port,method,password}; wireguard settings{secretKey,address[],peers[0]{publicKey,endpoint,allowedIPs,preSharedKey?,keepAlive?},mtu?,reserved?} + AWG jc/jmin/jmax/s1/s2/h1-h4 only if wgAWGSupported (v1 default false). applyNodeOpts adds mux + sockopt.
  • groups→balancer: member tag g_<name>__%04d_<node>; balancer {tag,selector,strategy,fallbackTag}; strategies leastPing/random/roundRobin/leastLoad/(failover=leastLoad{expected:1}); single/1-member ⇒ direct route; multi-member requires observatory.
  • chains: inverted socks pairs (socks_<chain>_L<i-1>). → v0.2: native Detour.
  • routing rules: presets prepended, first-match by Order, scheduled/disabled skipped, WAN overrides applied; src IP/CIDR/host → engine source, MAC/iface:/zone: → nft plane; geo stripped if dat absent.

egress.go + apply.go — the EXACT nft plane (PORT VERBATIM)

Table inet shater. Marks: tproxy divert = FwmarkBase (0x2000); loop-guard = 0xff; per-egress = FwmarkBase + 0x100 + idx. Sets clients(ipv4 dynamic counter), clients6(ipv6). Per-rule c_rule_<id>, per-inbound c_in_<id>.

table inet shater {
  set clients  { type ipv4_addr; flags dynamic; counter; }
  set clients6 { type ipv6_addr; flags dynamic; counter; }
  counter c_rule_<name> { } ...
  chain prerouting { type filter hook prerouting priority mangle; policy accept;
    meta mark 0xff accept                        # loop-guard
    meta mark 0x21xx accept                      # each interface/tunnel egress mark
    fib daddr type local accept                  # SSH/LuCI/DNS to any router IP
    ip daddr { 10/8,172.16/12,192.168/16,127/8,169.254/16,224/4,255.255.255.255 } accept
    ip6 daddr { ::1, fc00::/7, fe80::/10, ff00::/8 } accept
    <iif> l4proto {tcp,udp} th dport 853 accept  # DoT/DoQ out of tproxy (rejected in forward)
    <iif> l4proto {tcp,udp} th dport 53  accept  # :53 out (redirected by dnsnat)
    <iif> l4proto tcp <ip saddr X> update @clients {ip saddr} counter name "c_rule_x" tproxy ip to :12345 meta mark set 0x2000 accept
  }
  chain forward { type filter hook forward priority filter; policy accept;
    <iif> l4proto {tcp,udp} th dport 853 reject
    # ipv6-off / closed kill-switch: keep ND/RA/fe80/ff00, then meta nfproto ipv6 drop
  }
  chain dnsnat { type nat hook prerouting priority dstnat; policy accept;
    fib daddr type local accept
    <iif> l4proto {tcp,udp} th dport 53 redirect to :53
  }
}

Policy routing: ip -4/-6 rule add fwmark 0x2000 lookup 8192 + ip route add local default dev lo table 8192. Per-egress: mark→table TableBase+0x10+idx default via <gw> dev <dev> (or dev <dev> p2p). Never touches fw4. Apply/rollback: apSnapshot (run→last-good, nft→last-good.nft, route marks) → disabled?teardown : BuildConfig → TestConfig → syncRunJSON (sha256 compare; restart engine only on change) → applyNft (nft -c then -f) → applyRouting → optional commit-confirm (apArmAutoRollback detached watcher). Reconcile = idempotent re-apply, allowColdStart=false (fork-storm guard). Rollback restores nft→routing→run in order.

v0.2: "restart engine only on change" → config-hash gate + Close+New box (no reload).

uci.go — /etc/config/shater schema

  • config globals: enabled, loglevel, kill_switch, dns_mode, ipv6, fwmark_base, table_base, confirm_timeout, resolver_default, resolver_fallback, probe_url, probe_interval, schema_version, active_profile.
  • config inbound: name, enabled, type, network, tproxy_port(12345), listen, port, auth, user, pass, target_addr, target_port, target_network, tcp, udp, sniff.
  • config subscription: name, enabled, url, update_interval, fetch_via(direct|proxy), ua, hwid, device_os, ver_os, device_model, list header, format, list include/exclude/filter_proto/filter_country, dedup, expire_alert_days.
  • config node: name, enabled, uri, mux, mux_concurrency, xudp_concurrency, xudp_udp443, sockopt_mark, tcp_fast_open, tcp_keepalive_idle.
  • config group: name, source, subscription, list node, strategy, include/exclude/filter_proto/filter_country, dedup, probe_url, probe_interval.
  • config chain: name, list hop. config egress: name, type, interface, target.
  • config ruleset: name, type(domain|ipcidr), source(inline|file|url), url, path, format, update_interval, list entry.
  • config rule: name, enabled, order, list src/dst_domain/dst_ruleset/dst_ip, dst_port, proto, target, egress, kill, sched_enabled, list sched_day, sched_start/end/tz.
  • config preset: name, enabled, order, target. config profile: name, enabled, priority, list match_iface, probe_url, probe_mode, sched_*, list enable_rule/disable_rule, default_target, default_egress.
  • config resolver: name, type, address, detour, pool. config dns_rule: order, list match_domain/match_src, resolver.

Subscriptions & HAPP fetch

Schemes: vless:// vmess:// trojan:// ss:// wireguard:// wg://. Body formats (DetectSubFormat): clash-YAML, xray-JSON, singbox-JSON, base64/plain link list. All converge to URIs re-parsed by ParseShareLink. HAPP fetch: UA default Happ/3.13.0; headers x-hwid (auto UUIDv4/sub), x-device-os, x-ver-os, x-device-model, custom. fetch_via=proxy dials local socks. Quota/expiry from Subscription-Userinfo (upload;download;total;expire). Reconcile by Fingerprint (sha256 of proto|addr|port|id|net|sec|sni|path) → new/keep/stale (drop after 3 stale refreshes).


PART B — v0.1 packaging (shater-core/, branch v0.1)

Pure scripts+config, PKGARCH:=all. v0.1 DEPENDS: +xrayctl +xray-core +dnsmasq-full +kmod-nft-tproxy +kmod-nft-socket +ip-full. → v0.2 deps: +shaterd +kmod-nft-tproxy +kmod-nft-socket +ip-full (engine does DNS in-process, so dnsmasq-full may be droppable — confirm the :53 listener is our engine). /etc/config/shater is a conffile.

  • init.d/shater (procd, START=99/STOP=10): v0.1 supervised xray run -c /etc/xray/run.json; → v0.2 supervises shaterd. respawn 3600 5 0 (infinite). No procd_set_param file watch (would bounce tunnel on commit). Inert unless globals.enabled=1. ACTIVE_FLAG=/var/run/shater.active gates hotplug/cron. stop clears flag + tears down nft table + reserved routing tables. reload_service→start/stop. trigger procd_add_reload_trigger "shater".
  • init.d/shater-cron (START=96): supervised loop; per-item due-check, runs sub/ruleset update + reconcile + schedule due; watchdog: engine dead 5 ticks ⇒ kill_switch=open stops stack (fail-open), closed logs crit.
  • uci-defaults/30_shater-core: seed rt_tables (8192 shater), enable both inits, seed preset packs (disabled), run migrate, apply sysctl.
  • hotplug.d/iface/99-shater: ifup/ifdown → debounced (2s) reconcile (netifd wipes ip rules on reload). Guarded by enabled + ACTIVE_FLAG.
  • sysctl.d/99-shater.conf: ip_forward=1, rp_filter=0 (all+default), lo.route_localnet=1, lo.accept_local=1, all.src_valid_mark=1, ipv6.all.forwarding=1.

PART C — sing-box option surface (GENERATOR TARGET, on main)

Type consts (constant/proxy.go): TypeVLESS="vless" TypeVMess="vmess" TypeTrojan="trojan" TypeShadowsocks="shadowsocks" TypeHysteria2="hysteria2" TypeTUIC="tuic" TypeShadowTLS="shadowtls" TypeWireGuard="wireguard" TypeTProxy="tproxy" TypeMixed="mixed" TypeDirect="direct" TypeSelector="selector" TypeURLTest="urltest".

Wrappers: option.Inbound/Outbound/Endpoint{Type,Tag,Options any} — Options decoded by a per-type registry from the service context; must be pointers to the concrete struct. Shared: ListenOptions{Listen *badoption.Addr, ListenPort uint16, BindInterface, RoutingMark, TCPFastOpen, ...}; ServerOptions{Server string; ServerPort uint16}; DialerOptions{Detour, BindInterface, RoutingMark FwMark, ConnectTimeout, DomainResolver, ...}. Legacy inbound sniff/domain_strategy are REMOVED — sniff/resolve are route-rule actions now.

Inbounds

  • tproxy option.TProxyInboundOptions{ ListenOptions; Network NetworkList } (option/redir.go) — thin; sniff via a route rule action:"sniff".
  • mixed MixedInboundOptions, socks SocksInboundOptions (option/simple.go).

Outbounds (struct + key fields)

  • VLESS VLESSOutboundOptions{ DialerOptions; ServerOptions; UUID; Flow; Network; OutboundTLSOptionsContainer; Multiplex *OutboundMultiplexOptions; Transport *V2RayTransportOptions; PacketEncoding *string }
  • VMess VMessOutboundOptions{ …; UUID; Security; AlterId; GlobalPadding; AuthenticatedLength; Network; TLS; PacketEncoding; Multiplex; Transport }
  • Trojan TrojanOutboundOptions{ …; Password; Network; TLS; Multiplex; Transport }
  • Shadowsocks ShadowsocksOutboundOptions{ …; Method; Password; Plugin; PluginOptions; Network; UDPOverTCP; Multiplex } (no TLS/transport)
  • Hysteria2 Hysteria2OutboundOptions{ …; ServerPorts; HopInterval; UpMbps; DownMbps; Obfs *Hysteria2Obfs; Password; Network; TLS; QUICOptions; BrutalDebug }
  • TUIC TUICOutboundOptions{ …; UUID; Password; CongestionControl; UDPRelayMode; UDPOverStream; ZeroRTTHandshake; Heartbeat; Network; TLS; QUICOptions }
  • ShadowTLS ShadowTLSOutboundOptions{ …; Version; Password; TLS }

Shared TLS/Reality/uTLS/transport (option/tls.go, option/v2ray_transport.go)

OutboundTLSOptionsContainer{ TLS *OutboundTLSOptions }. OutboundTLSOptions{ Enabled, ServerName, Insecure, ALPN, MinVersion/MaxVersion, Certificate*, Fragment, UTLS *OutboundUTLSOptions{Enabled,Fingerprint}, Reality *OutboundRealityOptions{Enabled,PublicKey,ShortID}, ECH }. Reality = PublicKey + single ShortID; SNI from ServerName, fp from UTLS.Fingerprint (v1 spiderX/multi-shortId don't map). Transport *V2RayTransportOptions{Type}: "ws"({Path,Headers,MaxEarlyData,EarlyDataHeaderName}), "grpc"({ServiceName,...}), "httpupgrade"({Host,Path,Headers}), "http"({Host,Path,Method,Headers}), "quic", "xhttp"(V2RayXHTTPOptions, lx region).

Endpoints — WireGuard/AmneziaWG (option/wireguard.go, wireguard_awg.go)

WireGuardEndpointOptions{ System, Name, MTU, Address []netip.Prefix, PrivateKey, ListenPort, Peers []WireGuardPeer, UDPTimeout, Workers; AmneziaWGOptions (embedded, promoted to root); DialerOptions }. WireGuardPeer{ Address, Port, PublicKey, PreSharedKey, AllowedIPs []netip.Prefix, PersistentKeepaliveInterval, Reserved []uint8 }. AmneziaWGOptions{ Jc,Jmin,Jmax,S1,S2,S3,S4 uint32; H1..H4 MagicHeader(uint32 or "min-max"); I1..I5 string; Id,Ip,Ib string } — gated by with_awg (set-without-tag = hard error). Endpoints register as route targets by Tag (shared tag namespace with outbounds).

Route (option/route.go, rule.go, rule_action.go)

RouteOptions{ Rules []Rule; RuleSet []RuleSet; Final string; AutoDetectInterface; DefaultMark FwMark; DefaultDomainResolver; ...; LXIdleSuspend }. Rule = default|logical. RawDefaultRule matchers: Inbound, Network, Protocol, Domain, DomainSuffix, DomainKeyword, DomainRegex, Geosite, GeoIP, SourceGeoIP, SourceIPCIDR, IPCIDR, SourcePort(Range), Port(Range), SourceMACAddress, ProcessName, ClashMode, RuleSet, Invert. Action via embedded RuleAction{Action}: "route"→RouteActionOptions{Outbound; OverrideAddress; OverridePort}; plus direct, reject, hijack-dns, sniff, resolve, route-options. Target outbound/endpoint by action:"route", outbound:"<tag>"; default = route.final. Groups (option/group.go): SelectorOutboundOptions{Outbounds []string; Default; InterruptExistConnections}; URLTestOutboundOptions{Outbounds []string; URL; Interval; Tolerance; IdleTimeout; Mode "least_test"|"round_robin"; Balancer *URLTestBalancerOptions{Pool,PoolTolerance,StickyHash}(lx SPEC 019)}. Replaces xray balancer+observatory.

DNS (option/dns.go)

DNSOptions{ RawDNSOptions{ Servers []DNSServerOptions; Rules []DNSRule; Final string; ReverseMapping; DNSClientOptions{Strategy,Timeout,DisableCache,ClientSubnet} } }. Legacy flat servers + top-level fakeip REMOVED (v1.14). Servers typed via registry DNSServerOptions{Type,Tag,Options}; type strings (constant/dns.go): udp tcp tls https quic h3 local hosts fakeip dhcp mdns tailscale. Structs: RemoteDNSServerOptions(udp/tcp+DialerOptions+addr), RemoteTLSDNSServerOptions(+TLS), RemoteHTTPSDNSServerOptions(+Path/Method/Headers), LocalDNSServerOptions, FakeIPDNSServerOptions{Inet4Range,Inet6Range}. Each server has its own Detour. DNS rules reuse the route-rule shape targeting a DNS server tag. Observability (lx gRPC, experimental/libbox/command_client_command_lx.go): SubscribeDNSQueries streams DnsQuery{QueryType,DNSServer,DNSServerType,Answers(),Outbound()} (SPEC 018); GetRules(); URLTestOutbound(tag,link,timeout). This is the v0.2 stats/observability surface (replaces xray StatsService/ObservatoryService).

In-process embedding (box.go)

box.New(box.Options{Context, Options}) (*Box, error); PreStart()/Start()/Close() — NO Reload. Apply = Close old + New/Start fresh. ctx = include.Context(service.ContextWith(bg, deprecated.NewStderrManager(log.StdLogger()))). Options must be pointers.


Porting risks (v0.1 features with no clean sing-box equivalent)

  1. Live reload — box has no Reload; every apply is Close+New (drops connections). Gate on a config hash so unchanged applies don't churn.
  2. Chains — replace inverted socks scaffold with native DialerOptions.Detour (rewrite).
  3. Balancer strategies — map onto native urltest(least_test)+lx round_robin; no direct failover/random; fallbackTag fail-closed → trailing route rule.
  4. AmneziaWG — only under with_awg; the shater build enables it (router tag set), else awg fields hard-error.
  5. nft/routing/DNS-hijack plane — engine-independent; port verbatim (external nft, not engine config). tproxy loop-guard maps to ListenOptions.RoutingMark.
  6. Reality outbound — single ShortID; v1 spiderX/multi-shortId don't map.
  7. share-link parsers output — change from xray map[string]any to typed option.*OutboundOptions (new mapping layer; parsing logic reusable).
  8. DNS block-via-localhost hacks — replace with native typed DNS + per-server detour + reject DNS rule action.
  9. compat.go — obsolete (build-tag feature detection).
  10. api 10853 StatsService/ObservatoryService — replace with lx libbox command server.