feat: 优化 inbound 的方法

This commit is contained in:
chuan
2026-05-27 15:30:41 +08:00
Unverified
parent f97f01ec52
commit 2f84365db4
12 changed files with 179 additions and 43 deletions
+2 -2
View File
@@ -20,7 +20,7 @@ UI 中有些字段隐藏但仍存在于 `settings.toml`。下表的“UI”列
| 分类 | 文档 | 主要影响 |
| --- | --- | --- |
| 核心 | [config/core.md](config/core.md) | Xray 日志、Mux、TCP Fast Open。 |
| 入站 | [config/inbounds.md](config/inbounds.md) | SOCKS/HTTP/rule/API/自定义入站。 |
| 入站 | [config/inbounds.md](config/inbounds.md) | Mixed/rule/API/自定义入站。 |
| 路由 | [config/routing.md](config/routing.md) | rule 入站和透明代理流量的分流策略。 |
| DNS | [config/dns.md](config/dns.md) | Xray DNS 模块、本地 DNS 入站、DNS 服务器路由。 |
| 透明代理 | [config/transparent.md](config/transparent.md) | transparent inbound、iptables/nft、resolv、TinyTun。 |
@@ -45,7 +45,7 @@ UI 中有些字段隐藏但仍存在于 `settings.toml`。下表的“UI”列
| 设置 | 默认值 | 说明 |
| --- | --- | --- |
| `core.log_level` | `info` | 日常日志等级。 |
| `inbounds.rule_http_port` | `20172` | 规则 HTTP 代理入口。 |
| `inbounds.rule_http_port` | `20172` | 规则 Mixed 代理入口。 |
| `routing.mode` | `whitelist` | 国内/私有直连,其它代理。 |
| `transparent.mode` | `close` | 默认不启用透明代理。 |
| `transparent.type` | `redirect` | 推荐先用 redirect。 |
+45 -3
View File
@@ -8,8 +8,10 @@
| `port_sharing` | 隐藏 | `false` | `bool` | 为 `true` 时监听地址强制变成 `0.0.0.0`。 | 当前 UI 用 `listen` 控制,通常不改。 |
| `socks_port` | 隐藏 | `20170` | `0-65535` | 普通 SOCKS 入站,流量最终兜底走 `proxy`。UI 保存时写 `0`。 | 需要无规则 SOCKS 入口时手改。 |
| `http_port` | 隐藏 | `20171` | `0-65535` | 普通 HTTP 入站,流量最终兜底走 `proxy`。UI 保存时写 `0`。 | 需要无规则 HTTP 入口时手改。 |
| `rule_socks_port` | 显示 | `0` | `0-65535` | 规则 SOCKS 入站,流量按 `[routing]` 规则分流。UI 显示值会在为 `0` 时回退展示 `socks_port`。 | 应用要用 SOCKS 并希望按规则分流时设置。 |
| `rule_http_port` | 显示 | `20172` | `0-65535` | 规则 HTTP 入站,流量按 `[routing]` 规则分流。 | 浏览器/系统显式 HTTP 代理时使用。 |
| `rule_socks_port` | 隐藏 | `0` | `0-65535` | 旧版规则 SOCKS 入站兼容字段。当前 UI 保存时写 `0`。 | 通常不改。 |
| `rule_http_port` | 显示为 Mixed 端口 | `20172` | `0-65535` | 规则 mixed 入站,同一个端口同时支持 HTTP 和 SOCKS,流量按 `[routing]` 规则分流。 | 浏览器系统代理、CLI 工具显式代理时使用。 |
| `auth_user` | 显示 | `""` | 字符串 | mixed/SOCKS 入站认证用户名。 | 需要给局域网开放代理但不想裸奔时设置。 |
| `auth_password` | 显示 | `""` | 字符串 | mixed/SOCKS 入站认证密码。 | 与 `auth_user` 一起设置;两者都为空时使用 `noauth`。 |
| `vmess_port` | 隐藏 | `0` | `0-65535` | 额外 VMess 入站。`0` 不生成。 | 当前很少需要。 |
| `inbound_sniffing` | 显示 | `http,tls,quic` | `disable` / `http,tls` / `http,tls,quic` | 写入每个支持入站的 `sniffing.destOverride`。 | 域名路由不准时保持开启;兼容性异常时降级或关闭。 |
| `route_only` | 显示 | `false` | `bool` | 写入 `sniffing.routeOnly`。 | 只希望嗅探域名用于路由、不改连接目标时开启。 |
@@ -31,6 +33,46 @@
| 入站 tag | 来源 | 路由行为 |
| --- | --- | --- |
| `socks` / `http` | `socks_port` / `http_port` | 不进入 `[routing]` 模式规则,最终兜底走 `proxy`。 |
| `rule-socks` / `rule-http` | `rule_socks_port` / `rule_http_port` | 进入 `[routing]` 模式规则。 |
| `rule-mixed` | `rule_http_port` | 同一端口支持 HTTP 和 SOCKS进入 `[routing]` 模式规则。 |
| `vmess` | `vmess_port` | 额外 VMess 入站。 |
| `api-in` | `api.port` | 路由到 `api-out`。 |
## Mixed 入站
`rule-mixed` 生成示例:
```json
{
"tag": "rule-mixed",
"listen": "0.0.0.0",
"port": 20172,
"protocol": "mixed",
"settings": {
"auth": "noauth",
"udp": true,
"allowTransparent": false
}
}
```
如果填写用户名和密码:
```json
"settings": {
"auth": "password",
"udp": true,
"allowTransparent": false,
"accounts": [
{"user": "alice", "pass": "secret"}
]
}
```
使用方式:
```sh
curl -x http://10.11.11.100:20172 http://google.com/
curl --socks5-hostname 10.11.11.100:20172 https://google.com/
curl -x http://alice:secret@10.11.11.100:20172 http://google.com/
curl --socks5-hostname alice:secret@10.11.11.100:20172 https://google.com/
```
+1 -1
View File
@@ -167,7 +167,7 @@ tags = ["google"]
| 问题 | 原因 | 处理 |
| --- | --- | --- |
| 规则没生效 | 入口不是 rule 入站或透明代理入站。 | 使用 `rule_http_port` / `rule_socks_port`,或开启透明代理。 |
| 规则没生效 | 入口不是 rule 入站或透明代理入站。 | 使用 `rule_http_port` 对应的 mixed 端口,或开启透明代理。 |
| 域名规则没命中 | 流量只有 IP,没有域名。 | 开启 sniffing,或改用 `ip(...)` 规则。 |
| IP 规则导致 DNS 查询 | 当前 pyxray 生成 `domainStrategy = "IPOnDemand"`。 | 避免过度使用 IP 规则,或接受 Xray 为路由进行 DNS 解析。 |
| `routing_a` 里的 `default:` 无效 | pyxray 解析器只识别 `domain(...)``ip(...)`。 | 用 `default_rule` 设置兜底。 |
+30 -3
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
import os
import stat
import urllib.parse
import urllib.request
import zipfile
from dataclasses import dataclass
@@ -202,13 +203,39 @@ def _open_url(url: str, proxy_url: str | None):
"""打开 URL;指定代理时使用该代理,否则走系统默认代理配置。"""
if proxy_url:
opener = urllib.request.build_opener(
urllib.request.ProxyHandler({"http": proxy_url, "https": proxy_url})
)
opener = _proxy_opener(proxy_url)
return opener.open(url)
return urllib.request.urlopen(url) # noqa: S310
def _proxy_opener(proxy_url: str):
parsed = urllib.parse.urlsplit(proxy_url)
if not parsed.username:
return urllib.request.build_opener(urllib.request.ProxyHandler({"http": proxy_url, "https": proxy_url}))
clean_proxy_url = _proxy_url_without_auth(parsed)
password_manager = urllib.request.HTTPPasswordMgrWithDefaultRealm()
password_manager.add_password(
None,
clean_proxy_url,
urllib.parse.unquote(parsed.username),
urllib.parse.unquote(parsed.password or ""),
)
return urllib.request.build_opener(
urllib.request.ProxyHandler({"http": clean_proxy_url, "https": clean_proxy_url}),
urllib.request.ProxyBasicAuthHandler(password_manager),
urllib.request.ProxyDigestAuthHandler(password_manager),
)
def _proxy_url_without_auth(parsed: urllib.parse.SplitResult) -> str:
host = parsed.hostname or ""
if ":" in host and not host.startswith("["):
host = f"[{host}]"
netloc = f"{host}:{parsed.port}" if parsed.port else host
return urllib.parse.urlunsplit((parsed.scheme, netloc, parsed.path, parsed.query, parsed.fragment))
def _extract_from_zip(data: bytes, directory: Path, names: set[str]) -> None:
"""从 zip 中按文件名提取需要的文件,忽略目录层级。"""
+24 -4
View File
@@ -65,8 +65,7 @@ def _build_inbounds(settings: XrayConfigSettings) -> list[dict[str, Any]]:
inbounds = [
_socks_inbound(settings.inbounds.socks_port, listen, "socks", settings),
_http_inbound(settings.inbounds.http_port, listen, "http", settings),
_socks_inbound(settings.inbounds.rule_socks_port, listen, "rule-socks", settings),
_http_inbound(settings.inbounds.rule_http_port, listen, "rule-http", settings),
_mixed_inbound(settings.inbounds.rule_http_port, listen, "rule-mixed", settings),
_vmess_inbound(settings.inbounds.vmess_port, listen, settings),
]
for custom in settings.inbounds.custom:
@@ -82,7 +81,7 @@ def _build_inbounds(settings: XrayConfigSettings) -> list[dict[str, Any]]:
def _socks_inbound(port: int, listen: str, tag: str, settings: XrayConfigSettings) -> dict[str, Any]:
return _with_sniffing(
{"port": port, "listen": listen, "protocol": "socks", "settings": {"auth": "noauth", "udp": True}, "tag": tag},
{"port": port, "listen": listen, "protocol": "socks", "settings": _inbound_proxy_settings(settings), "tag": tag},
settings,
)
@@ -91,6 +90,19 @@ def _http_inbound(port: int, listen: str, tag: str, settings: XrayConfigSettings
return _with_sniffing({"port": port, "listen": listen, "protocol": "http", "tag": tag}, settings)
def _mixed_inbound(port: int, listen: str, tag: str, settings: XrayConfigSettings) -> dict[str, Any]:
return _with_sniffing(
{
"port": port,
"listen": listen,
"protocol": "mixed",
"settings": _inbound_proxy_settings(settings),
"tag": tag,
},
settings,
)
def _vmess_inbound(port: int, listen: str, settings: XrayConfigSettings) -> dict[str, Any]:
if port <= 0:
return {"port": 0}
@@ -101,6 +113,14 @@ def _vmess_inbound(port: int, listen: str, settings: XrayConfigSettings) -> dict
)
def _inbound_proxy_settings(settings: XrayConfigSettings) -> dict[str, Any]:
proxy_settings: dict[str, Any] = {"auth": "noauth", "udp": True, "allowTransparent": False}
if settings.inbounds.auth_user and settings.inbounds.auth_password:
proxy_settings["auth"] = "password"
proxy_settings["accounts"] = [{"user": settings.inbounds.auth_user, "pass": settings.inbounds.auth_password}]
return proxy_settings
def _transparent_inbounds(settings: XrayConfigSettings) -> list[dict[str, Any]]:
if settings.transparent.mode == "close":
return []
@@ -239,7 +259,7 @@ def _build_dns_routing(settings: XrayConfigSettings) -> list[dict[str, Any]]:
def _build_rule_port_routing(settings: XrayConfigSettings) -> list[dict[str, Any]]:
tags = [tag for tag, port in (("rule-http", settings.inbounds.rule_http_port), ("rule-socks", settings.inbounds.rule_socks_port)) if port > 0]
tags = ["rule-mixed"] if settings.inbounds.rule_http_port > 0 else []
if not tags:
return []
mode = settings.routing.mode
+4
View File
@@ -43,6 +43,8 @@ class InboundSettings:
http_port: int = 20171
rule_socks_port: int = 0
rule_http_port: int = 20172
auth_user: str = ""
auth_password: str = ""
vmess_port: int = 0
inbound_sniffing: str = "http,tls,quic"
route_only: bool = False
@@ -202,6 +204,8 @@ def validate_settings(settings: XrayConfigSettings) -> None:
if not 1 <= settings.core.mux_concurrency <= 1024:
raise ValueError("core.mux_concurrency must be between 1 and 1024")
_validate_choice(settings.inbounds.inbound_sniffing, {"disable", "http,tls", "http,tls,quic"}, "inbounds.inbound_sniffing")
if bool(settings.inbounds.auth_user) != bool(settings.inbounds.auth_password):
raise ValueError("inbounds.auth_user and inbounds.auth_password must be set together")
_validate_choice(settings.routing.mode, {"whitelist", "gfwlist", "custom", "routingA", "proxy", "direct", "block"}, "routing.mode")
_validate_choice(settings.routing.default_rule, {"direct", "proxy", "block"}, "routing.default_rule")
_validate_choice(settings.transparent.mode, {"close", "proxy", "whitelist", "gfwlist", "pac"}, "transparent.mode")
+1 -1
View File
@@ -292,7 +292,7 @@ def _inbound_networks(inbound: dict) -> set[str]:
return {item.strip() for item in network.split(",") if item.strip() in {"tcp", "udp"}}
if protocol == "dokodemo-door":
return {"tcp", "udp"}
if protocol == "socks" and (inbound.get("settings") or {}).get("udp"):
if protocol in {"socks", "mixed"} and (inbound.get("settings") or {}).get("udp"):
return {"tcp", "udp"}
return {"tcp"}
+19 -12
View File
@@ -1,22 +1,28 @@
<section class="config-card">
<h3 class="config-card-title">入站端口</h3>
<label class="config-field">
<span>监听地址</span>
<select name="inbounds.listen">
{% for value in ["127.0.0.1", "0.0.0.0"] %}
<option value="{{ value }}" {% if settings.inbounds.listen == value %}selected{% endif %}>{{ value }}</option>
{% endfor %}
</select>
</label>
<div class="config-grid">
<label class="config-field">
<span>监听地址</span>
<select name="inbounds.listen">
{% for value in ["127.0.0.1", "0.0.0.0"] %}
<option value="{{ value }}" {% if settings.inbounds.listen == value %}selected{% endif %}>{{ value }}</option>
{% endfor %}
</select>
</label>
<label class="config-field">
<span>Mixed 端口</span>
<input name="inbounds.rule_http_port" type="number" min="0" max="65535" value="{{ settings.inbounds.rule_http_port }}" />
</label>
</div>
<div class="config-grid mt-4">
<label class="config-field">
<span>规则 SOCKS 端口</span>
<input name="inbounds.rule_socks_port" type="number" min="0" max="65535" value="{{ settings.inbounds.rule_socks_port or settings.inbounds.socks_port }}" />
<span>认证用户名</span>
<input name="inbounds.auth_user" type="text" autocomplete="username" value="{{ settings.inbounds.auth_user }}" placeholder="留空使用 noauth" />
</label>
<label class="config-field">
<span>规则 HTTP 端口</span>
<input name="inbounds.rule_http_port" type="number" min="0" max="65535" value="{{ settings.inbounds.rule_http_port }}" />
<span>认证密码</span>
<input name="inbounds.auth_password" type="password" autocomplete="current-password" value="{{ settings.inbounds.auth_password }}" placeholder="留空使用 noauth" />
</label>
</div>
@@ -40,6 +46,7 @@
<input name="inbounds.socks_port" type="hidden" value="0" />
<input name="inbounds.http_port" type="hidden" value="0" />
<input name="inbounds.rule_socks_port" type="hidden" value="0" />
<input name="inbounds.vmess_port" type="hidden" value="0" />
<input name="inbounds.api.port" type="hidden" value="0" />
<input name="inbounds.port_sharing" type="hidden" value="off" />
@@ -35,7 +35,7 @@
<label class="grid gap-2">
<span class="text-sm text-zinc-400">下载代理</span>
<input class="rounded-2xl border border-zinc-700 bg-zinc-950 px-4 py-3 outline-none focus:border-zinc-400" name="proxy_url" placeholder="空则使用系统代理;系统没有代理则直连" value="{{ form.proxy_url }}" />
<input class="rounded-2xl border border-zinc-700 bg-zinc-950 px-4 py-3 outline-none focus:border-zinc-400" name="proxy_url" placeholder="例如 http://user:pass@127.0.0.1:20172空则使用系统代理" value="{{ form.proxy_url }}" />
</label>
<label class="grid gap-2">
+2
View File
@@ -149,6 +149,8 @@ def _settings_from_request() -> XrayConfigSettings:
settings.inbounds.http_port = _int("inbounds.http_port", settings.inbounds.http_port)
settings.inbounds.rule_socks_port = _int("inbounds.rule_socks_port", settings.inbounds.rule_socks_port)
settings.inbounds.rule_http_port = _int("inbounds.rule_http_port", settings.inbounds.rule_http_port)
settings.inbounds.auth_user = form.get("inbounds.auth_user", settings.inbounds.auth_user).strip()
settings.inbounds.auth_password = form.get("inbounds.auth_password", settings.inbounds.auth_password).strip()
settings.inbounds.vmess_port = _int("inbounds.vmess_port", settings.inbounds.vmess_port)
settings.inbounds.inbound_sniffing = form.get("inbounds.inbound_sniffing", settings.inbounds.inbound_sniffing)
settings.inbounds.route_only = _enabled("inbounds.route_only")
+42 -15
View File
@@ -80,6 +80,8 @@ def test_settings_defaults_match_v2raya_core_values() -> None:
assert settings.inbounds.http_port == 20171
assert settings.inbounds.rule_socks_port == 0
assert settings.inbounds.rule_http_port == 20172
assert settings.inbounds.auth_user == ""
assert settings.inbounds.auth_password == ""
assert settings.transparent.mode == "close"
assert settings.transparent.type == "redirect"
assert settings.transparent.port == 52345
@@ -103,7 +105,10 @@ def test_generate_default_config_matches_v2raya_template_shape() -> None:
assert _inbound(config, "socks")["protocol"] == "socks"
assert _inbound(config, "socks")["settings"]["udp"] is True
assert _inbound(config, "http")["port"] == 20171
assert _inbound(config, "rule-http")["port"] == 20172
assert _inbound(config, "rule-mixed")["port"] == 20172
assert _inbound(config, "rule-mixed")["protocol"] == "mixed"
assert _inbound(config, "rule-mixed")["settings"] == {"auth": "noauth", "udp": True, "allowTransparent": False}
assert "rule-http" not in {item["tag"] for item in config["inbounds"]}
assert "rule-socks" not in {item["tag"] for item in config["inbounds"]}
assert _outbound(config, "proxy")["protocol"] == "shadowsocks"
assert _outbound(config, "direct")["protocol"] == "freedom"
@@ -117,6 +122,22 @@ def test_generate_default_config_matches_v2raya_template_shape() -> None:
assert "dns-in" not in {item["tag"] for item in config["inbounds"]}
def test_generate_mixed_inbound_with_password_auth() -> None:
settings = XrayConfigSettings()
settings.inbounds.auth_user = "alice"
settings.inbounds.auth_password = "secret"
mixed = _inbound(generate_xray_config(parse_node_link(_ss_link()), settings), "rule-mixed")
assert mixed["protocol"] == "mixed"
assert mixed["settings"] == {
"auth": "password",
"udp": True,
"allowTransparent": False,
"accounts": [{"user": "alice", "pass": "secret"}],
}
def test_generate_none_log_level_disables_xray_logs() -> None:
settings = XrayConfigSettings()
settings.core.log_level = "none"
@@ -291,7 +312,6 @@ def test_generate_vless_ws_early_data_grpc_and_xhttp_settings() -> None:
def test_generate_inbounds_custom_api_and_transparent_tproxy() -> None:
settings = XrayConfigSettings()
settings.inbounds.port_sharing = True
settings.inbounds.rule_socks_port = 20173
settings.inbounds.vmess_port = 20174
settings.inbounds.api.port = 20175
settings.inbounds.custom.append(CustomInboundSettings(tag="extra-http", protocol="http", port=20176))
@@ -301,7 +321,8 @@ def test_generate_inbounds_custom_api_and_transparent_tproxy() -> None:
config = generate_xray_config(parse_node_link(_ss_link()), settings)
assert _inbound(config, "socks")["listen"] == "0.0.0.0"
assert _inbound(config, "rule-socks")["port"] == 20173
assert _inbound(config, "rule-mixed")["listen"] == "0.0.0.0"
assert _inbound(config, "rule-mixed")["port"] == 20172
assert _inbound(config, "vmess")["protocol"] == "vmess"
assert _inbound(config, "extra-http")["protocol"] == "http"
assert _inbound(config, "transparent")["streamSettings"]["sockopt"]["tproxy"] == "tproxy"
@@ -312,15 +333,14 @@ def test_generate_inbounds_custom_api_and_transparent_tproxy() -> None:
def test_generate_routing_modes_follow_v2raya_rules() -> None:
settings = XrayConfigSettings()
settings.inbounds.rule_socks_port = 20173
whitelist = generate_xray_config(parse_node_link(_ss_link()), settings)["routing"]["rules"]
assert {"type": "field", "inboundTag": ["rule-http", "rule-socks"], "domain": ["geosite:cn"], "outboundTag": "direct"} in whitelist
assert {"type": "field", "inboundTag": ["rule-http", "rule-socks"], "domain": ["geosite:google"], "outboundTag": "proxy"} in whitelist
assert {"type": "field", "inboundTag": ["rule-mixed"], "domain": ["geosite:cn"], "outboundTag": "direct"} in whitelist
assert {"type": "field", "inboundTag": ["rule-mixed"], "domain": ["geosite:google"], "outboundTag": "proxy"} in whitelist
settings.routing.mode = "gfwlist"
gfwlist = generate_xray_config(parse_node_link(_ss_link()), settings)["routing"]["rules"]
assert {"type": "field", "inboundTag": ["rule-http", "rule-socks"], "outboundTag": "direct"} in gfwlist
assert {"type": "field", "inboundTag": ["rule-mixed"], "outboundTag": "direct"} in gfwlist
assert any("91.108.4.0/22" in rule.get("ip", []) for rule in gfwlist)
@@ -331,8 +351,8 @@ def test_generate_custom_routing_a_applies_before_proxy_mode_fallback() -> None:
rules = generate_xray_config(parse_node_link(_ss_link()), settings)["routing"]["rules"]
google_rule = {"type": "field", "inboundTag": ["rule-http"], "outboundTag": "direct", "domain": ["keyword:google"]}
fallback = {"type": "field", "inboundTag": ["rule-http"], "outboundTag": "proxy"}
google_rule = {"type": "field", "inboundTag": ["rule-mixed"], "outboundTag": "direct", "domain": ["keyword:google"]}
fallback = {"type": "field", "inboundTag": ["rule-mixed"], "outboundTag": "proxy"}
assert google_rule in rules
assert fallback in rules
assert rules.index(google_rule) < rules.index(fallback)
@@ -340,7 +360,6 @@ def test_generate_custom_routing_a_applies_before_proxy_mode_fallback() -> None:
def test_generate_custom_text_rules_before_route_mode_and_default_proxy() -> None:
settings = XrayConfigSettings()
settings.inbounds.rule_socks_port = 20173
settings.routing.mode = "whitelist"
settings.routing.default_rule = "proxy"
settings.routing.routing_a = "domain(geosite:google, domain:example.com)->proxy\nip(geoip:cn)->direct"
@@ -348,20 +367,20 @@ def test_generate_custom_text_rules_before_route_mode_and_default_proxy() -> Non
rules = generate_xray_config(parse_node_link(_ss_link()), settings)["routing"]["rules"]
custom_domain = {
"type": "field",
"inboundTag": ["rule-http", "rule-socks"],
"inboundTag": ["rule-mixed"],
"domain": ["geosite:google", "domain:example.com"],
"outboundTag": "proxy",
}
custom_ip = {
"type": "field",
"inboundTag": ["rule-http", "rule-socks"],
"inboundTag": ["rule-mixed"],
"ip": ["geoip:cn"],
"outboundTag": "direct",
}
assert rules.index(custom_domain) < rules.index({"type": "field", "inboundTag": ["rule-http", "rule-socks"], "domain": ["geosite:cn"], "outboundTag": "direct"})
assert rules.index(custom_ip) < rules.index({"type": "field", "inboundTag": ["rule-http", "rule-socks"], "ip": ["geoip:private", "geoip:cn"], "outboundTag": "direct"})
assert {"type": "field", "inboundTag": ["rule-http", "rule-socks"], "outboundTag": "proxy"} in rules
assert rules.index(custom_domain) < rules.index({"type": "field", "inboundTag": ["rule-mixed"], "domain": ["geosite:cn"], "outboundTag": "direct"})
assert rules.index(custom_ip) < rules.index({"type": "field", "inboundTag": ["rule-mixed"], "ip": ["geoip:private", "geoip:cn"], "outboundTag": "direct"})
assert {"type": "field", "inboundTag": ["rule-mixed"], "outboundTag": "proxy"} in rules
def test_generate_dns_rules_and_dns_routing() -> None:
@@ -392,6 +411,14 @@ def test_validate_settings_rejects_invalid_values() -> None:
validate_settings(settings)
def test_validate_settings_requires_complete_inbound_auth() -> None:
settings = XrayConfigSettings()
settings.inbounds.auth_user = "alice"
with pytest.raises(ValueError, match="auth_user"):
validate_settings(settings)
def _ss_link() -> str:
user = base64.urlsafe_b64encode(b"chacha20-ietf-poly1305:secret").decode().rstrip("=")
return f"ss://{user}@ss.example.net:8388#ss-node"
+8 -1
View File
@@ -648,6 +648,8 @@ def test_xray_config_api_saves_settings_from_form_controls(tmp_path: Path) -> No
"inbounds.http_port": "0",
"inbounds.rule_socks_port": "0",
"inbounds.rule_http_port": "20181",
"inbounds.auth_user": "alice",
"inbounds.auth_password": "secret",
"inbounds.vmess_port": "0",
"inbounds.inbound_sniffing": "http,tls",
"inbounds.route_only": "on",
@@ -685,11 +687,16 @@ def test_xray_config_api_saves_settings_from_form_controls(tmp_path: Path) -> No
assert "log_level = \"error\"" in saved.get_json()["settings_toml"]
assert "socks_port = 0" in saved.get_json()["settings_toml"]
assert "http_port = 0" in saved.get_json()["settings_toml"]
assert "auth_user = \"alice\"" in saved.get_json()["settings_toml"]
assert "auth_password = \"secret\"" in saved.get_json()["settings_toml"]
assert "route_only = true" in saved.get_json()["settings_toml"]
assert "output_bypass_rules = \"tcp 117.72.47.28:33010\"" in saved.get_json()["settings_toml"]
assert generated.status_code == 200
assert config["log"]["loglevel"] == "error"
assert config["inbounds"][0]["port"] == 20180
assert any(inbound["tag"] == "rule-mixed" and inbound["port"] == 20181 for inbound in config["inbounds"])
assert next(inbound for inbound in config["inbounds"] if inbound["tag"] == "rule-mixed")["settings"]["accounts"] == [
{"user": "alice", "pass": "secret"}
]
assert config["outbounds"][0]["mux"] == {"enabled": True, "concurrency": 16}
assert config["dns"]["queryStrategy"] == "UseIPv4"