#
# Shater desired-state config (/etc/config/shater) — v0.2.
#
# This shipped default is intentionally INERT: globals.enabled='0' means the
# daemon (`shaterd run`) stays inert, opens no engine and touches no networking,
# so a fresh install cannot affect connectivity. Configure via the LuCI app (or
# uci), set enabled='1', then apply (the daemon reads this file directly — the
# procd reload trigger restarts it, or `shaterd reconcile` re-reads live).
#
# ONE Go binary, shaterd, embeds the sing-box engine + control-plane + an
# in-process DNS server and renders the nft table `inet shater` + policy routing
# from this file. There is no separate xray/dnsmasq and no generated run.json.
#
# Full schema: docs-shater/PORTING.md (PART A "uci.go — /etc/config/shater
# schema") and shater/model.
#
# WHAT SURVIVES WHAT. This file is installed as a conffile, so `apk upgrade
# shater-core` will not replace it: your VALUES survive a package upgrade.
#
# These COMMENTS do not survive the first write, and that write does not need
# you to make it. The daemon and the panel persist the whole package in one go
# (`uci delete shater` + `uci import`), and a package rebuilt by `uci import`
# keeps no comments and no hand-made blank lines; the sections come back in the
# daemon's own order. Three things write here with nobody at the keyboard: a save
# in the admin panel, a subscription refresh (cron, every 6 h) and the profile
# watcher switching profiles (it looks every 25 s). So expect the annotations
# below to be gone shortly after the box is first configured.
#
# THE ANNOTATED COPY IS KEPT: /usr/share/shater/config.sample is this same file,
# installed by the package where nothing rewrites it. Read it there (`cat
# /usr/share/shater/config.sample`) and copy the fragment you need. It is
# refreshed by each package upgrade, so it always documents the build you have.
#
# BEFORE THE FIRST CHANGE, the previous file is copied to
# /etc/shater/config.pre-v<schema>.bak — once per schema version, never
# overwritten afterwards. That copy is the one taken at the transition (a schema
# migration, or the first save on a newly installed build); it is not a rolling
# backup, and it is deliberately not carried across a sysupgrade.
#

