SSPanel (mod_mu)
With panel_type = "sspanel", a katana node takes its description and its users from an SSPanel installation through the mod_mu API, the same node API XrayR uses. katana polls the panel, builds the listener the panel describes, and sends back per-user traffic and audit hits.
Read this page when you point katana at SSPanel, when you move an SSPanel node over from XrayR, or when a node refuses to start and the log names a panel field. The general layout of the config file is on Config file; this page covers what is specific to SSPanel.
At a glance
Section titled “At a glance”| Property | SSPanel in katana |
|---|---|
panel_type |
"sspanel", matched without regard to case |
| Authentication | [node.api].key, sent as both the key and muKey query parameters |
| Node types | V2ray (VMess or VLESS), Trojan, Hysteria 2. Shadowsocks is refused. |
| Node description | custom_config on SSPanel 2021.11 and later unless disable_custom_config = true, otherwise the legacy server string |
| Speed limits | The node limit and each user’s limit from the panel, or one local override |
| Audit | Panel detect rules plus an optional local rule file; hits are reported to detectlog |
| Not implemented | Online IP reports (aliveip) and node status reports |
Minimal example
Section titled “Minimal example”This node serves VMess. The panel decides the port, the transport and whether TLS is on; the certificate always comes from the local files. With the custom_config shown below, the node serves VMess over WebSocket and TLS on port 443.
# A katana node for SSPanel (mod_mu): one V2ray node that serves VMess over# TLS. The panel supplies the port, the transport and the user list; the# certificate is always local. Replace the panel key and certificate paths.
[log]level = "info"
[[node]]panel_type = "sspanel"
[node.api]host = "https://panel.example.com"node_id = 1key = "replace-with-the-panel-key" # sent as both `key` and `muKey`node_type = "V2ray" # V2ray serves VMess unless the panel enables VLESStimeout = 10 # seconds per panel request; 0 means 5speed_limit = 0 # Mbps; 0 keeps the panel's own limitsrule_list_path = "/etc/katana/rules.txt"disable_custom_config = false # read custom_config on SSPanel 2021.11 and later
[node.controller]listen_ip = "0.0.0.0"update_periodic = 60 # seconds between panel polls
[node.controller.cert]mode = "file"cert_file = "/etc/katana/fullchain.pem"key_file = "/etc/katana/privkey.pem"-
Create the node in SSPanel. Give it the V2ray type and fill in its
custom_config(see custom_config below). Note the node ID the panel assigns, and themuKeyfrom the panel’s configuration. -
Write the config. Put the panel URL in
host, the node ID innode_idand themuKeyinkey. Pointcert_fileandkey_fileat a certificate for the name clients connect to. -
Check it.
--testbuilds the panel client and the router without contacting the panel:Terminal window katana --test -c /etc/katana/config.tomlConfiguration OK--testdoes not fetch the node, so it cannot tell you whether the panel’s description is one katana can serve. A Shadowsocks node, for example, passes--testand is refused only when katana starts. The one exception is a Hysteria 2 node:--testalso checks its local[node.hysteria]settings and certificate. -
Start katana and watch for the listener line:
INFO katana::manager::node: node 1: listening on 0.0.0.0:443
Settings
Section titled “Settings”Every SSPanel setting lives in [node.api]. All keys are optional as far as the parser is concerned, but a node without host, node_id, key and node_type cannot reach the panel or does not start. Unknown keys are an error, so a typo such as mu_key stops --test.
| Key | Type | Default | On SSPanel |
|---|---|---|---|
host |
string | "" |
The panel’s base URL with its scheme, for example "https://panel.example.com". katana removes a trailing / and appends /mod_mu/…, so a panel under a sub-path works. |
node_id |
integer | 0 |
The node ID from SSPanel. It goes into the node info path and into the node_id query parameter. |
key |
string | "" |
The panel’s muKey. katana sends it twice, as the key and the muKey query parameters, as XrayR does. |
node_type |
string (enum) | "" |
V2ray (aliases vmess, vless), Trojan, Hysteria2 (aliases hysteria, hy2), or Shadowsocks, which SSPanel nodes refuse. Case does not matter. Anything else, including an empty value, fails with unknown node_type. The alias vless does not turn VLESS on by itself; see enable_vless. |
enable_vless |
bool | false |
Serve VLESS instead of VMess on a V2ray node. It is combined with the panel’s own enable_vless: either one turns VLESS on. |
vless_flow |
string | "" |
Read only when the node comes from the legacy server string. Any non-empty value is refused, because katana does not implement XTLS flows. |
timeout |
integer | 0 |
Seconds allowed for each panel request, from connect to the end of the body. 0 means 5. |
speed_limit |
float | 0 |
A local per-user limit in Mbps. A value above 0 replaces every limit the panel sends. See Speed limits. |
device_limit |
integer | 0 |
Accepted for XrayR compatibility and ignored. |
rule_list_path |
string | "" |
A local file of audit rules, one regular expression per line. A file katana cannot read is logged and gives no rules. See Audit rules. |
disable_custom_config |
bool | false |
Always read the legacy server string, even when the panel is 2021.11 or later. |
The keys under [node.controller] apply to every panel type: update_periodic sets the poll interval, disable_get_rule turns audit rules off, and disable_upload_traffic stops traffic reports. Config file describes them.
What katana asks the panel
Section titled “What katana asks the panel”katana calls five mod_mu endpoints. Every request carries key and muKey in the query string; the ones that concern a node’s users also carry node_id.
| Request | Query | Body | ETag | When |
|---|---|---|---|---|
GET /mod_mu/nodes/<node_id>/info |
key, muKey |
yes | At start, then every poll | |
GET /mod_mu/users |
key, muKey, node_id |
yes | At start, then every poll | |
GET /mod_mu/func/detect_rules |
key, muKey |
yes | After the node starts, then every poll, unless disable_get_rule = true |
|
POST /mod_mu/users/traffic |
key, muKey, node_id |
{"data":[{"user_id","u","d"}]} |
Every poll that has traffic to report, and once more when the node stops, unless disable_upload_traffic = true |
|
POST /mod_mu/users/detectlog |
key, muKey, node_id |
{"data":[{"list_id","user_id"}]} |
Every poll that has panel-rule hits to report, and once more when the node stops |
One poll runs these requests in order, every update_periodic seconds (60 by default):
sequenceDiagram
participant K as katana node
participant P as SSPanel
K->>P: GET /mod_mu/nodes/1/info
P-->>K: node description, or 304
K->>P: GET /mod_mu/users
P-->>K: user list, or 304
Note over K: rebuild the listener or refresh users if anything changed
K->>P: GET /mod_mu/func/detect_rules
P-->>K: audit rules, or 304
K->>P: POST /mod_mu/users/traffic
K->>P: POST /mod_mu/users/detectlog
The response envelope
Section titled “The response envelope”SSPanel wraps every answer in an envelope, {"ret": 1, "data": …}. katana accepts a GET response only when ret is exactly 1; a missing ret counts as 0. Anything else is an error that names the endpoint:
node 1: node_info failed: /mod_mu/nodes/1/info: panel returned ret=0; retrying in 1sFor the two POST requests, katana treats a body that is not JSON, such as an empty body, as success. A JSON object whose ret is not 1, including one with no ret at all, is a failure.
An HTTP status of 400 or above is an error too, and so are a network failure and a timeout. katana removes the URL from these errors before it logs them, because the URL contains the panel key, so the log line names only the method and path, not the status or the cause:
node 1: node_info failed: GET /mod_mu/nodes/1/info; retrying in 1sFor each of the three GET endpoints, katana remembers the last ETag header the panel sent and returns it in If-None-Match on the next request. An HTTP 304 Not Modified answer means “unchanged”: katana keeps the node description, user list or rule set it already has and does no work. A panel that sends no ETag is answered in full every time.
When a poll’s GET fails, katana logs a warning and keeps the last state it applied, so a panel outage does not take a running node down.
Two cases start without ETags, so the panel answers in full:
- The first start. katana forgets the ETags before every attempt to bring the node up, then fetches the node info and the user list. If an attempt fails, katana logs
node <node_id>: …; retrying in <N>sand tries again; see When a node cannot start. - A config edit to
[node.api]. katana builds a new panel client, which holds no ETags. See Changing settings while katana runs.
How katana reads the node
Section titled “How katana reads the node”SSPanel describes a node in one of two ways: the JSON custom_config object that SSPanel 2021.11 introduced, or the older semicolon-separated server string. katana picks one with the version field of the node info response and disable_custom_config:
flowchart TB
A{"node_type is Shadowsocks?"} -->|yes| R1["refused"]
A -->|no| B{"disable_custom_config = true?"}
B -->|yes| L["legacy server string"]
B -->|no| C{"panel version 2021.11 or later?"}
C -->|no| L
C -->|yes| CC["custom_config"]
L --> D{"node_type is Hysteria2?"}
D -->|yes| R2["refused"]
D -->|no| OK["V2ray or Trojan node"]
katana compares the version one dot-separated number at a time, ignoring any character that is not a digit. 2021.11, 2021.11.0 and 2023.3 all select custom_config; 2021.10 and an empty or missing version select the legacy string.
The response’s node_speedlimit is read in both cases (see Speed limits).
custom_config
Section titled “custom_config”katana reads these keys from custom_config. It ignores every other key, and a missing key counts as empty.
| Key | JSON type | Used for | Meaning |
|---|---|---|---|
offset_port_node |
string | all | The port katana listens on, for example "443". It must be a JSON string holding a number from 1 to 65535. |
network |
string | V2ray, Trojan | The transport, in any case: tcp (or raw, or empty), ws (or websocket), grpc (or gun). |
security |
string | V2ray | tls or xtls, in lower case, puts TLS on the listener. Any other value, including empty, means no TLS. |
enable_vless |
string | V2ray | "1" serves VLESS instead of VMess. |
flow |
string | V2ray, Trojan | Must be empty. Any flow is refused. |
host |
string | V2ray, Trojan | The WebSocket Host for a ws node. |
path |
string | V2ray, Trojan | The WebSocket path for a ws node. Empty means /. |
servicename |
string | V2ray, Trojan | The gRPC service name for a grpc node. |
enable_reality |
bool | V2ray, Trojan | Must be false. REALITY is refused. |
header |
object | V2ray, Trojan | Read, but katana has no TCP header camouflage. |
obfs |
string | Hysteria 2 | "salamander", or empty for none. |
obfs-password |
string | Hysteria 2 | The Salamander password, at least 4 bytes. |
A Trojan node always has TLS, whatever security says. A Hysteria 2 node reads only offset_port_node, obfs and obfs-password; its QUIC listener has no transport to choose. method and server_key are read but only matter to Shadowsocks, which SSPanel nodes do not serve.
A VMess node over WebSocket and TLS on port 443 looks like this in SSPanel:
{ "offset_port_node": "443", "network": "ws", "security": "tls", "host": "proxy.example.com", "path": "/ws"}When a panel is 2021.11 or later but the node has no custom_config, katana does not fall back to the server string. It stops with custom_config is empty, disable custom config. Either fill in custom_config, or set disable_custom_config = true so katana reads the legacy string.
The legacy server string
Section titled “The legacy server string”katana reads the server field when the panel is older than 2021.11 or when disable_custom_config = true. The field has a different layout for each node type.
proxy.example.com;443;0;ws;tls;path=/ws|host=proxy.example.comThe string is split on ; and needs at least six parts:
| Part | Meaning |
|---|---|
| 1 | Address. katana ignores it. |
| 2 | The port katana listens on. |
| 3 | alterId. katana ignores it. |
| 4 and 5 | Each is tls, a transport name (tcp, ws, grpc), or empty, in either order. tls turns TLS on. |
| 6 | key=value pairs separated by |: path, host, servicename and headerType. A path may itself contain =. |
headerType has the same limitation as header in custom_config: katana reads it but serves plain TCP. VLESS comes only from enable_vless in [node.api], and the flow only from vless_flow.
proxy.example.com;port=443#8443|host=proxy.example.com|grpc=1|servicename=trojan| Item | Meaning |
|---|---|
port=443 |
The port. katana listens on it when no inside port follows. |
#8443 |
An optional inside port. When it is present, katana listens on it and ignores the port before #. |
grpc |
Any grpc item, whatever its value, makes the node gRPC. Without it the node is TCP. |
servicename |
The gRPC service name. |
host |
Read, but has no effect on a TCP or gRPC listener. |
A legacy Trojan node always has TLS and cannot use WebSocket; use custom_config for that.
A legacy string cannot describe a Hysteria 2 node. katana refuses one with sspanel: a hysteria2 node needs custom_config; the legacy server string cannot describe one.
Node types
Section titled “Node types”node_type |
Protocol | Transports | TLS | Notes |
|---|---|---|---|---|
V2ray |
VMess, or VLESS when enabled | TCP, WebSocket, gRPC | From security, or tls in the legacy string |
No VLESS flow, no REALITY |
Trojan |
Trojan | TCP, WebSocket, gRPC (legacy: TCP, gRPC) | Always | Each user’s password is their UUID |
Hysteria2 |
Hysteria 2 over QUIC | none | Always | custom_config only, or a local description |
Shadowsocks |
Refused: sspanel: Shadowsocks node type is not supported |
Every node that has TLS needs mode = "file" with cert_file and key_file under [node.controller.cert]. katana never takes a certificate from the panel, and it refuses the ACME modes dns, http and tls. Protocols covers what each protocol supports once it is running.
Hysteria 2 on SSPanel
Section titled “Hysteria 2 on SSPanel”SSPanel has no Hysteria 2 node type of its own. katana decides the protocol from node_type in its own config, not from the type the panel shows, so you can create the node with any type SSPanel offers and give it a custom_config with the keys shown above. There are two ways to describe it:
- From the panel. Leave
[node.hysteria].portat0. katana reads the port fromoffset_port_nodeand the obfuscation fromobfsandobfs-password. The panel must be 2021.11 or later anddisable_custom_configmust befalse. - Locally. Set
[node.hysteria].port. katana then never fetches the node info and takes the port and obfuscation from[node.hysteria]. It still fetches users, reports traffic and reads audit rules from SSPanel.
The settings no panel can express, such as how a credential is read, UDP relay and the masquerade page, always come from [node.hysteria]. Hysteria 2 nodes describes them.
katana reads three fields of each entry in the user list:
| Field | Use |
|---|---|
id |
The user ID that traffic and audit reports carry. Required. |
uuid |
The credential: the VMess or VLESS ID, the Trojan password, or the Hysteria 2 credential. Required. |
node_speedlimit |
This user’s speed limit in Mbps. |
passwd, port and method are read but not used, because they only matter to Shadowsocks. If an entry lacks id or uuid, the whole list fails to parse (parse sspanel user list) and katana keeps the previous list. On the first start there is no previous list, so the start fails with user_list failed: parse sspanel user list and katana retries it. On a V2ray node, a user whose uuid is not a valid UUID is skipped with a warning that names the user ID and never the value:
WARN katana::manager: skipping user 8: uuid is not a valid UUIDWhen the panel returns an empty user list, katana closes the node’s listener until users come back. Traffic already counted is still reported.
Speed limits
Section titled “Speed limits”SSPanel sends two limits, both in Mbps: node_speedlimit in the node info, and node_speedlimit on each user. katana converts Mbps to bytes per second by multiplying by 125 000, and treats 0 or a negative value as no limit.
| Node limit | User limit | Each user gets |
|---|---|---|
| 0 | 0 | No limit |
| 0 | U | U |
| N | 0 | N |
| N | U | The smaller of N and U |
The node limit applies to each user separately. It is not a cap on the node’s total bandwidth.
Setting speed_limit in [node.api] to a value above 0 replaces both panel values with that number, so every user on the node gets exactly that limit. A Hysteria 2 node described locally takes its node limit from speed_limit alone, because katana does not fetch its node info.
When the panel changes a limit, katana applies it on the next poll without restarting the listener or dropping the user’s connections. Flows that are already open keep the old rate; flows opened after the change use the new one. An edit to speed_limit in the config file is applied the same way, at once. Speed limits explains how the token bucket enforces them.
Audit rules
Section titled “Audit rules”katana builds each node’s rule list from two sources, in this order:
- The local file at
rule_list_path: one regular expression per line, with blank lines and lines starting with#skipped. Every local rule has the ID-1. - The panel’s
detect_rules: a list of{"id": …, "regex": …}objects. A rule whose regular expression does not compile is logged and skipped.
katana matches each flow’s destination host, a domain name or an IP address without the port, against the rules in order. A rule matches anywhere in the host unless its expression is anchored with ^ and $. The first match refuses the flow (for UDP, katana drops the datagram) and records the pair of user ID and rule ID; each pair is recorded once per poll. At the end of each poll, katana posts the recorded pairs to /mod_mu/users/detectlog as list_id and user_id, and only for rules whose ID is 0 or higher. A local rule therefore blocks the connection but never appears in SSPanel’s audit log. A panel rule with no id gets the ID 0.
Audit covers the matching rules in more detail.
Traffic reports
Section titled “Traffic reports”Each poll, katana posts the bytes each user moved since the last successful report:
{"data": [{"user_id": 7, "u": 1048576, "d": 52428800}]}u is upload and d is download, both in bytes. Users with no traffic are left out, and katana sends no request when there is nothing to report. If the request fails, the bytes are kept and sent with the next report, so a panel outage delays traffic accounting without losing it. Traffic reporting describes the accounting.
What katana does not call
Section titled “What katana does not call”XrayR calls two more mod_mu endpoints that katana leaves out:
| Endpoint | XrayR uses it for | In katana |
|---|---|---|
POST /mod_mu/users/aliveip |
Online user IPs | Not called. SSPanel shows no online IPs for katana nodes, and device_limit has no effect. |
POST /mod_mu/nodes/<node_id>/info |
Node load and uptime, on panels older than 2023.2 | Not called. The panel shows no load for katana nodes. |
Changing settings while katana runs
Section titled “Changing settings while katana runs”katana watches its config file. For an SSPanel node, the changes fall into these groups:
| You change | What happens |
|---|---|
panel_type (other than its letter case), host, node_id or key |
These identify the panel node, so katana stops the node and starts it again with a new panel client. The ETags are forgotten, so the next requests are answered in full. |
node_type, enable_vless, vless_flow, timeout, speed_limit, rule_list_path or disable_custom_config |
Applied in place. The node builds a new panel client and at once reads the node info and the user list again, in full. enable_vless always rebuilds the listener. For the others, katana rebuilds the listener, which drops the node’s connections, only if the new description changes the protocol or the transport; otherwise it applies the new users and speed limits as on a normal poll. |
listen_ip, the certificate settings, disable_sniffing, [node.hysteria] or [node.route] |
katana rebuilds the listener, which drops the node’s connections. |
update_periodic, disable_get_rule or disable_upload_traffic |
Applied from the next poll, without touching the listener. |
Before katana applies an edit, it builds the new panel client of every node whose settings changed. If one does not build, for example because of a misspelled node_type, katana applies nothing from that edit and logs reload: node <node>: <error>; keeping current config, where <node> is the panel type, host and node ID, for example sspanel@https://panel.example.com#1. Every node keeps running as it was.
An edit that reaches a node which is still retrying its first start is stored, and the node tries again at once with the new settings.
Hot reload covers reloads in general.
Common errors
Section titled “Common errors”These are the messages katana logs when an SSPanel node cannot start. Most appear after node <node_id>: node_info failed: or node <node_id>: initial start failed:, followed by ; retrying in <N>s. The same messages appear after node <node_id>: node_info: or node <node_id>: rebuild failed: when a running node’s next poll hits them.
| Message | Cause | Fix |
|---|---|---|
unknown panel_type "…" |
panel_type is misspelled. --test reports it. |
Use "sspanel". |
unknown node_type "…" |
node_type is empty or misspelled. --test reports it. |
Use V2ray, Trojan or Hysteria2. |
no nodes could be started |
At startup, no [[node]] could build its panel client (for example because of the two errors above) or its router, so katana exits. |
Fix the errors logged before it; --test shows the first one. |
/mod_mu/nodes/1/info: panel returned ret=0 |
The panel refused the request, usually because of a wrong key or node_id. |
Check the muKey and the node ID in SSPanel. |
GET /mod_mu/nodes/1/info |
Network failure, timeout, or an HTTP error status. | Check host, that the panel is reachable, and the panel’s own logs. |
parse /mod_mu/nodes/1/info |
The panel answered with something that is not the JSON envelope, such as an HTML page. | Check that host points at the panel itself, including any sub-path. |
sspanel: Shadowsocks node type is not supported |
node_type = "Shadowsocks". |
Serve the node with another type, or use a panel that supports Shadowsocks nodes. |
custom_config is empty, disable custom config |
The panel is 2021.11 or later and the node has no custom_config. |
Fill in custom_config, or set disable_custom_config = true. |
invalid offset_port_node "" |
offset_port_node is missing or is not a port number. |
Set it to a string such as "443". |
parse sspanel custom_config |
A custom_config key has the wrong JSON type, for example offset_port_node written as a number. |
Write string keys as strings and enable_reality as a boolean. |
no server info in response |
katana reads the legacy string and the node’s server field is empty. |
Fill in server, or use custom_config on SSPanel 2021.11 or later. |
malformed legacy v2ray server string: "…" |
The legacy string has fewer than six ;-separated parts. |
Add the missing parts, even if they are empty. |
invalid legacy port "…" |
The second part of the legacy V2ray string is not a port number. | Put the listening port in the second part. |
invalid legacy trojan port "" |
The legacy Trojan string has no port=. |
Add port=443, or port=443#8443 for an inside port. |
sspanel: a hysteria2 node needs custom_config; the legacy server string cannot describe one |
A Hysteria 2 node on an old panel, or with disable_custom_config = true. |
Use custom_config, or describe the node locally with [node.hysteria].port. |
node requests kernel-unsupported feature: VLESS XTLS flow |
flow in custom_config, or vless_flow, is not empty. |
Clear the flow. |
node requests kernel-unsupported feature: REALITY |
enable_reality is true. |
Turn REALITY off for this node. |
node requests kernel-unsupported feature: transport "…" |
network names a transport katana does not serve, such as h2 or kcp. httpupgrade and splithttp produce their own variant of this message. |
Use tcp, ws or grpc. |
TLS node requires cert.mode = "file" |
The node has TLS and no local certificate is configured. A Hysteria 2 node logs hysteria2 node requires cert.mode = "file". |
Set mode = "file" with cert_file and key_file. |
TLS node requires cert.cert_file and cert.key_file |
mode = "file" is set but one of the paths is empty. |
Set both cert_file and key_file. |
panel returned port 0 |
The node’s port is 0. At startup the line reads node 1: panel returned port 0; retrying in …. A running node logs node 1: refreshed port is 0, keeping the last one instead, keeps its port and still applies the user list. |
Set a real port in SSPanel. |
Troubleshooting has more diagnostics.
When a node cannot start
Section titled “When a node cannot start”A node whose first start fails, because it could not fetch the node info or the user list or could not build the listener, logs the error and tries again. The first retry waits 1 second, and each failure after that doubles the wait up to 60 seconds, or up to update_periodic if that is shorter. Each attempt forgets the ETags and reads the node info and the user list in full:
ERROR katana::manager::node: node 1: node_info failed: GET /mod_mu/nodes/1/info; retrying in 1sERROR katana::manager::node: node 1: initial start failed: TLS node requires cert.mode = "file"; retrying in 2sA failed user list fetch logs user_list failed: … in the same way. Fix the cause in the panel or in the config file. An edit to the config file makes the node try again at once; for a fix in the panel, the node picks it up on its next attempt.
Once a node has started, a failed poll only logs a warning and the node keeps serving its last known state. If the panel later describes the node in a way katana cannot build, katana logs rebuild failed, the node stops listening, and katana tries again on every poll until the description can be built.