Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export CLASH_CONTROLLER_SECRET=""
export MIXED_PORT="7890"
export EXTERNAL_CONTROLLER="0.0.0.0:9090"
export CLASH_DNS_PORT="1053"
# 保留订阅自身的 dns.listen。设为 true 后不会用 CLASH_DNS_PORT 覆盖,也不会自动改写该端口;请确保端口可用。
export CLASH_PRESERVE_DNS_LISTEN="false"

# 订阅更新策略
export CLASH_AUTO_UPDATE_SUBSCRIPTIONS="true"
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ OpenWrt 下 root/system 安装会把 `clash`、`clashon`、`clashoff` 等命令

### 多订阅管理

订阅地址包含 `&` 等 Shell 特殊字符时,请用引号包裹完整地址;也可以直接运行 `clash add`,按提示交互式粘贴地址。例如:`clash add 'https://example.com/sub?target=clash&emoji=true' west`。

```
clash add <订阅链接> <名称>
clash use
Expand Down Expand Up @@ -337,6 +339,8 @@ Control 层负责把常用动作收口成可理解的命令和反馈。
KERNEL_TYPE=mihomo
MIXED_PORT=7890
EXTERNAL_CONTROLLER=0.0.0.0:9090
CLASH_DNS_PORT=1053
CLASH_PRESERVE_DNS_LISTEN=false
CLASH_CONTROLLER_SECRET=your-secret
CLASH_SUBSCRIPTION_URL=https://example.com/sub
MIHOMO_VERSION=v1.19.23
Expand All @@ -355,6 +359,7 @@ CLASH_IPV6=auto
按需设置即可,不需要每项都写。

- `CLASH_SHELL_AUTO_RESTORE_PROXY`:控制登录 Shell 是否自动恢复上次 `clashon` 写入的代理变量。默认 `true` 保持兼容;如果不希望 SSH 远程登录后自动带上 `http_proxy` / `https_proxy`,设为 `false`,之后仍可手动执行 `clashon`。
- `CLASH_PRESERVE_DNS_LISTEN`:设为 `true` 时保留活动订阅中的 `.dns.listen`,不再用 `CLASH_DNS_PORT` 覆盖,也不会因端口占用自动换成其他 DNS 端口;如果订阅没有有效的 `.dns.listen`,仍回退到 `CLASH_DNS_PORT`。请先确认保留的端口未被其他服务占用。
- `CLASH_PREDOWNLOAD_GEO`:控制安装期是否预下载 GEO 数据。默认 `true`,会提前下载 `Country.mmdb`、`geoip.metadb`、`GeoIP.dat`、`GeoSite.dat` 等规则分流常用资源;临时部署、只想先跑起来时可设为 `false` 跳过安装期预下载。注意:当最终运行配置实际使用 `GEOIP` 规则时,启动前仍会按需准备 `Country.mmdb`,否则 Mihomo 无法可靠加载该配置。
- `CLASH_IPV6`:可设为 `true`、`false` 或 `auto`。`true` 同时启用内核 IPv6 与 DNS AAAA,适用于 IPv6-only 节点;`auto` 保留订阅中的 IPv6 设置。
- `CLASH_SUBSCRIPTION_UA`:覆盖拉取订阅时的 User-Agent。默认使用 `clash-verge/v2.4.0`,以便订阅服务返回 Mihomo 支持的 Hysteria2、AnyTLS 等现代协议节点;如果服务商有专用 UA 要求,可在此显式设置。
Expand Down
5 changes: 4 additions & 1 deletion scripts/core/clashctl.sh
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,10 @@ ensure_runtime_ports_ready() {
info "检测到 external-controller 冲突:${controller_port} -> ${new_controller_port}"
fi

if [ -n "${dns_port:-}" ] && [ "$dns_port" != "null" ] && is_port_in_use "$dns_port"; then
if [ "$(config_preserve_dns_listen)" != "true" ] \
&& [ -n "${dns_port:-}" ] \
&& [ "$dns_port" != "null" ] \
&& is_port_in_use "$dns_port"; then
new_dns_port="$(resolve_free_port 1053 1999)"
write_env_value "CLASH_DNS_PORT" "$new_dns_port"
write_runtime_value "INSTALL_PLAN_DNS_PORT" "$new_dns_port"
Expand Down
53 changes: 49 additions & 4 deletions scripts/core/config.sh
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,10 @@ config_bool_env_value() {
esac
}

config_preserve_dns_listen() {
config_bool_env_value "CLASH_PRESERVE_DNS_LISTEN" "false"
}

config_ipv6_mode() {
local value

Expand Down Expand Up @@ -842,7 +846,7 @@ normalize_runtime_config() {
local mixed_port controller tun_enable_value tun_stack_value dns_port_value controller_secret_value
local tun_auto_route_value tun_auto_redirect_value tun_strict_route_value tun_dns_hijack_value
local dashboard_dir_value dashboard_url_value allow_lan_value ipv6_mode_value
local ipv6_value dns_ipv6_value
local ipv6_value dns_ipv6_value dns_listen_value
local resolved_ports err_file output

[ -s "$file" ] || die "待规范化的配置文件不存在:$file"
Expand All @@ -859,6 +863,11 @@ normalize_runtime_config() {
tun_strict_route_value="$(tun_strict_route)"
tun_dns_hijack_value="$(tun_dns_hijack)"
dns_port_value="$CLASH_DNS_PORT_RESOLVED"
dns_listen_value="0.0.0.0:${dns_port_value}"
if [ "$(config_preserve_dns_listen)" = "true" ]; then
dns_listen_value="$(config_dns_listen_value "$file" 2>/dev/null || true)"
[ -n "${dns_listen_value:-}" ] || dns_listen_value="0.0.0.0:${dns_port_value}"
fi
controller_secret_value="$(ensure_controller_secret)"
dashboard_dir_value="$(runtime_dashboard_dir)"
dashboard_url_value="$DEFAULT_DASHBOARD_UI_URL"
Expand Down Expand Up @@ -893,7 +902,7 @@ normalize_runtime_config() {
controller_secret_value="$controller_secret_value" \
dashboard_dir_value="$dashboard_dir_value" \
dashboard_url_value="$dashboard_url_value" \
dns_listen_value="0.0.0.0:${dns_port_value}" \
dns_listen_value="$dns_listen_value" \
"$(yq_bin)" eval -i '
.["mixed-port"] = (env(mixed_port) | tonumber) |
del(.port, .["socks-port"], .["redir-port"], .["tproxy-port"]) |
Expand Down Expand Up @@ -1898,6 +1907,20 @@ is_valid_port_number() {
[ "$port" -ge 1 ] && [ "$port" -le 65535 ]
}

config_dns_listen_value() {
local file="${1:-}"
local listen port

[ -s "$file" ] || return 1
listen="$("$(yq_bin)" eval '.dns.listen // ""' "$file" 2>/dev/null | head -n 1)"
[ -n "${listen:-}" ] || return 1
[ "$listen" != "null" ] || return 1

port="${listen##*:}"
is_valid_port_number "$port" || return 1
printf '%s\n' "$listen"
}

csv_has_value() {
local csv="$1"
local value="$2"
Expand Down Expand Up @@ -2099,7 +2122,7 @@ load_resolved_runtime_ports() {
resolve_runtime_ports() {
# Optional: path to a config file to read port from when MIXED_PORT is not set
local _hint_config_file="${1:-}"
local preferred_mixed preferred_controller preferred_dns
local preferred_mixed preferred_controller preferred_dns preserved_dns_listen
local controller_host preferred_controller_port
local mixed_port controller_port dns_port
local used_ports=""
Expand All @@ -2116,6 +2139,16 @@ resolve_runtime_ports() {
preferred_mixed="${preferred_mixed:-7890}"
preferred_controller="${EXTERNAL_CONTROLLER:-0.0.0.0:9090}"
preferred_dns="${CLASH_DNS_PORT:-1053}"
preserved_dns_listen=""

if [ "$(config_preserve_dns_listen)" = "true" ] \
&& [ -n "${_hint_config_file:-}" ] \
&& [ -s "${_hint_config_file:-}" ]; then
preserved_dns_listen="$(config_dns_listen_value "$_hint_config_file" 2>/dev/null || true)"
if [ -n "${preserved_dns_listen:-}" ]; then
preferred_dns="${preserved_dns_listen##*:}"
fi
fi

is_valid_port_number "$preferred_mixed" || die "MIXED_PORT 不合法:$preferred_mixed"
is_valid_port_number "$preferred_dns" || die "CLASH_DNS_PORT 不合法:$preferred_dns"
Expand All @@ -2131,7 +2164,19 @@ resolve_runtime_ports() {
is_valid_port_number "$controller_port" || die "invalid external-controller resolution: $controller_port"
used_ports="$(csv_append_value "$used_ports" "$controller_port")"

dns_port="$(resolve_runtime_port "$preferred_dns" 1053 1199 "$used_ports" "dns.listen")"
if [ -n "${preserved_dns_listen:-}" ]; then
if csv_has_value "$used_ports" "$preferred_dns"; then
die "订阅保留的 DNS 端口与其他运行端口冲突:$preferred_dns"
fi

if is_port_in_use "$preferred_dns" && ! port_reserved_by_current_runtime "$preferred_dns"; then
die "订阅保留的 DNS 端口已被占用:$preferred_dns"
fi

dns_port="$preferred_dns"
else
dns_port="$(resolve_runtime_port "$preferred_dns" 1053 1199 "$used_ports" "dns.listen")"
fi
is_valid_port_number "$dns_port" || die "invalid dns.listen resolution: $dns_port"
used_ports="$(csv_append_value "$used_ports" "$dns_port")"

Expand Down
42 changes: 42 additions & 0 deletions scripts/dev/check-runtime-config-normalization.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,39 @@ source "$PROJECT_DIR/scripts/core/config.sh"
# Keep this regression test independent from the developer's project .env.
read_env_value() { return 1; }

# Keep port resolution deterministic for this test; the preservation path must
# not depend on whatever happens to be listening on the developer's machine.
is_port_in_use() { return 1; }
port_reserved_by_current_runtime() { return 1; }

preserved_port_config="$tmp_dir/preserved-dns-port-config.yaml"
cat > "$preserved_port_config" <<'YAML'
mixed-port: 7890
external-controller: 0.0.0.0:9090
dns:
listen: 127.0.0.1:7874
proxies: []
proxy-groups: []
rules: []
YAML

unset MIXED_PORT EXTERNAL_CONTROLLER CLASH_DNS_PORT CLASH_PRESERVE_DNS_LISTEN
default_port_resolution="$(resolve_runtime_ports "$preserved_port_config")"
if printf '%s\n' "$default_port_resolution" | grep -qx 'CLASH_DNS_PORT_RESOLVED=1053'; then
echo "ok - default DNS port remains configurable"
else
echo "not ok - default DNS port resolution changed unexpectedly: $default_port_resolution" >&2
exit 1
fi

preserved_port_resolution="$(CLASH_PRESERVE_DNS_LISTEN=true resolve_runtime_ports "$preserved_port_config")"
if printf '%s\n' "$preserved_port_resolution" | grep -qx 'CLASH_DNS_PORT_RESOLVED=7874'; then
echo "ok - preserves subscription DNS port during resolution"
else
echo "not ok - subscription DNS port was not preserved: $preserved_port_resolution" >&2
exit 1
fi

resolve_runtime_ports() {
printf 'MIXED_PORT_RESOLVED=7891\n'
printf 'EXTERNAL_CONTROLLER_RESOLVED=0.0.0.0:9090\n'
Expand Down Expand Up @@ -96,6 +129,7 @@ secret: old-secret
allow-lan: false
ipv6: true
dns:
listen: 127.0.0.1:7874
ipv6: true
proxies:
- name: hy2-ipv6
Expand All @@ -113,6 +147,9 @@ proxy-groups: []
rules: []
YAML

preserved_normalization_config="$tmp_dir/preserved-dns-normalization.yaml"
cp "$sample_config" "$preserved_normalization_config"

normalize_runtime_config "$sample_config"

assert_file_yq() {
Expand Down Expand Up @@ -143,11 +180,16 @@ assert_yq "removes legacy tproxy-port" 'has("tproxy-port")' "false"
assert_yq "keeps controller normalization" '.["external-controller"]' "0.0.0.0:9090"
assert_yq "preserves subscription IPv6" '.ipv6' "true"
assert_yq "preserves subscription DNS IPv6" '.dns.ipv6' "true"
assert_yq "normalizes DNS listen by default" '.dns.listen' "0.0.0.0:1053"
assert_yq "preserves Hysteria2 proxy type" '.proxies[0].type' "hysteria2"
assert_yq "preserves IPv6-only proxy server" '.proxies[0].server' "2001:db8::1"
assert_yq "preserves AnyTLS proxy type" '.proxies[1].type' "anytls"
assert_yq "preserves AnyTLS SNI" '.proxies[1].sni' "example.com"

CLASH_PRESERVE_DNS_LISTEN=true normalize_runtime_config "$preserved_normalization_config"
assert_file_yq "preserves subscription DNS listen when enabled" "$preserved_normalization_config" '.dns.listen' "127.0.0.1:7874"
unset CLASH_PRESERVE_DNS_LISTEN

CLASH_IPV6=false normalize_runtime_config "$sample_config"
assert_yq "IPv6 off disables kernel IPv6" '.ipv6' "false"
assert_yq "IPv6 off disables DNS IPv6" '.dns.ipv6' "false"
Expand Down