Destination audit
Destination audit lets a katana node refuse connections to hosts you do not want it to reach, such as BitTorrent trackers or a site your terms of service forbid. Each rule is a regular expression. katana tests it against the host every flow asks for, refuses the flow on the first match, and remembers which user hit which rule so that a panel that keeps a detection log can be told.
Rules come from two places, and you can use both at once: the panel, and a local file named by rule_list_path. Read this page if you run a node for a panel that manages audit rules, or if you want a fixed block list that does not depend on the panel.
Quick start: a local rules file
Section titled “Quick start: a local rules file”This procedure adds a local block list to an existing node. It works with either panel.
-
Write the rules file. Put one regex on each line. Blank lines and lines that start with
#are skipped./etc/katana/rules.txt # katana destination audit rules: one regex per line.# Hosts named tracker.*, announce.* or open.tracker.*, a common naming# pattern for public BitTorrent trackers(?i)^(tracker|announce|open\.tracker)\.# One site and every subdomain of it, with or without a trailing dot(?i)(^|\.)blocked\.example\.com\.?$# Any destination given as an IPv4 literal in 203.0.113.0/24^203\.0\.113\.\d+$ -
Point the node at it. Set
rule_list_pathin the node’s[node.api]table:/etc/katana/config.toml [[node]]panel_type = "newV2board"[node.api]host = "https://panel.example.com"node_id = 1key = "replace-with-the-panel-key"node_type = "V2ray"rule_list_path = "/etc/katana/rules.txt" -
Check the config.
katana --test -c /etc/katana/config.tomlchecks that the key is spelled right and holds a string. It does not open the rules file, so a wrong path or a bad regex still printsConfiguration OK. -
Apply the change. If katana is already running, saving the config is enough: the hot reload loads the new path at once and keeps open connections. Otherwise start katana. The node reads the rules at the end of its start-up and again on every panel poll, so later edits to the file’s contents need no reload or restart: see When rules change.
-
Check the log. A path katana cannot read and each regex it cannot compile produce a
WARNline, and so does a failed SSPanel rules request. The log lines table lists them.
Settings
Section titled “Settings”Both keys belong to one [[node]] entry. Each node has its own rule set, so two nodes can use different files, or the same one.
| Key | Table | Type | Default | Description |
|---|---|---|---|---|
rule_list_path |
[node.api] |
path | "" |
The local rules file. Empty means no local rules. katana reads the file on every rule refresh. A relative path is resolved against katana’s working directory, so use an absolute path. |
disable_get_rule |
[node.controller] |
bool | false |
true turns rule refreshes off entirely, for both the panel rules and the local file. Set at start-up, it means the node audits nothing. |
Both tables reject unknown keys, so a typo stops katana instead of silently turning audit off:
configuration error: config parse error: TOML parse error at line 9, column 1 |9 | disable_get_rules = true | ^^^^^^^^^^^^^^^^^unknown field `disable_get_rules`, expected one of `listen_ip`, `send_ip`, `update_periodic`, `disable_upload_traffic`, `disable_get_rule`, `disable_sniffing`, `cert`update_periodic in [node.controller] (default 60 seconds) sets how often the node polls the panel, and therefore how often rules are refreshed and hits are reported. The katana config file page describes it with the other controller keys.
Where rules come from
Section titled “Where rules come from”On every refresh, katana builds one ordered list: the local rules first, in file order, then the panel’s rules. What the panel contributes depends on the panel type.
With panel_type = "newV2board" (or "v2board"), katana takes rules from the routes array of the node config that the UniProxy API returns. It makes no extra request for them.
"routes": [ { "id": 3, "match": ["(?i)(^|\\.)blocked\\.example\\.com$", "(?i)^tracker\\."], "action": "block" }, { "id": 7, "match": ["example.org"], "action": "dns", "action_value": "198.51.100.53" }]- Only entries whose
actionis exactlyblockbecome audit rules. katana ignores the other actions. - The entries of one route’s
matchlist are joined with|into a single regex. The first route above becomes(?i)(^|\.)blocked\.example\.com$|(?i)^tracker\.. - The rule’s id is the route’s position in the
routesarray, counting from0, not the panel’sidfield. The first route above gets id0. The id only matters for reporting, and this panel type has no report. - If the joined regex does not compile, katana logs a warning and skips the whole route, not just the bad entry.
- katana uses every entry as a regex, verbatim. It does not interpret prefixes such as
regexp:orprotocol:that some other node agents understand, so write plain regexes. - katana keeps the routes from the last node config it received. When the panel answers
304 Not Modifiedor the request fails, the previous routes stay in use. - When a hot reload gives the node a new panel client, after an edit to its
[node.api]table, the new client takes over these routes. The rules they produce stay in force even if the panel cannot be reached at that moment.
With panel_type = "sspanel", katana fetches GET /mod_mu/func/detect_rules on every refresh. Each item carries its own id and regex; katana reads only these two fields.
{ "ret": 1, "data": [ { "id": 5, "regex": "(?i)^tracker\\." }, { "id": 6, "regex": "(?i)(^|\\.)blocked\\.example\\.com$" } ]}- Each item becomes one rule with the panel’s
id. - An item whose
regexdoes not compile is logged and skipped. The other items still load. - An item without an
idgets the id0; an item without aregexgets the empty regex, which matches every host. - When the panel answers
304 Not Modified, the request fails,retis not1, or the body does not parse, katana keeps the rule set it already has. In these cases it does not re-read the local file either, so if the panel’s rules request never succeeds, the local file never loads.
The local file contributes the same way for both panel types:
- katana trims each line, then skips it if it is empty or starts with
#. There are no inline comments:foo # baris the regexfoo # bar. - Every other line is compiled as one regex. A line that does not compile is logged and skipped, and the rest of the file still loads.
- Every local rule gets the id
-1. - If the file cannot be read, katana logs a warning and the refresh continues with no local rules. The panel’s rules still load.
| Xboard / V2board | SSPanel | Local file | |
|---|---|---|---|
| Source | routes entries with action = "block" |
/mod_mu/func/detect_rules |
rule_list_path |
| One rule per | route (its match list joined with |) |
item | line |
| Rule id | position in routes, from 0 |
the item’s id |
-1 |
| Hits reported to the panel | no | yes, for ids 0 and above |
no |
What a rule matches
Section titled “What a rule matches”A rule is tested against the host the client asked for, as a string without the port:
- a domain name, exactly as the client sent it, for example
www.blocked.example.com; - or an IP literal, for example
203.0.113.7or2001:db8::1. IPv6 addresses have no brackets and are written in compressed lowercase form, so2001:0DB8:0:0::1is tested as2001:db8::1.
The match is an unanchored search with the Rust regex syntax. That means:
blocked\.example\.comalso matchesnotblocked.example.com.evil.test. Anchor with^and$, and use(^|\.)to cover a domain and its subdomains.- katana does not strip a trailing dot, so a client that asks for
blocked.example.com.does not match a pattern that ends incom$. End the pattern with\.?$to cover both forms. .matches any character. Escape it as\.in names and addresses.- Matching is case-sensitive. Start the pattern with
(?i)to ignore case, since clients do not always lowercase names. - Look-around (
(?=…),(?<!…)) and backreferences are not supported. Such a regex fails to compile and is skipped. - The port is not part of the string, so an audit rule cannot target a port. Use a routing rule for that.
What happens to a flow
Section titled “What happens to a flow”Every flow any inbound opens goes through the same check: a TCP request, each multiplexed sub-flow, each Hysteria 2 stream, and each UDP association. katana first admits the user, then routes the flow, and only then audits it.
flowchart TB
A["New flow"] --> B{"User still registered?"}
B -- no --> R1["Refused"]
B -- yes --> C{"UDP?"}
C -- yes --> U["Per-packet checks"]
C -- no --> D{"Route picks block?"}
D -- yes --> R2["Refused, no hit"]
D -- no --> E{"An audit rule matches?"}
E -- yes --> R3["Refused, hit recorded"]
E -- no --> F["Dialled through the routed outbound"]
- The first match wins. katana tests the rules in list order, local rules first, and records only the rule that matched first.
- A refused flow looks like a routing block. katana refuses the flow in exactly the way it refuses a flow routed to
block, so the client cannot tell the two apart. katana writes no log line that names the rule or the user. - Route-blocked flows are not audited. If the route already chose
block, katana refuses the flow without testing the audit rules, so no hit is recorded.
A UDP association has no single destination, because every datagram carries its own address. katana therefore routes and audits each outgoing packet. A packet whose destination is route-blocked or matches a rule is dropped silently. It is not counted as traffic, and the association stays open for its other destinations. A packet that matches a rule records a hit, just as a stream does.
The whole rule list is tested for every TCP flow and every outgoing UDP packet until a rule matches, so keep the list to the rules you need.
Hits and reporting
Section titled “Hits and reporting”A hit is a pair of user id and rule id. katana keeps a set of the pairs seen since the last report, so one user who hits the same rule a thousand times between two polls appears once. At the end of every poll, and once more when the node stops, katana takes the whole set, clears it, and sends it to the panel:
| Panel | What katana sends |
|---|---|
| SSPanel | POST /mod_mu/users/detectlog with a body of {"data":[{"list_id":5,"user_id":42}]}, one item per hit. Hits with a rule id below 0, meaning local rules, are left out. If no hits remain, no request is made. |
| Xboard / V2board | Nothing. katana has no detection-log report for this panel type, so the hits are cleared and discarded. |
A failed report is logged and not retried: the hits it carried are already cleared. This differs from traffic reporting, which keeps unreported traffic for the next poll.
When rules change
Section titled “When rules change”Each poll runs these steps in order:
- Fetch the node config and the user list, and apply them.
- Refresh the rules, unless
disable_get_ruleistrue: read the local file and collect the panel’s rules as described above. - Report traffic.
- Report and clear the hits.
A new rule therefore takes effect within one update_periodic interval. At start-up, katana brings the node’s listener up first and loads the rules right after it. When a hot reload changes a running node’s [node.api] table or its listener settings, such as listen_ip, katana runs one poll at once instead of waiting for the timer, so that poll refreshes the rules too.
katana compares the new list with the one in force, by id and regex text, in order. If they are identical, it keeps the current set. Any difference, including a change of order, replaces the whole set at once. TCP flows that are already open are not re-checked; each later UDP packet is checked against the set in force when it is sent.
Three cases do not follow this rule:
- With SSPanel, a
304 Not Modifiedanswer to the rules request keeps the current set without re-reading the local file. If your panel answers that way, an edit to the local file takes effect the next time the panel’s rules change, when you save an edit to the node’s[node.api]table, or when katana restarts. A[node.api]edit gives the node a new panel client that holds no ETags, so the panel answers its first rules request in full and katana reads the local file again. - When the listener changes. katana keeps rules per listener, named by node type,
listen_ipand port. When the panel moves the node to another port, the new listener gets its rules in the refresh of the same poll. When a hot reload changeslisten_ip, katana rebuilds the listener and gets its rules from the poll that the reload runs at once. With SSPanel, if either refresh is answered304 Not Modified, the new listener has no rules until the panel’s rules change, a[node.api]edit, or a restart. Neither a port move nor alisten_ipedit gives the node a new panel client, so it keeps its ETags and the panel can still answer304here. - With a Hysteria 2 node on Xboard / V2board whose
[node.hysteria].portis set, katana describes the node from the config file and never fetches the panel’s node config, so the node receives no panelroutes. Only the local file applies.
To point a running node at another file, change rule_list_path and save the config. The hot reload builds the node a new panel client and runs one poll at once, and that poll’s refresh reads the new file. New flows are checked against the new rules right away, and the edit drops no open connections. With SSPanel, the new file loads with the first rules request that succeeds; until then the current set stays in force. Edits to the contents of the file the node already points at need no reload: the next refresh reads them. disable_get_rule is read on every poll and can be changed by hot reload as well.
Log lines
Section titled “Log lines”katana logs rule problems at WARN and keeps running. 1 stands for the node’s node_id.
| Log line | Cause and fix |
|---|---|
cannot read rule_list_path /etc/katana/rules.txt: No such file or directory (os error 2) |
The file is missing or unreadable by the katana user. The node runs with the panel’s rules only until a later refresh can read it. |
invalid local rule "(foo": regex parse error: … |
One line of the local file does not compile. That line is skipped; fix it and it loads on the next refresh. |
invalid block rule ["(foo", "bar"]: regex parse error: … |
A Xboard / V2board block route whose joined match list does not compile. The whole route is skipped. |
invalid panel rule "(foo": regex parse error: … |
An SSPanel detect rule does not compile. That item is skipped. |
node 1: node_rule: … |
The SSPanel rules request failed. The rule set in force stays unchanged. |
node 1: report illegal: … |
The detection-log report to SSPanel failed. The hits it carried are dropped. |
A regex parse error spans several lines, with a caret under the problem:
invalid local rule "(foo": regex parse error: (foo ^error: unclosed groupTroubleshooting
Section titled “Troubleshooting”A rule never matches.
- The client may be connecting by IP address. Add IP patterns, or use a routing rule, which sees the sniffed name.
- Check the case: add
(?i). - Check the anchors against the exact host string:
^blocked\.example\.com$matches neitherwww.blocked.example.comnorblocked.example.com.. - Look for a
WARNline about the rule, and make suredisable_get_ruleis nottrue. - With SSPanel, look for
node_rulewarnings: while the rules request fails, the local file is not loaded either. - If a routing rule already sends the destination to
block, the flow is refused without an audit hit.
Everything is refused.
- Look for an empty or overly broad pattern: an empty
matchlist or entry on ablockroute, an SSPanel item with an empty or missingregex, or a local rule such as.or.*. - Comment out local rules one at a time; each edit takes effect on the next refresh, subject to the SSPanel
304case in When rules change.
SSPanel shows no detection log entries.
- Hits from local rules (id
-1) are never reported. - Hits are sent at the end of each poll, so wait one
update_periodicinterval. - Look for
report illegalwarnings.