config globals 'globals'
	option enabled '0'
	option loglevel 'warning'
	# closed = fail-closed (block on engine loss); open = fail-open (plain routing).
	option kill_switch 'closed'
	# There is no dns_mode option: routing is decided by in-engine rule-sets and
	# fake-IP is a resolver type (`config resolver` with type=fakeip + pool).
	#
	# Force ALL LAN plaintext DNS (:53) into the engine, INCLUDING queries the
	# client sends to the router itself (the address DHCP hands out). ON by
	# default: with it off, a client using the router as its resolver is answered
	# by dnsmasq and forwarded to the ISP in the clear — no blocklists, no
	# per-device DNS rules, no resolver detour — while a client that hard-codes
	# 8.8.8.8 IS intercepted. The obedient client leaked; the evader did not.
	#
	# Set to '0' to opt out (dnsmasq answers router-addressed :53 again). Your
	# explicit value is never overwritten: this file is a conffile, and the daemon
	# always writes the option back as '1'/'0'.
	#
	# .lan and the private reverse (PTR) zones keep working: with at least one
	# `config resolver` present the engine gets a synthetic server pointed at
	# dnsmasq on 127.0.0.1:53 plus a rule that sends those suffixes to it; with no
	# resolver at all the engine falls back to the system resolver, which is
	# dnsmasq too. If you changed dnsmasq's domain away from `lan`, add a
	# `config dns_rule` for it (only `lan` + RFC6303 reverse zones are built in).
	#
	# While the engine is DOWN the LAN is NOT left without DNS: the fail-closed
	# holding plane hooks `forward` only, so dnsmasq still answers router-addressed
	# :53 — unfiltered and in the clear, the documented trade-off (blocking it
	# would also cut the daemon's own name resolution and its chance to recover).
	# Queries aimed at an EXTERNAL resolver are dropped with the rest of the LAN's
	# forwarded traffic.
	option dns_intercept '1'
	# Carry LAN ping through the tunnel. ON by default, and the alternative is
	# why: without it a ping is decided by `untunnelable` below, whose rungs are
	# "drop it" (block, the default) or "let it out of the WAN interface with the
	# client's real IP on it" (icmp/direct). There was no setting in which ping
	# both worked and stayed inside the tunnel. With this on, the engine opens a
	# TUN, LAN ICMP is routed into it, and an outbound that speaks layer 3
	# (WireGuard/AmneziaWG, or a direct route) carries the echo for real. An
	# outbound that does not (vless/trojan/shadowsocks) makes the ping DROP —
	# honestly: no reply is forged, ping reports loss. So a ping that used to
	# "work" through such a node was a ping that was leaking.
	#
	# It costs a permanent TUN device plus the gVisor netstack behind it, about
	# 2 MB of RSS for as long as the daemon runs.
	#
	# Set to '0' to opt out — worth it on a 32/64 MB router, or to bisect whether
	# the L3 ingress is what broke something. `shaterd apply` will tell you what
	# the off state costs. Your explicit value is never overwritten: this file is
	# a conffile and the daemon always writes the option back as '1'/'0'.
	#
	# NOTE the interaction: with this ON, `untunnelable` no longer governs ping at
	# all (the L3 route decision happens before the firewall chain its verdicts
	# live in). It still governs ESP/AH/GRE/IGMP/SCTP, which no tunnel of ours can
	# carry. `untunnelable 'icmp'` in particular stops meaning "block plus working
	# ping" and is reported as such.
	option l3_tunnel '1'
	option ipv6 '1'
	# Reserved fwmark base and routing-table base (do not overlap fw4/other apps).
	option fwmark_base '0x2000'
	option table_base '0x2000'
	# Seconds to auto-rollback an unconfirmed apply. SHIPPED AS 0, i.e.
	# commit-confirm is OFF: `shaterd apply` arms nothing, and an apply that costs
	# you SSH/LuCI access stays until you undo it by hand. Set a window (e.g.
	# '120') to arm it, and run `shaterd confirm` inside that window to keep the
	# new config. Note the option is written back only when NON-zero, so an
	# explicit '0' disappears from this file on the first write by the daemon or
	# the panel — absent and 0 are the same thing.
	option confirm_timeout '0'
	# Master enable of the DNS blocklist/allowlist filter (D15). OFF by default;
	# it needs at least one `config resolver` to have a DNS plane to filter with.
	# See the "DNS filter" section at the end of this file.
	option dns_filter '0'
	option schema_version '2'

# LAN interception inbound. `network` is a UCI interface name; shaterd resolves
# it to its device (e.g. 'lan' -> br-lan) for the nft TPROXY plane. Enable
# globals above and adjust `network` to the interface(s) you want proxied.
#
# There is no per-inbound `sniff` option: since sing-box 1.11 sniffing is a
# leading route ACTION rule with no inbound matcher, so EVERY inbound is sniffed,
# always. Do not add one back — the hijack-dns rule matches the SNIFFED `dns`
# protocol, so a per-inbound sniff toggle would be a DNS-leak switch (D14, and
# the long argument at shater/model/model.go Inbound).
config inbound
	option name 'lan'
	option enabled '1'
	option type 'tproxy'
	option network 'lan'
	option tproxy_port '12345'
	option tcp '1'
	option udp '1'

