feat: 优化 inbound 的方法
This commit is contained in:
+2
-2
@@ -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
@@ -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/
|
||||
```
|
||||
|
||||
@@ -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` 设置兜底。 |
|
||||
|
||||
@@ -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 中按文件名提取需要的文件,忽略目录层级。"""
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user