# --- Commented examples (copy, uncomment, adjust, then enable globals) -------
#
# A proxy node from a share link (vless/vmess/trojan/ss/wireguard://...).
#config node
#	option name 'my-node'
#	option enabled '1'
#	option uri 'vless://uuid@host:443?security=reality&pbk=...&sni=example.com#my-node'
#
# A group balancing several nodes (strategy: leastping|random|roundrobin|
# failover|single). Source 'manual' lists nodes explicitly; 'subscription'
# pulls a subscription's nodes.
#config group
#	option name 'main'
#	option source 'manual'
#	option strategy 'leastping'
#	list node 'my-node'
#
# A routing rule. target: chain:<n>|group:<n>|node:<n>|egress:<n>|direct|block.
# Match on src / dst_ruleset / dst_port / proto. A rule with NO matcher at all is
# the default route for everything that reached it.
#
# WHERE the traffic is going is named ONLY by dst_ruleset — one or more
# `config ruleset` names; the rule matches when ANY of them matches. There is no
# inline domain or address list on a rule (`dst_domain`/`dst_ip` were removed in
# schema v2): a destination list is written once as a ruleset, compiled into a
# .srs and shared by every rule that references it. `shaterd migrate` converts
# older configs automatically, creating a `rule-<name>` ruleset per rule.
#config ruleset
#	option name 'blocked-video'
#	option type 'domain'
#	option source 'inline'
#	list entry 'youtube.com'
#	list entry 'suffix:googlevideo.com'
#
#config rule
#	option name 'video-via-main'
#	option enabled '1'
#	option order '50'
#	list dst_ruleset 'blocked-video'
#	option target 'group:main'
#
#config rule
#	option name 'all-via-main'
#	option enabled '1'
#	option order '100'
#	option target 'group:main'
#
# A native DPI-bypass egress (D13). type 'direct' sends traffic straight out (no
# tunnel), and `dpi` applies a compiled sing-box desync on the ClientHello:
#   off      - none (default)
#   fragment - tls_fragment (split the TLS record; the usual light bypass)
#   record   - tls_record_fragment (alternative; mutually exclusive w/ fragment)
#   spoof    - tls_spoof (inject a decoy ClientHello; needs root NET_RAW/NET_ADMIN)
# Point a rule's target at it for DPI-blocked-but-not-IP-blocked domains — direct
# and fragmented, no exit node, no extra binary. These three are the WHOLE set;
# the external desync egress that once stood beside them is removed (D29), and a
# `type 'byedpi'` egress left over from an older build is blocked, not routed.
#config egress
#	option name 'frag'
#	option type 'direct'
#	option dpi 'fragment'
#
#config ruleset
#	option name 'youtube'
#	option source 'geosite'
#	list category 'youtube'
#
#config rule
#	option name 'youtube-fragment'
#	option enabled '1'
#	option order '50'
#	list dst_ruleset 'youtube'
#	option target 'egress:frag'
#
# A DNS resolver (type: doh|dot|plain|local|fakeip). `detour` routes its queries
# through a group/node so DNS does not leak.
#config resolver
#	option name 'cloudflare'
#	option type 'doh'
#	option address 'https://1.1.1.1/dns-query'
#	option detour 'group:main'
#
# --- DNS filter (D15) -------------------------------------------------------
# Network-wide domain blocking, built on sing-box rule-sets + reject DNS rules.
# Turn it ON by flipping `option dns_filter` to '1' in `config globals` above (it
# is shipped '0'). Filtering needs at least one `config resolver` (the in-engine
# DNS plane). A blocklist answers matched domains with NXDOMAIN; an allowlist
# always OVERRIDES the blocklists (allowlisted domains resolve normally).
#
# A blocklist. source: inline|file|url|geosite.
#   inline  - domains listed here (bare domain => also blocks its subdomains;
#             'full:host' exact, '.suffix' suffix, 'keyword:str' substring).
#   url     - a remote sing-box rule-set (.srs); sing-box fetches + caches it
#             itself on the given update_interval (default 24h), via `direct`.
#   file    - a local compiled .srs / source rule-set at `path`.
#   geosite - inert until geodata is shipped (fail-open).
# response: nxdomain (default) | zero  (zero currently falls back to NXDOMAIN).
#
# Well-known lists (url source, shipped DISABLED — flip enabled to '1'):
#config blocklist
#	option name 'stevenblack'
#	option enabled '0'
#	option source 'url'
#	option url 'https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts.srs'
#	option response 'nxdomain'
#	option update_interval '24h'
#
#config blocklist
#	option name 'oisd'
#	option enabled '0'
#	option source 'url'
#	option url 'https://raw.githubusercontent.com/sjhgvr/oisd/main/domainswild2_small.srs'
#	option response 'nxdomain'
#	option update_interval '24h'
#
#config blocklist
#	option name 'adguard'
#	option enabled '0'
#	option source 'url'
#	option url 'https://raw.githubusercontent.com/ph00lt0/blocklist/master/adguard.srs'
#	option response 'nxdomain'
#	option update_interval '24h'
#
# An inline blocklist (small, hand-curated):
#config blocklist
#	option name 'my-blocks'
#	option enabled '0'
#	option source 'inline'
#	list entry 'ads.example'
#	list entry '.doubleclick.net'
#	list entry 'keyword:telemetry'
#
# An allowlist (overrides every blocklist above):
#config allowlist
#	option name 'my-allows'
#	option enabled '0'
#	option source 'inline'
#	list entry 'analytics.mycompany.example'
