mirror of
https://github.com/mack-a/v2ray-agent.git
synced 2026-09-02 13:56:37 +00:00
feat(脚本): docker 脚本增加订阅
This commit is contained in:
+1
-1
@@ -9978,7 +9978,7 @@ menu() {
|
|||||||
cd "$HOME" || exit
|
cd "$HOME" || exit
|
||||||
echoContent red "\n=============================================================="
|
echoContent red "\n=============================================================="
|
||||||
echoContent green "作者:mack-a"
|
echoContent green "作者:mack-a"
|
||||||
echoContent green "当前版本:v3.5.18"
|
echoContent green "当前版本:v3.5.19"
|
||||||
echoContent green "Github:https://github.com/mack-a/v2ray-agent"
|
echoContent green "Github:https://github.com/mack-a/v2ray-agent"
|
||||||
echoContent green "描述:八合一共存脚本\c"
|
echoContent green "描述:八合一共存脚本\c"
|
||||||
showInstallStatus
|
showInstallStatus
|
||||||
|
|||||||
+590
-12
@@ -45,6 +45,8 @@ email=""
|
|||||||
generateOnly=0
|
generateOnly=0
|
||||||
startOnly=0
|
startOnly=0
|
||||||
skipSelfInstall=0
|
skipSelfInstall=0
|
||||||
|
subscribePort=""
|
||||||
|
resolvedSubscribePort=""
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# QA 覆盖:设置 V2RAY_AGENT_FORCE_NO_DOCKER=1 可在测试中跳过 Docker 检查
|
# QA 覆盖:设置 V2RAY_AGENT_FORCE_NO_DOCKER=1 可在测试中跳过 Docker 检查
|
||||||
@@ -74,6 +76,7 @@ showHelp() {
|
|||||||
echoContent "white" " --port <端口> Vision 或单协议模式的外部监听端口(留空则随机)"
|
echoContent "white" " --port <端口> Vision 或单协议模式的外部监听端口(留空则随机)"
|
||||||
echoContent "white" " --xhttp-port <端口> XHTTP 模式端口(留空则随机)"
|
echoContent "white" " --xhttp-port <端口> XHTTP 模式端口(留空则随机)"
|
||||||
echoContent "white" " --xhttp-path <路径> XHTTP path 基础值(最终写为 /<path>xHTTP)"
|
echoContent "white" " --xhttp-path <路径> XHTTP path 基础值(最终写为 /<path>xHTTP)"
|
||||||
|
echoContent "white" " --subscribe-port <端口> 订阅访问端口(留空则随机)"
|
||||||
echoContent "white" " --server-name <sni> Reality 握手所用的 SNI / 服务器名称"
|
echoContent "white" " --server-name <sni> Reality 握手所用的 SNI / 服务器名称"
|
||||||
echoContent "white" " --private-key <密钥> X25519 私钥(留空则自动生成)"
|
echoContent "white" " --private-key <密钥> X25519 私钥(留空则自动生成)"
|
||||||
echoContent "white" " --uuid <uuid> 客户端 UUID(留空则自动生成)"
|
echoContent "white" " --uuid <uuid> 客户端 UUID(留空则自动生成)"
|
||||||
@@ -85,7 +88,7 @@ showHelp() {
|
|||||||
echoContent "white" ""
|
echoContent "white" ""
|
||||||
echoContent "white" "操作说明:"
|
echoContent "white" "操作说明:"
|
||||||
echoContent "white" " 1. 交互模式会同时检测 config.json 与 v2ray-agent-docker 容器是否存在,再显示对应菜单。"
|
echoContent "white" " 1. 交互模式会同时检测 config.json 与 v2ray-agent-docker 容器是否存在,再显示对应菜单。"
|
||||||
echoContent "white" " 2. 若两者都不存在,则显示安装菜单;若任一存在,则显示查看账号/重新安装/启动或重建菜单。"
|
echoContent "white" " 2. 若两者都不存在,则显示安装菜单;若任一存在,则显示用户管理/重新安装/启动或重建菜单。"
|
||||||
echoContent "white" " 3. 安装时可选 Reality Vision、Reality XHTTP 或全部安装;XHTTP 会额外使用 path 输入。"
|
echoContent "white" " 3. 安装时可选 Reality Vision、Reality XHTTP 或全部安装;XHTTP 会额外使用 path 输入。"
|
||||||
echoContent "white" " 4. 非交互模式需要显式提供所有值,但 privateKey/uuid/email 留空时可自动生成/推导。"
|
echoContent "white" " 4. 非交互模式需要显式提供所有值,但 privateKey/uuid/email 留空时可自动生成/推导。"
|
||||||
echoContent "white" " 5. 运行数据目录默认为 /etc/v2ray-agent/docker/;可用 --data-dir 覆盖以用于 QA 或自定义路径。"
|
echoContent "white" " 5. 运行数据目录默认为 /etc/v2ray-agent/docker/;可用 --data-dir 覆盖以用于 QA 或自定义路径。"
|
||||||
@@ -199,6 +202,371 @@ renderXHTTPPath() {
|
|||||||
printf '/%sxHTTP' "$1"
|
printf '/%sxHTTP' "$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
urlEncode() {
|
||||||
|
local input="$1"
|
||||||
|
local length=${#input}
|
||||||
|
local index char
|
||||||
|
|
||||||
|
for ((index = 0; index < length; index++)); do
|
||||||
|
char="${input:index:1}"
|
||||||
|
case "${char}" in
|
||||||
|
[a-zA-Z0-9.~_-]) printf '%s' "${char}" ;;
|
||||||
|
*) printf '%%%02X' "'${char}" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# buildVisionSubscriptionLink — 生成 Vision 订阅 URI,空字段时返回失败。
|
||||||
|
buildVisionSubscriptionLink() {
|
||||||
|
local displayAddress="$1"
|
||||||
|
local displayPort="$2"
|
||||||
|
local displayServerName="$3"
|
||||||
|
local displayPublicKey="$4"
|
||||||
|
local displayUUID="$5"
|
||||||
|
local displayEmail="$6"
|
||||||
|
local displayShortId="$7"
|
||||||
|
|
||||||
|
if [[ -z "${displayAddress}" || -z "${displayPort}" || -z "${displayServerName}" || -z "${displayPublicKey}" || -z "${displayUUID}" || -z "${displayEmail}" || -z "${displayShortId}" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'vless://%s@%s:%s?encryption=none&security=reality&type=tcp&sni=%s&fp=chrome&pbk=%s&sid=%s&flow=xtls-rprx-vision#%s' \
|
||||||
|
"${displayUUID}" \
|
||||||
|
"${displayAddress}" \
|
||||||
|
"${displayPort}" \
|
||||||
|
"${displayServerName}" \
|
||||||
|
"${displayPublicKey}" \
|
||||||
|
"${displayShortId}" \
|
||||||
|
"${displayEmail}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# buildXHTTPSubscriptionLink — 生成 XHTTP 订阅 URI,空字段时返回失败。
|
||||||
|
buildXHTTPSubscriptionLink() {
|
||||||
|
local displayAddress="$1"
|
||||||
|
local displayPort="$2"
|
||||||
|
local displayServerName="$3"
|
||||||
|
local displayPublicKey="$4"
|
||||||
|
local displayUUID="$5"
|
||||||
|
local displayEmail="$6"
|
||||||
|
local displayShortId="$7"
|
||||||
|
local displayPath="$8"
|
||||||
|
|
||||||
|
if [[ -z "${displayAddress}" || -z "${displayPort}" || -z "${displayServerName}" || -z "${displayPublicKey}" || -z "${displayUUID}" || -z "${displayEmail}" || -z "${displayShortId}" || -z "${displayPath}" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'vless://%s@%s:%s?encryption=none&security=reality&type=xhttp&sni=%s&host=%s&fp=chrome&path=%s&pbk=%s&sid=%s#%s' \
|
||||||
|
"${displayUUID}" \
|
||||||
|
"${displayAddress}" \
|
||||||
|
"${displayPort}" \
|
||||||
|
"${displayServerName}" \
|
||||||
|
"${displayServerName}" \
|
||||||
|
"${displayPath}" \
|
||||||
|
"${displayPublicKey}" \
|
||||||
|
"${displayShortId}" \
|
||||||
|
"${displayEmail}"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildXHTTPDefaultSubscriptionLink() {
|
||||||
|
local displayAddress="$1"
|
||||||
|
local displayPort="$2"
|
||||||
|
local displayServerName="$3"
|
||||||
|
local displayPublicKey="$4"
|
||||||
|
local displayUUID="$5"
|
||||||
|
local displayEmail="$6"
|
||||||
|
local displayShortId="$7"
|
||||||
|
local displayPath="$8"
|
||||||
|
|
||||||
|
if [[ -z "${displayAddress}" || -z "${displayPort}" || -z "${displayServerName}" || -z "${displayPublicKey}" || -z "${displayUUID}" || -z "${displayEmail}" || -z "${displayShortId}" || -z "${displayPath}" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'vless://%s@%s:%s?encryption=none&security=reality&type=xhttp&sni=%s&fp=chrome&path=%s&pbk=%s&sid=%s#%s' \
|
||||||
|
"${displayUUID}" \
|
||||||
|
"${displayAddress}" \
|
||||||
|
"${displayPort}" \
|
||||||
|
"${displayServerName}" \
|
||||||
|
"${displayPath}" \
|
||||||
|
"${displayPublicKey}" \
|
||||||
|
"${displayShortId}" \
|
||||||
|
"${displayEmail}"
|
||||||
|
}
|
||||||
|
|
||||||
|
writeVisionClashMetaNode() {
|
||||||
|
local outputFile="$1"
|
||||||
|
local displayAddress="$2"
|
||||||
|
local displayPort="$3"
|
||||||
|
local displayServerName="$4"
|
||||||
|
local displayPublicKey="$5"
|
||||||
|
local displayUUID="$6"
|
||||||
|
local displayEmail="$7"
|
||||||
|
local displayShortId="$8"
|
||||||
|
|
||||||
|
if [[ -z "${displayAddress}" || -z "${displayPort}" || -z "${displayServerName}" || -z "${displayPublicKey}" || -z "${displayUUID}" || -z "${displayEmail}" || -z "${displayShortId}" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF >>"${outputFile}"
|
||||||
|
- name: "${displayEmail}"
|
||||||
|
type: vless
|
||||||
|
server: ${displayAddress}
|
||||||
|
port: ${displayPort}
|
||||||
|
uuid: ${displayUUID}
|
||||||
|
network: tcp
|
||||||
|
tls: true
|
||||||
|
udp: true
|
||||||
|
flow: xtls-rprx-vision
|
||||||
|
servername: ${displayServerName}
|
||||||
|
reality-opts:
|
||||||
|
public-key: ${displayPublicKey}
|
||||||
|
short-id: ${displayShortId}
|
||||||
|
client-fingerprint: chrome
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
writeXHTTPClashMetaNode() {
|
||||||
|
local outputFile="$1"
|
||||||
|
local displayAddress="$2"
|
||||||
|
local displayPort="$3"
|
||||||
|
local displayServerName="$4"
|
||||||
|
local displayPublicKey="$5"
|
||||||
|
local displayUUID="$6"
|
||||||
|
local displayEmail="$7"
|
||||||
|
local displayShortId="$8"
|
||||||
|
local displayPath="$9"
|
||||||
|
|
||||||
|
if [[ -z "${displayAddress}" || -z "${displayPort}" || -z "${displayServerName}" || -z "${displayPublicKey}" || -z "${displayUUID}" || -z "${displayEmail}" || -z "${displayShortId}" || -z "${displayPath}" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF >>"${outputFile}"
|
||||||
|
- name: "${displayEmail}"
|
||||||
|
type: vless
|
||||||
|
server: ${displayAddress}
|
||||||
|
port: ${displayPort}
|
||||||
|
uuid: ${displayUUID}
|
||||||
|
udp: true
|
||||||
|
tls: true
|
||||||
|
network: xhttp
|
||||||
|
client-fingerprint: chrome
|
||||||
|
alpn:
|
||||||
|
- h2
|
||||||
|
servername: ${displayServerName}
|
||||||
|
xhttp-opts:
|
||||||
|
path: ${displayPath}
|
||||||
|
host: ${displayServerName}
|
||||||
|
reality-opts:
|
||||||
|
public-key: ${displayPublicKey}
|
||||||
|
short-id: ${displayShortId}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
writeClashMetaProfile() {
|
||||||
|
local outputFile="$1"
|
||||||
|
local providerURL="$2"
|
||||||
|
local providerName="$3"
|
||||||
|
|
||||||
|
cat <<EOF >"${outputFile}"
|
||||||
|
log-level: debug
|
||||||
|
mode: rule
|
||||||
|
ipv6: true
|
||||||
|
mixed-port: 7890
|
||||||
|
allow-lan: true
|
||||||
|
bind-address: "*"
|
||||||
|
find-process-mode: strict
|
||||||
|
external-controller: 0.0.0.0:9090
|
||||||
|
global-client-fingerprint: chrome
|
||||||
|
|
||||||
|
profile:
|
||||||
|
store-selected: true
|
||||||
|
store-fake-ip: true
|
||||||
|
|
||||||
|
dns:
|
||||||
|
enable: true
|
||||||
|
listen: 0.0.0.0:1053
|
||||||
|
ipv6: true
|
||||||
|
enhanced-mode: fake-ip
|
||||||
|
fake-ip-range: 198.18.0.1/16
|
||||||
|
nameserver:
|
||||||
|
- https://1.1.1.1/dns-query
|
||||||
|
- https://8.8.8.8/dns-query
|
||||||
|
- 1.1.1.1
|
||||||
|
- 8.8.8.8
|
||||||
|
|
||||||
|
proxy-providers:
|
||||||
|
${providerName}:
|
||||||
|
type: http
|
||||||
|
path: ./${providerName}.yaml
|
||||||
|
url: ${providerURL}
|
||||||
|
interval: 3600
|
||||||
|
proxy: DIRECT
|
||||||
|
health-check:
|
||||||
|
enable: true
|
||||||
|
url: https://cp.cloudflare.com/generate_204
|
||||||
|
interval: 300
|
||||||
|
|
||||||
|
proxy-groups:
|
||||||
|
- name: 手动切换
|
||||||
|
type: select
|
||||||
|
use:
|
||||||
|
- ${providerName}
|
||||||
|
proxies: null
|
||||||
|
- name: 自动选择
|
||||||
|
type: url-test
|
||||||
|
url: http://www.gstatic.com/generate_204
|
||||||
|
interval: 36000
|
||||||
|
tolerance: 50
|
||||||
|
use:
|
||||||
|
- ${providerName}
|
||||||
|
proxies: null
|
||||||
|
- name: 全球代理
|
||||||
|
type: select
|
||||||
|
use:
|
||||||
|
- ${providerName}
|
||||||
|
proxies:
|
||||||
|
- 手动切换
|
||||||
|
- 自动选择
|
||||||
|
- name: 漏网之鱼
|
||||||
|
type: select
|
||||||
|
use:
|
||||||
|
- ${providerName}
|
||||||
|
proxies:
|
||||||
|
- 全球代理
|
||||||
|
- DIRECT
|
||||||
|
- 手动切换
|
||||||
|
- 自动选择
|
||||||
|
|
||||||
|
rules:
|
||||||
|
- GEOIP,LAN,DIRECT,no-resolve
|
||||||
|
- GEOSITE,private,DIRECT
|
||||||
|
- GEOSITE,cn,DIRECT
|
||||||
|
- GEOIP,CN,DIRECT
|
||||||
|
- MATCH,漏网之鱼
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
initRandomSalt() {
|
||||||
|
local chars="abcdefghijklmnopqrtuxyz"
|
||||||
|
local randomSalt=""
|
||||||
|
local _idx
|
||||||
|
for _idx in 1 2 3 4 5 6 7 8 9 10; do
|
||||||
|
randomSalt+="${chars:RANDOM%${#chars}:1}"
|
||||||
|
done
|
||||||
|
printf '%s' "${randomSalt}"
|
||||||
|
}
|
||||||
|
|
||||||
|
md5Text() {
|
||||||
|
if command -v md5sum >/dev/null 2>&1; then
|
||||||
|
printf '%s' "$1" | md5sum | awk '{print $1}'
|
||||||
|
else
|
||||||
|
printf '%s' "$1" | md5 | awk '{print $NF}'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
base64FileSingleLine() {
|
||||||
|
if base64 --help 2>&1 | grep -q -- '-w'; then
|
||||||
|
base64 -w 0 "$1"
|
||||||
|
else
|
||||||
|
base64 <"$1" | tr -d '\n'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
generateSubscribeSalt() {
|
||||||
|
local saltFile="${dataDir%/}/subscribe_local/subscribeSalt"
|
||||||
|
if [[ -n "${persistedSubscribeSalt}" ]]; then
|
||||||
|
printf '%s' "${persistedSubscribeSalt}"
|
||||||
|
elif [[ -s "${saltFile}" ]]; then
|
||||||
|
cat "${saltFile}"
|
||||||
|
else
|
||||||
|
local newSalt
|
||||||
|
newSalt="$(initRandomSalt)"
|
||||||
|
mkdir -p "${dataDir%/}/subscribe_local"
|
||||||
|
printf '%s' "${newSalt}" >"${saltFile}"
|
||||||
|
printf '%s' "${newSalt}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
startSubscribeContainer() {
|
||||||
|
local displayAddress="$1"
|
||||||
|
local containerName="v2ray-agent-docker-subscribe"
|
||||||
|
local nginxImage="nginx:alpine"
|
||||||
|
local activePort="${persistedSubscribePort:-${resolvedSubscribePort}}"
|
||||||
|
local nginxConfig="${dataDir%/}/subscribe-nginx.conf"
|
||||||
|
|
||||||
|
if [[ -z "${activePort}" ]]; then
|
||||||
|
parsePort ""
|
||||||
|
activePort="${resolvedPort}"
|
||||||
|
checkPortInUse "${activePort}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "${dataDir%/}/subscribe/default" "${dataDir%/}/subscribe/clashMeta" "${dataDir%/}/subscribe/clashMetaProfiles"
|
||||||
|
cat >"${nginxConfig}" <<'EOF'
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
location ~ ^/s/(default|clashMeta|clashMetaProfiles)/(.*) {
|
||||||
|
default_type 'text/plain; charset=utf-8';
|
||||||
|
alias /usr/share/nginx/html/subscribe/$1/$2;
|
||||||
|
}
|
||||||
|
location / {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
docker pull "${nginxImage}" >/dev/null 2>&1 || true
|
||||||
|
if docker ps -a --filter "name=^/${containerName}$" --format '{{.Names}}' | grep -q "^${containerName}$"; then
|
||||||
|
docker rm -f "${containerName}" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
if ! docker run -d \
|
||||||
|
--name "${containerName}" \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p "${activePort}:80" \
|
||||||
|
-v "${dataDir%/}:/usr/share/nginx/html:ro" \
|
||||||
|
-v "${nginxConfig}:/etc/nginx/conf.d/default.conf:ro" \
|
||||||
|
"${nginxImage}" >/dev/null; then
|
||||||
|
echoContent "red" "启动订阅访问容器失败"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
persistedSubscribePort="${activePort}"
|
||||||
|
echoContent "green" "订阅访问服务已启动:http://${displayAddress}:${activePort}/s/default/<订阅ID>"
|
||||||
|
}
|
||||||
|
|
||||||
|
persistSubscribeState() {
|
||||||
|
local summaryFile="${dataDir%/}/client-summary.txt"
|
||||||
|
local tmpFile=""
|
||||||
|
|
||||||
|
[[ -f "${summaryFile}" ]] || return 0
|
||||||
|
tmpFile="$(mktemp)"
|
||||||
|
while IFS= read -r line; do
|
||||||
|
case "${line}" in
|
||||||
|
subscribePort:\ * | subscribeSalt:\ *) ;;
|
||||||
|
*) printf '%s\n' "${line}" >>"${tmpFile}" ;;
|
||||||
|
esac
|
||||||
|
done <"${summaryFile}"
|
||||||
|
printf 'subscribePort: %s\n' "${persistedSubscribePort}" >>"${tmpFile}"
|
||||||
|
printf 'subscribeSalt: %s\n' "${persistedSubscribeSalt}" >>"${tmpFile}"
|
||||||
|
mv "${tmpFile}" "${summaryFile}"
|
||||||
|
chmod 600 "${summaryFile}"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureSubscribeRuntimeValues() {
|
||||||
|
local inputPort=""
|
||||||
|
|
||||||
|
if [[ -z "${persistedSubscribePort}" ]]; then
|
||||||
|
inputPort="$(promptValue $'请输入订阅访问端口,[回车]随机:' "")"
|
||||||
|
if ! parsePort "${inputPort}"; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
persistedSubscribePort="${resolvedPort}"
|
||||||
|
fi
|
||||||
|
if [[ -z "${persistedSubscribeSalt}" ]]; then
|
||||||
|
persistedSubscribeSalt="$(initRandomSalt)"
|
||||||
|
fi
|
||||||
|
persistSubscribeState
|
||||||
|
}
|
||||||
|
|
||||||
installModeHasVision() {
|
installModeHasVision() {
|
||||||
[[ "$1" == "vision" || "$1" == "all" ]]
|
[[ "$1" == "vision" || "$1" == "all" ]]
|
||||||
}
|
}
|
||||||
@@ -356,6 +724,14 @@ parseCli() {
|
|||||||
email="$2"
|
email="$2"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
--subscribe-port)
|
||||||
|
if [[ $# -lt 2 || (-n "${2:-}" && "${2:0:1}" == "-") ]]; then
|
||||||
|
echoContent "red" "--subscribe-port 需要一个数字值"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
subscribePort="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
--generate-only)
|
--generate-only)
|
||||||
generateOnly=1
|
generateOnly=1
|
||||||
shift
|
shift
|
||||||
@@ -624,7 +1000,7 @@ checkPortInUse() {
|
|||||||
# 若所有监听 PID 均属于该容器的 docker-proxy 进程,则允许使用该端口。
|
# 若所有监听 PID 均属于该容器的 docker-proxy 进程,则允许使用该端口。
|
||||||
local containerExists=0
|
local containerExists=0
|
||||||
if docker ps -a --filter "name=^/v2ray-agent-docker$" --format '{{.Names}}' 2>/dev/null |
|
if docker ps -a --filter "name=^/v2ray-agent-docker$" --format '{{.Names}}' 2>/dev/null |
|
||||||
grep -q "^v2ray-agent-docker$"; then
|
grep -q "^v2ray-agent-docker$" || docker ps -a --filter "name=^/v2ray-agent-docker-subscribe$" --format '{{.Names}}' 2>/dev/null | grep -q "^v2ray-agent-docker-subscribe$"; then
|
||||||
containerExists=1
|
containerExists=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -784,6 +1160,8 @@ loadPersistedSummaryIfPresent() {
|
|||||||
persistedVisionEmail=""
|
persistedVisionEmail=""
|
||||||
persistedXHTTPEmail=""
|
persistedXHTTPEmail=""
|
||||||
persistedShortId="6ba85179e30d4fc2"
|
persistedShortId="6ba85179e30d4fc2"
|
||||||
|
persistedSubscribePort=""
|
||||||
|
persistedSubscribeSalt=""
|
||||||
persistedConfigPath=""
|
persistedConfigPath=""
|
||||||
|
|
||||||
if [[ ! -f "${summaryFile}" ]]; then
|
if [[ ! -f "${summaryFile}" ]]; then
|
||||||
@@ -804,6 +1182,8 @@ loadPersistedSummaryIfPresent() {
|
|||||||
visionEmail:\ *) persistedVisionEmail="${line#visionEmail: }" ;;
|
visionEmail:\ *) persistedVisionEmail="${line#visionEmail: }" ;;
|
||||||
xhttpEmail:\ *) persistedXHTTPEmail="${line#xhttpEmail: }" ;;
|
xhttpEmail:\ *) persistedXHTTPEmail="${line#xhttpEmail: }" ;;
|
||||||
shortId:\ *) persistedShortId="${line#shortId: }" ;;
|
shortId:\ *) persistedShortId="${line#shortId: }" ;;
|
||||||
|
subscribePort:\ *) persistedSubscribePort="${line#subscribePort: }" ;;
|
||||||
|
subscribeSalt:\ *) persistedSubscribeSalt="${line#subscribeSalt: }" ;;
|
||||||
configPath:\ *) persistedConfigPath="${line#configPath: }" ;;
|
configPath:\ *) persistedConfigPath="${line#configPath: }" ;;
|
||||||
esac
|
esac
|
||||||
done <"${summaryFile}"
|
done <"${summaryFile}"
|
||||||
@@ -820,6 +1200,8 @@ loadPersistedSummaryIfPresent() {
|
|||||||
persistedVisionEmail="${persistedVisionEmail#"${persistedVisionEmail%%[![:space:]]*}"}"
|
persistedVisionEmail="${persistedVisionEmail#"${persistedVisionEmail%%[![:space:]]*}"}"
|
||||||
persistedXHTTPEmail="${persistedXHTTPEmail#"${persistedXHTTPEmail%%[![:space:]]*}"}"
|
persistedXHTTPEmail="${persistedXHTTPEmail#"${persistedXHTTPEmail%%[![:space:]]*}"}"
|
||||||
persistedShortId="${persistedShortId#"${persistedShortId%%[![:space:]]*}"}"
|
persistedShortId="${persistedShortId#"${persistedShortId%%[![:space:]]*}"}"
|
||||||
|
persistedSubscribePort="${persistedSubscribePort#"${persistedSubscribePort%%[![:space:]]*}"}"
|
||||||
|
persistedSubscribeSalt="${persistedSubscribeSalt#"${persistedSubscribeSalt%%[![:space:]]*}"}"
|
||||||
persistedConfigPath="${persistedConfigPath#"${persistedConfigPath%%[![:space:]]*}"}"
|
persistedConfigPath="${persistedConfigPath#"${persistedConfigPath%%[![:space:]]*}"}"
|
||||||
|
|
||||||
if [[ -n "${persistedContainer}" && "${persistedContainer}" != "v2ray-agent-docker" ]]; then
|
if [[ -n "${persistedContainer}" && "${persistedContainer}" != "v2ray-agent-docker" ]]; then
|
||||||
@@ -1153,6 +1535,41 @@ hasExistingInstallState() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userManageMenu() {
|
||||||
|
local action=""
|
||||||
|
|
||||||
|
if ! hasPersistedConfig; then
|
||||||
|
echoContent "red" "未找到现有配置,无法进入用户管理。请先重新安装。"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echoContent "skyBlue" "─── 用户管理 ───────────────────────────────────────────"
|
||||||
|
echoContent "yellow" "1. 查看账号"
|
||||||
|
echoContent "yellow" "2. 查看订阅"
|
||||||
|
echoContent "yellow" "3. 退出"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
action="$(promptValue $'请选择 [1-3]:' "")"
|
||||||
|
case "${action}" in
|
||||||
|
1)
|
||||||
|
showAccountInfo
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
showSubscriptionInfo
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
echoContent "white" "已退出,不做任何修改。"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echoContent "red" "无效选择,请输入 1-3。"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
# uninstallDockerReality — 卸载 Docker Reality 独立脚本生成的容器、配置与快捷方式。
|
# uninstallDockerReality — 卸载 Docker Reality 独立脚本生成的容器、配置与快捷方式。
|
||||||
uninstallDockerReality() {
|
uninstallDockerReality() {
|
||||||
local answer=""
|
local answer=""
|
||||||
@@ -1172,6 +1589,10 @@ uninstallDockerReality() {
|
|||||||
docker rm -f v2ray-agent-docker >/dev/null 2>&1 || true
|
docker rm -f v2ray-agent-docker >/dev/null 2>&1 || true
|
||||||
echoContent "green" " ---> 删除 Docker 容器完成"
|
echoContent "green" " ---> 删除 Docker 容器完成"
|
||||||
fi
|
fi
|
||||||
|
if docker ps -a --filter "name=^/v2ray-agent-docker-subscribe$" --format '{{.Names}}' 2>/dev/null | grep -q '^v2ray-agent-docker-subscribe$'; then
|
||||||
|
docker rm -f v2ray-agent-docker-subscribe >/dev/null 2>&1 || true
|
||||||
|
echoContent "green" " ---> 删除订阅访问容器完成"
|
||||||
|
fi
|
||||||
|
|
||||||
rm -rf "${dataDir%/}" >/dev/null 2>&1 || true
|
rm -rf "${dataDir%/}" >/dev/null 2>&1 || true
|
||||||
echoContent "green" " ---> 删除 Docker Reality 配置目录完成"
|
echoContent "green" " ---> 删除 Docker Reality 配置目录完成"
|
||||||
@@ -1243,7 +1664,7 @@ promptExistingInstallAction() {
|
|||||||
echoContent "white" "已检测到容器,但未检测到配置文件。请选择下一步操作:"
|
echoContent "white" "已检测到容器,但未检测到配置文件。请选择下一步操作:"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echoContent "yellow" "1. 查看账号"
|
echoContent "yellow" "1. 用户管理"
|
||||||
echoContent "yellow" "2. 重新安装"
|
echoContent "yellow" "2. 重新安装"
|
||||||
echoContent "yellow" "3. 启动/重建容器"
|
echoContent "yellow" "3. 启动/重建容器"
|
||||||
echoContent "yellow" "4. 卸载"
|
echoContent "yellow" "4. 卸载"
|
||||||
@@ -1254,10 +1675,10 @@ promptExistingInstallAction() {
|
|||||||
case "${action}" in
|
case "${action}" in
|
||||||
1)
|
1)
|
||||||
if hasPersistedConfig; then
|
if hasPersistedConfig; then
|
||||||
showClientInfo
|
userManageMenu
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
echoContent "red" "未找到现有配置,无法查看账号。请先重新安装。"
|
echoContent "red" "未找到现有配置,无法进入用户管理。请先重新安装。"
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
2)
|
2)
|
||||||
@@ -1307,7 +1728,11 @@ showVisionAccount() {
|
|||||||
displayAddress="YOUR_SERVER_IP"
|
displayAddress="YOUR_SERVER_IP"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
vlessLink="vless://${displayUUID}@${displayAddress}:${displayPort}?encryption=none&security=reality&type=tcp&sni=${displayServerName}&fp=chrome&pbk=${displayPublicKey}&sid=${displayShortId}&flow=xtls-rprx-vision#${displayEmail}"
|
vlessLink="$(buildVisionSubscriptionLink "${displayAddress}" "${displayPort}" "${displayServerName}" "${displayPublicKey}" "${displayUUID}" "${displayEmail}" "${displayShortId}")"
|
||||||
|
if [[ -z "${vlessLink}" ]]; then
|
||||||
|
echoContent "yellow" " ---> Vision 账号信息不完整,已跳过订阅链接输出"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
qrData="${vlessLink//:/%3A}"
|
qrData="${vlessLink//:/%3A}"
|
||||||
qrData="${qrData//\//%2F}"
|
qrData="${qrData//\//%2F}"
|
||||||
qrData="${qrData//@/%40}"
|
qrData="${qrData//@/%40}"
|
||||||
@@ -1348,7 +1773,11 @@ showXHTTPAccount() {
|
|||||||
displayAddress="YOUR_SERVER_IP"
|
displayAddress="YOUR_SERVER_IP"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
vlessLink="vless://${displayUUID}@${displayAddress}:${displayPort}?encryption=none&security=reality&type=xhttp&sni=${displayServerName}&host=${displayServerName}&fp=chrome&path=${displayPath}&pbk=${displayPublicKey}&sid=${displayShortId}#${displayEmail}"
|
vlessLink="$(buildXHTTPSubscriptionLink "${displayAddress}" "${displayPort}" "${displayServerName}" "${displayPublicKey}" "${displayUUID}" "${displayEmail}" "${displayShortId}" "${displayPath}")"
|
||||||
|
if [[ -z "${vlessLink}" ]]; then
|
||||||
|
echoContent "yellow" " ---> XHTTP 账号信息不完整,已跳过订阅链接输出"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
qrLink="https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=vless%3A%2F%2F${displayUUID}%40${displayAddress}%3A${displayPort}%3Fencryption%3Dnone%26security%3Dreality%26type%3Dxhttp%26sni%3D${displayServerName}%26fp%3Dchrome%26path%3D${displayPath}%26host%3D${displayServerName}%26pbk%3D${displayPublicKey}%26sid%3D${displayShortId}%23${displayEmail}"
|
qrLink="https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=vless%3A%2F%2F${displayUUID}%40${displayAddress}%3A${displayPort}%3Fencryption%3Dnone%26security%3Dreality%26type%3Dxhttp%26sni%3D${displayServerName}%26fp%3Dchrome%26path%3D${displayPath}%26host%3D${displayServerName}%26pbk%3D${displayPublicKey}%26sid%3D${displayShortId}%23${displayEmail}"
|
||||||
|
|
||||||
echoContent "skyBlue" "============================= VLESS reality_xhttp =============================="
|
echoContent "skyBlue" "============================= VLESS reality_xhttp =============================="
|
||||||
@@ -1365,6 +1794,108 @@ showXHTTPAccount() {
|
|||||||
echoContent "green" " ${qrLink}"
|
echoContent "green" " ${qrLink}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# showChineseSubscriptionSection — 输出本地中文订阅内容与风险提示。
|
||||||
|
showChineseSubscriptionSection() {
|
||||||
|
local displayAddress="$1"
|
||||||
|
local mode="$2"
|
||||||
|
local displayServerName="$3"
|
||||||
|
local displayPublicKey="$4"
|
||||||
|
local displayUUID="$5"
|
||||||
|
local displayVisionPort="$6"
|
||||||
|
local displayXHTTPPort="$7"
|
||||||
|
local displayXHTTPPath="$8"
|
||||||
|
local displayVisionEmail="$9"
|
||||||
|
local displayXHTTPEmail="${10}"
|
||||||
|
local displayShortId="${11}"
|
||||||
|
local visionLink=""
|
||||||
|
local xhttpLink=""
|
||||||
|
local renderedPath=""
|
||||||
|
local subscribeName=""
|
||||||
|
local subscribeSalt=""
|
||||||
|
local emailMd5=""
|
||||||
|
local defaultLocalFile=""
|
||||||
|
local defaultPublicFile=""
|
||||||
|
local clashLocalFile=""
|
||||||
|
local clashPublicFile=""
|
||||||
|
local clashProfileFile=""
|
||||||
|
local subscribeDomain=""
|
||||||
|
local subscribeURL=""
|
||||||
|
local clashProxyURL=""
|
||||||
|
local clashProfileURL=""
|
||||||
|
|
||||||
|
if [[ -z "${displayAddress}" ]]; then
|
||||||
|
displayAddress="YOUR_SERVER_IP"
|
||||||
|
fi
|
||||||
|
|
||||||
|
subscribeName="${displayVisionEmail:-${displayXHTTPEmail}}"
|
||||||
|
subscribeName="${subscribeName%%-*}"
|
||||||
|
if [[ -z "${subscribeName}" ]]; then
|
||||||
|
echoContent "yellow" " ---> 订阅账号信息不完整,已跳过订阅文件生成"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "${dataDir%/}/subscribe_local/default" "${dataDir%/}/subscribe_local/clashMeta" "${dataDir%/}/subscribe/default" "${dataDir%/}/subscribe/clashMeta" "${dataDir%/}/subscribe/clashMetaProfiles"
|
||||||
|
defaultLocalFile="${dataDir%/}/subscribe_local/default/${subscribeName}"
|
||||||
|
clashLocalFile="${dataDir%/}/subscribe_local/clashMeta/${subscribeName}"
|
||||||
|
: >"${defaultLocalFile}"
|
||||||
|
: >"${clashLocalFile}"
|
||||||
|
|
||||||
|
if installModeHasVision "${mode}"; then
|
||||||
|
visionLink="$(buildVisionSubscriptionLink "${displayAddress}" "${displayVisionPort}" "${displayServerName}" "${displayPublicKey}" "${displayUUID}" "${displayVisionEmail}" "${displayShortId}")" || visionLink=""
|
||||||
|
if [[ -n "${visionLink}" ]]; then
|
||||||
|
printf '%s\n' "${visionLink}" >>"${defaultLocalFile}"
|
||||||
|
writeVisionClashMetaNode "${clashLocalFile}" "${displayAddress}" "${displayVisionPort}" "${displayServerName}" "${displayPublicKey}" "${displayUUID}" "${displayVisionEmail}" "${displayShortId}" || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if installModeHasXHTTP "${mode}"; then
|
||||||
|
renderedPath="$(renderXHTTPPath "${displayXHTTPPath}")"
|
||||||
|
xhttpLink="$(buildXHTTPDefaultSubscriptionLink "${displayAddress}" "${displayXHTTPPort}" "${displayServerName}" "${displayPublicKey}" "${displayUUID}" "${displayXHTTPEmail}" "${displayShortId}" "${renderedPath}")" || xhttpLink=""
|
||||||
|
if [[ -n "${xhttpLink}" ]]; then
|
||||||
|
printf '%s\n' "${xhttpLink}" >>"${defaultLocalFile}"
|
||||||
|
writeXHTTPClashMetaNode "${clashLocalFile}" "${displayAddress}" "${displayXHTTPPort}" "${displayServerName}" "${displayPublicKey}" "${displayUUID}" "${displayXHTTPEmail}" "${displayShortId}" "${renderedPath}" || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -s "${defaultLocalFile}" ]]; then
|
||||||
|
echoContent "yellow" " ---> 订阅账号信息不完整,未生成可用订阅内容"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
subscribeSalt="$(generateSubscribeSalt)"
|
||||||
|
emailMd5="$(md5Text "${subscribeName}${subscribeSalt}"$'\n')"
|
||||||
|
defaultPublicFile="${dataDir%/}/subscribe/default/${emailMd5}"
|
||||||
|
base64FileSingleLine "${defaultLocalFile}" >"${defaultPublicFile}"
|
||||||
|
|
||||||
|
startSubscribeContainer "${displayAddress}"
|
||||||
|
subscribeDomain="${displayAddress}:${persistedSubscribePort:-${resolvedSubscribePort}}"
|
||||||
|
subscribeURL="http://${subscribeDomain}/s/default/${emailMd5}"
|
||||||
|
if [[ -s "${clashLocalFile}" ]]; then
|
||||||
|
clashPublicFile="${dataDir%/}/subscribe/clashMeta/${emailMd5}"
|
||||||
|
clashProfileFile="${dataDir%/}/subscribe/clashMetaProfiles/${emailMd5}"
|
||||||
|
clashProxyURL="http://${subscribeDomain}/s/clashMeta/${emailMd5}"
|
||||||
|
clashProfileURL="http://${subscribeDomain}/s/clashMetaProfiles/${emailMd5}"
|
||||||
|
printf 'proxies:\n' >"${clashPublicFile}"
|
||||||
|
cat "${clashLocalFile}" >>"${clashPublicFile}"
|
||||||
|
writeClashMetaProfile "${clashProfileFile}" "${clashProxyURL}" "${subscribeSalt}_provider"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echoContent "skyBlue" "============================= 默认订阅 =============================="
|
||||||
|
echoContent "white" "HTTP 明文订阅有风险,可能暴露 订阅信息。"
|
||||||
|
echoContent "white" "当前 Docker 版通过本机订阅容器提供 HTTP 访问链接,请勿在不可信网络中传播。"
|
||||||
|
echoContent "white" ""
|
||||||
|
echoContent "green" "email: ${subscribeName}"
|
||||||
|
echoContent "yellow" "url: ${subscribeURL}"
|
||||||
|
echoContent "yellow" "在线二维码: https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=$(urlEncode "${subscribeURL}")"
|
||||||
|
if [[ -n "${clashProfileURL}" ]]; then
|
||||||
|
echo
|
||||||
|
echoContent "skyBlue" "--------------Clash Verge(mihomo)订阅--------------"
|
||||||
|
echo
|
||||||
|
echoContent "yellow" "url: ${clashProfileURL}"
|
||||||
|
echoContent "yellow" "在线二维码: https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=$(urlEncode "${clashProfileURL}")"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# resolveValues — 将所有 CLI 提供或留空的值解析为 resolved* 全局变量。
|
# resolveValues — 将所有 CLI 提供或留空的值解析为 resolved* 全局变量。
|
||||||
# 依赖:installMode/resolvedInstallMode、port、xhttpPort、xhttpPath、
|
# 依赖:installMode/resolvedInstallMode、port、xhttpPort、xhttpPath、
|
||||||
# serverName、privateKey、uuid、email(来自 parseCli 的全局变量)
|
# serverName、privateKey、uuid、email(来自 parseCli 的全局变量)
|
||||||
@@ -1407,6 +1938,20 @@ resolveValues() {
|
|||||||
resolvedXHTTPPath="${resolvedXHTTPPath:-${resolvedXHTTPPath}}"
|
resolvedXHTTPPath="${resolvedXHTTPPath:-${resolvedXHTTPPath}}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if ! parsePort "${subscribePort}"; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
resolvedSubscribePort="${resolvedPort}"
|
||||||
|
if installModeHasVision "${resolvedInstallMode}" && [[ "${resolvedSubscribePort}" == "${resolvedVisionPort}" ]]; then
|
||||||
|
echoContent "red" "订阅端口(${resolvedSubscribePort})与 Vision 端口相同,请使用不同端口"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if installModeHasXHTTP "${resolvedInstallMode}" && [[ "${resolvedSubscribePort}" == "${resolvedXHTTPPort}" ]]; then
|
||||||
|
echoContent "red" "订阅端口(${resolvedSubscribePort})与 XHTTP 端口相同,请使用不同端口"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
checkPortInUse "${resolvedSubscribePort}"
|
||||||
|
|
||||||
# --- 服务器名称 ---
|
# --- 服务器名称 ---
|
||||||
parseServerName "${serverName}"
|
parseServerName "${serverName}"
|
||||||
|
|
||||||
@@ -1716,6 +2261,8 @@ EOF
|
|||||||
printf 'xhttpEmail: %s\n' "${resolvedXHTTPEmail}"
|
printf 'xhttpEmail: %s\n' "${resolvedXHTTPEmail}"
|
||||||
fi
|
fi
|
||||||
printf 'shortId: 6ba85179e30d4fc2\n'
|
printf 'shortId: 6ba85179e30d4fc2\n'
|
||||||
|
printf 'subscribePort: %s\n' "${resolvedSubscribePort}"
|
||||||
|
printf 'subscribeSalt: %s\n' "$(initRandomSalt)"
|
||||||
printf 'configPath: %s\n' "${configFile}"
|
printf 'configPath: %s\n' "${configFile}"
|
||||||
} >"${summaryFile}"
|
} >"${summaryFile}"
|
||||||
chmod 600 "${summaryFile}"
|
chmod 600 "${summaryFile}"
|
||||||
@@ -1825,16 +2372,17 @@ startContainer() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# showClientInfo — 以 showAccounts 风格打印当前 Reality 账号信息
|
# showAccountInfo — 以 showAccounts 风格打印当前 Reality 账号信息
|
||||||
# 根据 installMode 只显示已安装协议的账号块
|
# 根据 installMode 只显示已安装协议的账号块
|
||||||
showClientInfo() {
|
showAccountInfo() {
|
||||||
local displayAddress=""
|
local displayAddress=""
|
||||||
|
local mode=""
|
||||||
|
local shortId=""
|
||||||
|
|
||||||
loadPersistedAccountInfo
|
loadPersistedAccountInfo
|
||||||
displayAddress="$(getPublicIP)"
|
displayAddress="$(getPublicIP)"
|
||||||
|
mode="${persistedInstallMode}"
|
||||||
local shortId="${persistedShortId:-6ba85179e30d4fc2}"
|
shortId="${persistedShortId:-6ba85179e30d4fc2}"
|
||||||
local mode="${persistedInstallMode}"
|
|
||||||
|
|
||||||
if installModeHasVision "${mode}"; then
|
if installModeHasVision "${mode}"; then
|
||||||
showVisionAccount \
|
showVisionAccount \
|
||||||
@@ -1860,6 +2408,36 @@ showClientInfo() {
|
|||||||
"${shortId}" \
|
"${shortId}" \
|
||||||
"${renderedPath}"
|
"${renderedPath}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
showSubscriptionInfo() {
|
||||||
|
local displayAddress=""
|
||||||
|
local mode=""
|
||||||
|
local shortId=""
|
||||||
|
|
||||||
|
loadPersistedAccountInfo
|
||||||
|
ensureSubscribeRuntimeValues
|
||||||
|
displayAddress="$(getPublicIP)"
|
||||||
|
mode="${persistedInstallMode}"
|
||||||
|
shortId="${persistedShortId:-6ba85179e30d4fc2}"
|
||||||
|
|
||||||
|
showChineseSubscriptionSection \
|
||||||
|
"${displayAddress}" \
|
||||||
|
"${mode}" \
|
||||||
|
"${persistedServerName}" \
|
||||||
|
"${persistedPublicKey}" \
|
||||||
|
"${persistedUUID}" \
|
||||||
|
"${persistedVisionPort}" \
|
||||||
|
"${persistedXHTTPPort}" \
|
||||||
|
"${persistedXHTTPPath}" \
|
||||||
|
"${persistedVisionEmail}" \
|
||||||
|
"${persistedXHTTPEmail}" \
|
||||||
|
"${shortId}"
|
||||||
|
}
|
||||||
|
|
||||||
|
showClientInfo() {
|
||||||
|
showAccountInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
+582
-9
@@ -45,6 +45,8 @@ email=""
|
|||||||
generateOnly=0
|
generateOnly=0
|
||||||
startOnly=0
|
startOnly=0
|
||||||
skipSelfInstall=0
|
skipSelfInstall=0
|
||||||
|
subscribePort=""
|
||||||
|
resolvedSubscribePort=""
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# QA override: set V2RAY_AGENT_FORCE_NO_DOCKER=1 to skip Docker check in tests
|
# QA override: set V2RAY_AGENT_FORCE_NO_DOCKER=1 to skip Docker check in tests
|
||||||
@@ -74,6 +76,7 @@ showHelp() {
|
|||||||
echoContent "white" " --port <port> Vision or single-protocol external listen port (leave blank to pick randomly)"
|
echoContent "white" " --port <port> Vision or single-protocol external listen port (leave blank to pick randomly)"
|
||||||
echoContent "white" " --xhttp-port <port> XHTTP mode port (leave blank to pick randomly)"
|
echoContent "white" " --xhttp-port <port> XHTTP mode port (leave blank to pick randomly)"
|
||||||
echoContent "white" " --xhttp-path <path> XHTTP path base value (rendered as /<path>xHTTP)"
|
echoContent "white" " --xhttp-path <path> XHTTP path base value (rendered as /<path>xHTTP)"
|
||||||
|
echoContent "white" " --subscribe-port <port> Subscription access port (leave blank to pick randomly)"
|
||||||
echoContent "white" " --server-name <sni> SNI / server name used for Reality handshake"
|
echoContent "white" " --server-name <sni> SNI / server name used for Reality handshake"
|
||||||
echoContent "white" " --private-key <key> X25519 private key (auto-generated if blank)"
|
echoContent "white" " --private-key <key> X25519 private key (auto-generated if blank)"
|
||||||
echoContent "white" " --uuid <uuid> Client UUID (auto-generated if blank)"
|
echoContent "white" " --uuid <uuid> Client UUID (auto-generated if blank)"
|
||||||
@@ -85,7 +88,7 @@ showHelp() {
|
|||||||
echoContent "white" ""
|
echoContent "white" ""
|
||||||
echoContent "white" "Notes:"
|
echoContent "white" "Notes:"
|
||||||
echoContent "white" " 1. Interactive mode checks both config.json and the v2ray-agent-docker container before showing a menu."
|
echoContent "white" " 1. Interactive mode checks both config.json and the v2ray-agent-docker container before showing a menu."
|
||||||
echoContent "white" " 2. If both are missing, it shows an install menu; if either exists, it shows view-account / reinstall / start-or-recreate options."
|
echoContent "white" " 2. If both are missing, it shows an install menu; if either exists, it shows user-management / reinstall / start-or-recreate options."
|
||||||
echoContent "white" " 3. Install mode selects Reality Vision, Reality XHTTP, or both; XHTTP requires an additional path input."
|
echoContent "white" " 3. Install mode selects Reality Vision, Reality XHTTP, or both; XHTTP requires an additional path input."
|
||||||
echoContent "white" " 4. Non-interactive mode requires all values explicitly; privateKey/uuid/email may still be auto-generated/derived."
|
echoContent "white" " 4. Non-interactive mode requires all values explicitly; privateKey/uuid/email may still be auto-generated/derived."
|
||||||
echoContent "white" " 5. Data directory defaults to /etc/v2ray-agent/docker/; override with --data-dir for QA or custom paths."
|
echoContent "white" " 5. Data directory defaults to /etc/v2ray-agent/docker/; override with --data-dir for QA or custom paths."
|
||||||
@@ -199,6 +202,369 @@ renderXHTTPPath() {
|
|||||||
printf '/%sxHTTP' "$1"
|
printf '/%sxHTTP' "$1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
urlEncode() {
|
||||||
|
local input="$1"
|
||||||
|
local length=${#input}
|
||||||
|
local index char
|
||||||
|
|
||||||
|
for ((index = 0; index < length; index++)); do
|
||||||
|
char="${input:index:1}"
|
||||||
|
case "${char}" in
|
||||||
|
[a-zA-Z0-9.~_-]) printf '%s' "${char}" ;;
|
||||||
|
*) printf '%%%02X' "'${char}" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
buildVisionSubscriptionLink() {
|
||||||
|
local displayAddress="$1"
|
||||||
|
local displayPort="$2"
|
||||||
|
local displayServerName="$3"
|
||||||
|
local displayPublicKey="$4"
|
||||||
|
local displayUUID="$5"
|
||||||
|
local displayEmail="$6"
|
||||||
|
local displayShortId="$7"
|
||||||
|
|
||||||
|
if [[ -z "${displayAddress}" || -z "${displayPort}" || -z "${displayServerName}" || -z "${displayPublicKey}" || -z "${displayUUID}" || -z "${displayEmail}" || -z "${displayShortId}" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'vless://%s@%s:%s?encryption=none&security=reality&type=tcp&sni=%s&fp=chrome&pbk=%s&sid=%s&flow=xtls-rprx-vision#%s' \
|
||||||
|
"${displayUUID}" \
|
||||||
|
"${displayAddress}" \
|
||||||
|
"${displayPort}" \
|
||||||
|
"${displayServerName}" \
|
||||||
|
"${displayPublicKey}" \
|
||||||
|
"${displayShortId}" \
|
||||||
|
"${displayEmail}"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildXHTTPSubscriptionLink() {
|
||||||
|
local displayAddress="$1"
|
||||||
|
local displayPort="$2"
|
||||||
|
local displayServerName="$3"
|
||||||
|
local displayPublicKey="$4"
|
||||||
|
local displayUUID="$5"
|
||||||
|
local displayEmail="$6"
|
||||||
|
local displayShortId="$7"
|
||||||
|
local displayPath="$8"
|
||||||
|
|
||||||
|
if [[ -z "${displayAddress}" || -z "${displayPort}" || -z "${displayServerName}" || -z "${displayPublicKey}" || -z "${displayUUID}" || -z "${displayEmail}" || -z "${displayShortId}" || -z "${displayPath}" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'vless://%s@%s:%s?encryption=none&security=reality&type=xhttp&sni=%s&host=%s&fp=chrome&path=%s&pbk=%s&sid=%s#%s' \
|
||||||
|
"${displayUUID}" \
|
||||||
|
"${displayAddress}" \
|
||||||
|
"${displayPort}" \
|
||||||
|
"${displayServerName}" \
|
||||||
|
"${displayServerName}" \
|
||||||
|
"${displayPath}" \
|
||||||
|
"${displayPublicKey}" \
|
||||||
|
"${displayShortId}" \
|
||||||
|
"${displayEmail}"
|
||||||
|
}
|
||||||
|
|
||||||
|
buildXHTTPDefaultSubscriptionLink() {
|
||||||
|
local displayAddress="$1"
|
||||||
|
local displayPort="$2"
|
||||||
|
local displayServerName="$3"
|
||||||
|
local displayPublicKey="$4"
|
||||||
|
local displayUUID="$5"
|
||||||
|
local displayEmail="$6"
|
||||||
|
local displayShortId="$7"
|
||||||
|
local displayPath="$8"
|
||||||
|
|
||||||
|
if [[ -z "${displayAddress}" || -z "${displayPort}" || -z "${displayServerName}" || -z "${displayPublicKey}" || -z "${displayUUID}" || -z "${displayEmail}" || -z "${displayShortId}" || -z "${displayPath}" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf 'vless://%s@%s:%s?encryption=none&security=reality&type=xhttp&sni=%s&fp=chrome&path=%s&pbk=%s&sid=%s#%s' \
|
||||||
|
"${displayUUID}" \
|
||||||
|
"${displayAddress}" \
|
||||||
|
"${displayPort}" \
|
||||||
|
"${displayServerName}" \
|
||||||
|
"${displayPath}" \
|
||||||
|
"${displayPublicKey}" \
|
||||||
|
"${displayShortId}" \
|
||||||
|
"${displayEmail}"
|
||||||
|
}
|
||||||
|
|
||||||
|
writeVisionClashMetaNode() {
|
||||||
|
local outputFile="$1"
|
||||||
|
local displayAddress="$2"
|
||||||
|
local displayPort="$3"
|
||||||
|
local displayServerName="$4"
|
||||||
|
local displayPublicKey="$5"
|
||||||
|
local displayUUID="$6"
|
||||||
|
local displayEmail="$7"
|
||||||
|
local displayShortId="$8"
|
||||||
|
|
||||||
|
if [[ -z "${displayAddress}" || -z "${displayPort}" || -z "${displayServerName}" || -z "${displayPublicKey}" || -z "${displayUUID}" || -z "${displayEmail}" || -z "${displayShortId}" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF >>"${outputFile}"
|
||||||
|
- name: "${displayEmail}"
|
||||||
|
type: vless
|
||||||
|
server: ${displayAddress}
|
||||||
|
port: ${displayPort}
|
||||||
|
uuid: ${displayUUID}
|
||||||
|
network: tcp
|
||||||
|
tls: true
|
||||||
|
udp: true
|
||||||
|
flow: xtls-rprx-vision
|
||||||
|
servername: ${displayServerName}
|
||||||
|
reality-opts:
|
||||||
|
public-key: ${displayPublicKey}
|
||||||
|
short-id: ${displayShortId}
|
||||||
|
client-fingerprint: chrome
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
writeXHTTPClashMetaNode() {
|
||||||
|
local outputFile="$1"
|
||||||
|
local displayAddress="$2"
|
||||||
|
local displayPort="$3"
|
||||||
|
local displayServerName="$4"
|
||||||
|
local displayPublicKey="$5"
|
||||||
|
local displayUUID="$6"
|
||||||
|
local displayEmail="$7"
|
||||||
|
local displayShortId="$8"
|
||||||
|
local displayPath="$9"
|
||||||
|
|
||||||
|
if [[ -z "${displayAddress}" || -z "${displayPort}" || -z "${displayServerName}" || -z "${displayPublicKey}" || -z "${displayUUID}" || -z "${displayEmail}" || -z "${displayShortId}" || -z "${displayPath}" ]]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat <<EOF >>"${outputFile}"
|
||||||
|
- name: "${displayEmail}"
|
||||||
|
type: vless
|
||||||
|
server: ${displayAddress}
|
||||||
|
port: ${displayPort}
|
||||||
|
uuid: ${displayUUID}
|
||||||
|
udp: true
|
||||||
|
tls: true
|
||||||
|
network: xhttp
|
||||||
|
client-fingerprint: chrome
|
||||||
|
alpn:
|
||||||
|
- h2
|
||||||
|
servername: ${displayServerName}
|
||||||
|
xhttp-opts:
|
||||||
|
path: ${displayPath}
|
||||||
|
host: ${displayServerName}
|
||||||
|
reality-opts:
|
||||||
|
public-key: ${displayPublicKey}
|
||||||
|
short-id: ${displayShortId}
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
writeClashMetaProfile() {
|
||||||
|
local outputFile="$1"
|
||||||
|
local providerURL="$2"
|
||||||
|
local providerName="$3"
|
||||||
|
|
||||||
|
cat <<EOF >"${outputFile}"
|
||||||
|
log-level: debug
|
||||||
|
mode: rule
|
||||||
|
ipv6: true
|
||||||
|
mixed-port: 7890
|
||||||
|
allow-lan: true
|
||||||
|
bind-address: "*"
|
||||||
|
find-process-mode: strict
|
||||||
|
external-controller: 0.0.0.0:9090
|
||||||
|
global-client-fingerprint: chrome
|
||||||
|
|
||||||
|
profile:
|
||||||
|
store-selected: true
|
||||||
|
store-fake-ip: true
|
||||||
|
|
||||||
|
dns:
|
||||||
|
enable: true
|
||||||
|
listen: 0.0.0.0:1053
|
||||||
|
ipv6: true
|
||||||
|
enhanced-mode: fake-ip
|
||||||
|
fake-ip-range: 198.18.0.1/16
|
||||||
|
nameserver:
|
||||||
|
- https://1.1.1.1/dns-query
|
||||||
|
- https://8.8.8.8/dns-query
|
||||||
|
- 1.1.1.1
|
||||||
|
- 8.8.8.8
|
||||||
|
|
||||||
|
proxy-providers:
|
||||||
|
${providerName}:
|
||||||
|
type: http
|
||||||
|
path: ./${providerName}.yaml
|
||||||
|
url: ${providerURL}
|
||||||
|
interval: 3600
|
||||||
|
proxy: DIRECT
|
||||||
|
health-check:
|
||||||
|
enable: true
|
||||||
|
url: https://cp.cloudflare.com/generate_204
|
||||||
|
interval: 300
|
||||||
|
|
||||||
|
proxy-groups:
|
||||||
|
- name: Manual Select
|
||||||
|
type: select
|
||||||
|
use:
|
||||||
|
- ${providerName}
|
||||||
|
proxies: null
|
||||||
|
- name: Auto Select
|
||||||
|
type: url-test
|
||||||
|
url: http://www.gstatic.com/generate_204
|
||||||
|
interval: 36000
|
||||||
|
tolerance: 50
|
||||||
|
use:
|
||||||
|
- ${providerName}
|
||||||
|
proxies: null
|
||||||
|
- name: Global Proxy
|
||||||
|
type: select
|
||||||
|
use:
|
||||||
|
- ${providerName}
|
||||||
|
proxies:
|
||||||
|
- Manual Select
|
||||||
|
- Auto Select
|
||||||
|
- name: Final
|
||||||
|
type: select
|
||||||
|
use:
|
||||||
|
- ${providerName}
|
||||||
|
proxies:
|
||||||
|
- Global Proxy
|
||||||
|
- DIRECT
|
||||||
|
- Manual Select
|
||||||
|
- Auto Select
|
||||||
|
|
||||||
|
rules:
|
||||||
|
- GEOIP,LAN,DIRECT,no-resolve
|
||||||
|
- GEOSITE,private,DIRECT
|
||||||
|
- GEOSITE,cn,DIRECT
|
||||||
|
- GEOIP,CN,DIRECT
|
||||||
|
- MATCH,Final
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
initRandomSalt() {
|
||||||
|
local chars="abcdefghijklmnopqrtuxyz"
|
||||||
|
local randomSalt=""
|
||||||
|
local _idx
|
||||||
|
for _idx in 1 2 3 4 5 6 7 8 9 10; do
|
||||||
|
randomSalt+="${chars:RANDOM%${#chars}:1}"
|
||||||
|
done
|
||||||
|
printf '%s' "${randomSalt}"
|
||||||
|
}
|
||||||
|
|
||||||
|
md5Text() {
|
||||||
|
if command -v md5sum >/dev/null 2>&1; then
|
||||||
|
printf '%s' "$1" | md5sum | awk '{print $1}'
|
||||||
|
else
|
||||||
|
printf '%s' "$1" | md5 | awk '{print $NF}'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
base64FileSingleLine() {
|
||||||
|
if base64 --help 2>&1 | grep -q -- '-w'; then
|
||||||
|
base64 -w 0 "$1"
|
||||||
|
else
|
||||||
|
base64 <"$1" | tr -d '\n'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
generateSubscribeSalt() {
|
||||||
|
local saltFile="${dataDir%/}/subscribe_local/subscribeSalt"
|
||||||
|
if [[ -n "${persistedSubscribeSalt}" ]]; then
|
||||||
|
printf '%s' "${persistedSubscribeSalt}"
|
||||||
|
elif [[ -s "${saltFile}" ]]; then
|
||||||
|
cat "${saltFile}"
|
||||||
|
else
|
||||||
|
local newSalt
|
||||||
|
newSalt="$(initRandomSalt)"
|
||||||
|
mkdir -p "${dataDir%/}/subscribe_local"
|
||||||
|
printf '%s' "${newSalt}" >"${saltFile}"
|
||||||
|
printf '%s' "${newSalt}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
startSubscribeContainer() {
|
||||||
|
local displayAddress="$1"
|
||||||
|
local containerName="v2ray-agent-docker-subscribe"
|
||||||
|
local nginxImage="nginx:alpine"
|
||||||
|
local activePort="${persistedSubscribePort:-${resolvedSubscribePort}}"
|
||||||
|
local nginxConfig="${dataDir%/}/subscribe-nginx.conf"
|
||||||
|
|
||||||
|
if [[ -z "${activePort}" ]]; then
|
||||||
|
parsePort ""
|
||||||
|
activePort="${resolvedPort}"
|
||||||
|
checkPortInUse "${activePort}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "${dataDir%/}/subscribe/default" "${dataDir%/}/subscribe/clashMeta" "${dataDir%/}/subscribe/clashMetaProfiles"
|
||||||
|
cat >"${nginxConfig}" <<'EOF'
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name _;
|
||||||
|
root /usr/share/nginx/html;
|
||||||
|
location ~ ^/s/(default|clashMeta|clashMetaProfiles)/(.*) {
|
||||||
|
default_type 'text/plain; charset=utf-8';
|
||||||
|
alias /usr/share/nginx/html/subscribe/$1/$2;
|
||||||
|
}
|
||||||
|
location / {
|
||||||
|
return 404;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
docker pull "${nginxImage}" >/dev/null 2>&1 || true
|
||||||
|
if docker ps -a --filter "name=^/${containerName}$" --format '{{.Names}}' | grep -q "^${containerName}$"; then
|
||||||
|
docker rm -f "${containerName}" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
if ! docker run -d \
|
||||||
|
--name "${containerName}" \
|
||||||
|
--restart unless-stopped \
|
||||||
|
-p "${activePort}:80" \
|
||||||
|
-v "${dataDir%/}:/usr/share/nginx/html:ro" \
|
||||||
|
-v "${nginxConfig}:/etc/nginx/conf.d/default.conf:ro" \
|
||||||
|
"${nginxImage}" >/dev/null; then
|
||||||
|
echoContent "red" "Failed to start subscription access container"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
persistedSubscribePort="${activePort}"
|
||||||
|
echoContent "green" "Subscription access service started: http://${displayAddress}:${activePort}/s/default/<subscription-id>"
|
||||||
|
}
|
||||||
|
|
||||||
|
persistSubscribeState() {
|
||||||
|
local summaryFile="${dataDir%/}/client-summary.txt"
|
||||||
|
local tmpFile=""
|
||||||
|
|
||||||
|
[[ -f "${summaryFile}" ]] || return 0
|
||||||
|
tmpFile="$(mktemp)"
|
||||||
|
while IFS= read -r line; do
|
||||||
|
case "${line}" in
|
||||||
|
subscribePort:\ * | subscribeSalt:\ *) ;;
|
||||||
|
*) printf '%s\n' "${line}" >>"${tmpFile}" ;;
|
||||||
|
esac
|
||||||
|
done <"${summaryFile}"
|
||||||
|
printf 'subscribePort: %s\n' "${persistedSubscribePort}" >>"${tmpFile}"
|
||||||
|
printf 'subscribeSalt: %s\n' "${persistedSubscribeSalt}" >>"${tmpFile}"
|
||||||
|
mv "${tmpFile}" "${summaryFile}"
|
||||||
|
chmod 600 "${summaryFile}"
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureSubscribeRuntimeValues() {
|
||||||
|
local inputPort=""
|
||||||
|
|
||||||
|
if [[ -z "${persistedSubscribePort}" ]]; then
|
||||||
|
inputPort="$(promptValue $'Enter subscription access port, [Enter] random: ' "")"
|
||||||
|
if ! parsePort "${inputPort}"; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
persistedSubscribePort="${resolvedPort}"
|
||||||
|
fi
|
||||||
|
if [[ -z "${persistedSubscribeSalt}" ]]; then
|
||||||
|
persistedSubscribeSalt="$(initRandomSalt)"
|
||||||
|
fi
|
||||||
|
persistSubscribeState
|
||||||
|
}
|
||||||
|
|
||||||
installModeHasVision() {
|
installModeHasVision() {
|
||||||
[[ "$1" == "vision" || "$1" == "all" ]]
|
[[ "$1" == "vision" || "$1" == "all" ]]
|
||||||
}
|
}
|
||||||
@@ -357,6 +723,14 @@ parseCli() {
|
|||||||
email="$2"
|
email="$2"
|
||||||
shift 2
|
shift 2
|
||||||
;;
|
;;
|
||||||
|
--subscribe-port)
|
||||||
|
if [[ $# -lt 2 || ( -n "${2:-}" && "${2:0:1}" == "-" ) ]]; then
|
||||||
|
echoContent "red" "--subscribe-port requires a numeric value"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
subscribePort="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
--generate-only)
|
--generate-only)
|
||||||
generateOnly=1
|
generateOnly=1
|
||||||
shift
|
shift
|
||||||
@@ -625,7 +999,7 @@ checkPortInUse() {
|
|||||||
# If all listening PIDs belong to that container's docker-proxy processes, allow the port.
|
# If all listening PIDs belong to that container's docker-proxy processes, allow the port.
|
||||||
local containerExists=0
|
local containerExists=0
|
||||||
if docker ps -a --filter "name=^/v2ray-agent-docker$" --format '{{.Names}}' 2>/dev/null \
|
if docker ps -a --filter "name=^/v2ray-agent-docker$" --format '{{.Names}}' 2>/dev/null \
|
||||||
| grep -q "^v2ray-agent-docker$"; then
|
| grep -q "^v2ray-agent-docker$" || docker ps -a --filter "name=^/v2ray-agent-docker-subscribe$" --format '{{.Names}}' 2>/dev/null | grep -q "^v2ray-agent-docker-subscribe$"; then
|
||||||
containerExists=1
|
containerExists=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -789,6 +1163,8 @@ loadPersistedSummaryIfPresent() {
|
|||||||
persistedVisionEmail=""
|
persistedVisionEmail=""
|
||||||
persistedXHTTPEmail=""
|
persistedXHTTPEmail=""
|
||||||
persistedShortId="6ba85179e30d4fc2"
|
persistedShortId="6ba85179e30d4fc2"
|
||||||
|
persistedSubscribePort=""
|
||||||
|
persistedSubscribeSalt=""
|
||||||
persistedConfigPath=""
|
persistedConfigPath=""
|
||||||
|
|
||||||
if [[ ! -f "${summaryFile}" ]]; then
|
if [[ ! -f "${summaryFile}" ]]; then
|
||||||
@@ -809,6 +1185,8 @@ loadPersistedSummaryIfPresent() {
|
|||||||
visionEmail:\ *) persistedVisionEmail="${line#visionEmail: }" ;;
|
visionEmail:\ *) persistedVisionEmail="${line#visionEmail: }" ;;
|
||||||
xhttpEmail:\ *) persistedXHTTPEmail="${line#xhttpEmail: }" ;;
|
xhttpEmail:\ *) persistedXHTTPEmail="${line#xhttpEmail: }" ;;
|
||||||
shortId:\ *) persistedShortId="${line#shortId: }" ;;
|
shortId:\ *) persistedShortId="${line#shortId: }" ;;
|
||||||
|
subscribePort:\ *) persistedSubscribePort="${line#subscribePort: }" ;;
|
||||||
|
subscribeSalt:\ *) persistedSubscribeSalt="${line#subscribeSalt: }" ;;
|
||||||
configPath:\ *) persistedConfigPath="${line#configPath: }" ;;
|
configPath:\ *) persistedConfigPath="${line#configPath: }" ;;
|
||||||
esac
|
esac
|
||||||
done <"${summaryFile}"
|
done <"${summaryFile}"
|
||||||
@@ -825,6 +1203,8 @@ loadPersistedSummaryIfPresent() {
|
|||||||
persistedVisionEmail="${persistedVisionEmail#"${persistedVisionEmail%%[![:space:]]*}"}"
|
persistedVisionEmail="${persistedVisionEmail#"${persistedVisionEmail%%[![:space:]]*}"}"
|
||||||
persistedXHTTPEmail="${persistedXHTTPEmail#"${persistedXHTTPEmail%%[![:space:]]*}"}"
|
persistedXHTTPEmail="${persistedXHTTPEmail#"${persistedXHTTPEmail%%[![:space:]]*}"}"
|
||||||
persistedShortId="${persistedShortId#"${persistedShortId%%[![:space:]]*}"}"
|
persistedShortId="${persistedShortId#"${persistedShortId%%[![:space:]]*}"}"
|
||||||
|
persistedSubscribePort="${persistedSubscribePort#"${persistedSubscribePort%%[![:space:]]*}"}"
|
||||||
|
persistedSubscribeSalt="${persistedSubscribeSalt#"${persistedSubscribeSalt%%[![:space:]]*}"}"
|
||||||
persistedConfigPath="${persistedConfigPath#"${persistedConfigPath%%[![:space:]]*}"}"
|
persistedConfigPath="${persistedConfigPath#"${persistedConfigPath%%[![:space:]]*}"}"
|
||||||
|
|
||||||
if [[ -n "${persistedContainer}" && "${persistedContainer}" != "v2ray-agent-docker" ]]; then
|
if [[ -n "${persistedContainer}" && "${persistedContainer}" != "v2ray-agent-docker" ]]; then
|
||||||
@@ -1146,6 +1526,41 @@ hasExistingInstallState() {
|
|||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userManageMenu() {
|
||||||
|
local action=""
|
||||||
|
|
||||||
|
if ! hasPersistedConfig; then
|
||||||
|
echoContent "red" "No existing config found. Cannot enter user management; reinstall first."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echoContent "skyBlue" "─── User Management ─────────────────────────────────────"
|
||||||
|
echoContent "yellow" "1. View account"
|
||||||
|
echoContent "yellow" "2. View subscription"
|
||||||
|
echoContent "yellow" "3. Exit"
|
||||||
|
|
||||||
|
while true; do
|
||||||
|
action="$(promptValue $'Choose [1-3]: ' "")"
|
||||||
|
case "${action}" in
|
||||||
|
1)
|
||||||
|
showAccountInfo
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
showSubscriptionInfo
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
echoContent "white" "Exiting without changes."
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echoContent "red" "Invalid selection. Please enter 1-3."
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
# uninstallDockerReality — remove the standalone Docker Reality container, data, script shortcut, and installed script copy.
|
# uninstallDockerReality — remove the standalone Docker Reality container, data, script shortcut, and installed script copy.
|
||||||
uninstallDockerReality() {
|
uninstallDockerReality() {
|
||||||
local answer=""
|
local answer=""
|
||||||
@@ -1165,6 +1580,10 @@ uninstallDockerReality() {
|
|||||||
docker rm -f v2ray-agent-docker >/dev/null 2>&1 || true
|
docker rm -f v2ray-agent-docker >/dev/null 2>&1 || true
|
||||||
echoContent "green" " ---> Docker container removed"
|
echoContent "green" " ---> Docker container removed"
|
||||||
fi
|
fi
|
||||||
|
if docker ps -a --filter "name=^/v2ray-agent-docker-subscribe$" --format '{{.Names}}' 2>/dev/null | grep -q '^v2ray-agent-docker-subscribe$'; then
|
||||||
|
docker rm -f v2ray-agent-docker-subscribe >/dev/null 2>&1 || true
|
||||||
|
echoContent "green" " ---> Subscription access container removed"
|
||||||
|
fi
|
||||||
|
|
||||||
rm -rf "${dataDir%/}" >/dev/null 2>&1 || true
|
rm -rf "${dataDir%/}" >/dev/null 2>&1 || true
|
||||||
echoContent "green" " ---> Docker Reality data directory removed"
|
echoContent "green" " ---> Docker Reality data directory removed"
|
||||||
@@ -1231,7 +1650,7 @@ promptExistingInstallAction() {
|
|||||||
else
|
else
|
||||||
echoContent "white" "Detected the container but not the config file. Choose the next action:"
|
echoContent "white" "Detected the container but not the config file. Choose the next action:"
|
||||||
fi
|
fi
|
||||||
echoContent "yellow" "1. View account"
|
echoContent "yellow" "1. User management"
|
||||||
echoContent "yellow" "2. Reinstall"
|
echoContent "yellow" "2. Reinstall"
|
||||||
echoContent "yellow" "3. Start/Recreate container"
|
echoContent "yellow" "3. Start/Recreate container"
|
||||||
echoContent "yellow" "4. Uninstall"
|
echoContent "yellow" "4. Uninstall"
|
||||||
@@ -1242,10 +1661,10 @@ promptExistingInstallAction() {
|
|||||||
case "${action}" in
|
case "${action}" in
|
||||||
1)
|
1)
|
||||||
if hasPersistedConfig; then
|
if hasPersistedConfig; then
|
||||||
showClientInfo
|
userManageMenu
|
||||||
exit 0
|
exit 0
|
||||||
else
|
else
|
||||||
echoContent "red" "No existing config was found, so the account cannot be shown. Please reinstall first."
|
echoContent "red" "No existing config was found, so user management cannot be opened. Please reinstall first."
|
||||||
fi
|
fi
|
||||||
;;
|
;;
|
||||||
2)
|
2)
|
||||||
@@ -1295,7 +1714,11 @@ showVisionAccount() {
|
|||||||
displayAddress="YOUR_SERVER_IP"
|
displayAddress="YOUR_SERVER_IP"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
vlessLink="vless://${displayUUID}@${displayAddress}:${displayPort}?encryption=none&security=reality&type=tcp&sni=${displayServerName}&fp=chrome&pbk=${displayPublicKey}&sid=${displayShortId}&flow=xtls-rprx-vision#${displayEmail}"
|
vlessLink="$(buildVisionSubscriptionLink "${displayAddress}" "${displayPort}" "${displayServerName}" "${displayPublicKey}" "${displayUUID}" "${displayEmail}" "${displayShortId}")"
|
||||||
|
if [[ -z "${vlessLink}" ]]; then
|
||||||
|
echoContent "yellow" " ---> Vision account data is incomplete; skipping subscription link output"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
qrData="${vlessLink//:/%3A}"
|
qrData="${vlessLink//:/%3A}"
|
||||||
qrData="${qrData//\//%2F}"
|
qrData="${qrData//\//%2F}"
|
||||||
qrData="${qrData//@/%40}"
|
qrData="${qrData//@/%40}"
|
||||||
@@ -1336,7 +1759,11 @@ showXHTTPAccount() {
|
|||||||
displayAddress="YOUR_SERVER_IP"
|
displayAddress="YOUR_SERVER_IP"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
vlessLink="vless://${displayUUID}@${displayAddress}:${displayPort}?encryption=none&security=reality&type=xhttp&sni=${displayServerName}&host=${displayServerName}&fp=chrome&path=${displayPath}&pbk=${displayPublicKey}&sid=${displayShortId}#${displayEmail}"
|
vlessLink="$(buildXHTTPSubscriptionLink "${displayAddress}" "${displayPort}" "${displayServerName}" "${displayPublicKey}" "${displayUUID}" "${displayEmail}" "${displayShortId}" "${displayPath}")"
|
||||||
|
if [[ -z "${vlessLink}" ]]; then
|
||||||
|
echoContent "yellow" " ---> XHTTP account data is incomplete; skipping subscription link output"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
qrLink="https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=vless%3A%2F%2F${displayUUID}%40${displayAddress}%3A${displayPort}%3Fencryption%3Dnone%26security%3Dreality%26type%3Dxhttp%26sni%3D${displayServerName}%26fp%3Dchrome%26path%3D${displayPath}%26host%3D${displayServerName}%26pbk%3D${displayPublicKey}%26sid%3D${displayShortId}%23${displayEmail}"
|
qrLink="https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=vless%3A%2F%2F${displayUUID}%40${displayAddress}%3A${displayPort}%3Fencryption%3Dnone%26security%3Dreality%26type%3Dxhttp%26sni%3D${displayServerName}%26fp%3Dchrome%26path%3D${displayPath}%26host%3D${displayServerName}%26pbk%3D${displayPublicKey}%26sid%3D${displayShortId}%23${displayEmail}"
|
||||||
|
|
||||||
echoContent "skyBlue" "============================= VLESS Reality XHTTP =============================="
|
echoContent "skyBlue" "============================= VLESS Reality XHTTP =============================="
|
||||||
@@ -1353,6 +1780,106 @@ showXHTTPAccount() {
|
|||||||
echoContent "green" " ${qrLink}"
|
echoContent "green" " ${qrLink}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# showEnglishSubscriptionSection — print the local subscription content and security note.
|
||||||
|
showEnglishSubscriptionSection() {
|
||||||
|
local displayAddress="$1"
|
||||||
|
local mode="$2"
|
||||||
|
local displayServerName="$3"
|
||||||
|
local displayPublicKey="$4"
|
||||||
|
local displayUUID="$5"
|
||||||
|
local displayVisionPort="$6"
|
||||||
|
local displayXHTTPPort="$7"
|
||||||
|
local displayXHTTPPath="$8"
|
||||||
|
local displayVisionEmail="$9"
|
||||||
|
local displayXHTTPEmail="${10}"
|
||||||
|
local displayShortId="${11}"
|
||||||
|
local visionLink=""
|
||||||
|
local xhttpLink=""
|
||||||
|
local renderedPath=""
|
||||||
|
local subscribeName=""
|
||||||
|
local subscribeSalt=""
|
||||||
|
local emailMd5=""
|
||||||
|
local defaultLocalFile=""
|
||||||
|
local defaultPublicFile=""
|
||||||
|
local clashLocalFile=""
|
||||||
|
local clashPublicFile=""
|
||||||
|
local clashProfileFile=""
|
||||||
|
local subscribeDomain=""
|
||||||
|
local subscribeURL=""
|
||||||
|
local clashProxyURL=""
|
||||||
|
local clashProfileURL=""
|
||||||
|
|
||||||
|
if [[ -z "${displayAddress}" ]]; then
|
||||||
|
displayAddress="YOUR_SERVER_IP"
|
||||||
|
fi
|
||||||
|
|
||||||
|
subscribeName="${displayVisionEmail:-${displayXHTTPEmail}}"
|
||||||
|
subscribeName="${subscribeName%%-*}"
|
||||||
|
if [[ -z "${subscribeName}" ]]; then
|
||||||
|
echoContent "yellow" " ---> Subscription account data is incomplete; skipping subscription file generation"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "${dataDir%/}/subscribe_local/default" "${dataDir%/}/subscribe_local/clashMeta" "${dataDir%/}/subscribe/default" "${dataDir%/}/subscribe/clashMeta" "${dataDir%/}/subscribe/clashMetaProfiles"
|
||||||
|
defaultLocalFile="${dataDir%/}/subscribe_local/default/${subscribeName}"
|
||||||
|
clashLocalFile="${dataDir%/}/subscribe_local/clashMeta/${subscribeName}"
|
||||||
|
: >"${defaultLocalFile}"
|
||||||
|
: >"${clashLocalFile}"
|
||||||
|
|
||||||
|
if installModeHasVision "${mode}"; then
|
||||||
|
visionLink="$(buildVisionSubscriptionLink "${displayAddress}" "${displayVisionPort}" "${displayServerName}" "${displayPublicKey}" "${displayUUID}" "${displayVisionEmail}" "${displayShortId}")" || visionLink=""
|
||||||
|
if [[ -n "${visionLink}" ]]; then
|
||||||
|
printf '%s\n' "${visionLink}" >>"${defaultLocalFile}"
|
||||||
|
writeVisionClashMetaNode "${clashLocalFile}" "${displayAddress}" "${displayVisionPort}" "${displayServerName}" "${displayPublicKey}" "${displayUUID}" "${displayVisionEmail}" "${displayShortId}" || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if installModeHasXHTTP "${mode}"; then
|
||||||
|
renderedPath="$(renderXHTTPPath "${displayXHTTPPath}")"
|
||||||
|
xhttpLink="$(buildXHTTPDefaultSubscriptionLink "${displayAddress}" "${displayXHTTPPort}" "${displayServerName}" "${displayPublicKey}" "${displayUUID}" "${displayXHTTPEmail}" "${displayShortId}" "${renderedPath}")" || xhttpLink=""
|
||||||
|
if [[ -n "${xhttpLink}" ]]; then
|
||||||
|
printf '%s\n' "${xhttpLink}" >>"${defaultLocalFile}"
|
||||||
|
writeXHTTPClashMetaNode "${clashLocalFile}" "${displayAddress}" "${displayXHTTPPort}" "${displayServerName}" "${displayPublicKey}" "${displayUUID}" "${displayXHTTPEmail}" "${displayShortId}" "${renderedPath}" || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -s "${defaultLocalFile}" ]]; then
|
||||||
|
echoContent "yellow" " ---> Subscription account data is incomplete; no usable subscription content was generated"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
subscribeSalt="$(generateSubscribeSalt)"
|
||||||
|
emailMd5="$(md5Text "${subscribeName}${subscribeSalt}"$'\n')"
|
||||||
|
defaultPublicFile="${dataDir%/}/subscribe/default/${emailMd5}"
|
||||||
|
base64FileSingleLine "${defaultLocalFile}" >"${defaultPublicFile}"
|
||||||
|
|
||||||
|
startSubscribeContainer "${displayAddress}"
|
||||||
|
subscribeDomain="${displayAddress}:${persistedSubscribePort:-${resolvedSubscribePort}}"
|
||||||
|
subscribeURL="http://${subscribeDomain}/s/default/${emailMd5}"
|
||||||
|
if [[ -s "${clashLocalFile}" ]]; then
|
||||||
|
clashPublicFile="${dataDir%/}/subscribe/clashMeta/${emailMd5}"
|
||||||
|
clashProfileFile="${dataDir%/}/subscribe/clashMetaProfiles/${emailMd5}"
|
||||||
|
clashProxyURL="http://${subscribeDomain}/s/clashMeta/${emailMd5}"
|
||||||
|
clashProfileURL="http://${subscribeDomain}/s/clashMetaProfiles/${emailMd5}"
|
||||||
|
printf 'proxies:\n' >"${clashPublicFile}"
|
||||||
|
cat "${clashLocalFile}" >>"${clashPublicFile}"
|
||||||
|
writeClashMetaProfile "${clashProfileFile}" "${clashProxyURL}" "${subscribeSalt}_provider"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echoContent "skyBlue" "============================= Default subscription =============================="
|
||||||
|
echoContent "white" "Plain HTTP subscription transport can expose UUID, Reality parameters, SNI, path, ports, and connection details."
|
||||||
|
echoContent "white" "The Docker edition serves this subscription through a local HTTP container; do not share the URL over untrusted networks."
|
||||||
|
echoContent "white" ""
|
||||||
|
echoContent "green" "email:${subscribeName}"
|
||||||
|
echoContent "yellow" "url:${subscribeURL}"
|
||||||
|
echoContent "yellow" "Online QR code:https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=$(urlEncode "${subscribeURL}")"
|
||||||
|
if [[ -n "${clashProfileURL}" ]]; then
|
||||||
|
echoContent "skyBlue" "\n----------clashMeta/Clash Verge subscription----------\n"
|
||||||
|
echoContent "yellow" "url:${clashProfileURL}"
|
||||||
|
echoContent "yellow" "Online QR code:https://api.qrserver.com/v1/create-qr-code/?size=400x400&data=$(urlEncode "${clashProfileURL}")"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
# resolveValues — resolve all CLI-supplied or blank values into resolved* globals.
|
# resolveValues — resolve all CLI-supplied or blank values into resolved* globals.
|
||||||
# Depends on: installMode, port, xhttpPort, xhttpPath, serverName, privateKey, uuid, email
|
# Depends on: installMode, port, xhttpPort, xhttpPath, serverName, privateKey, uuid, email
|
||||||
# Sets: resolvedInstallMode, resolvedVisionPort, resolvedXHTTPPort, resolvedXHTTPPath,
|
# Sets: resolvedInstallMode, resolvedVisionPort, resolvedXHTTPPort, resolvedXHTTPPath,
|
||||||
@@ -1393,6 +1920,20 @@ resolveValues() {
|
|||||||
resolvedXHTTPPath="${resolvedXHTTPPath:-${resolvedXHTTPPath}}"
|
resolvedXHTTPPath="${resolvedXHTTPPath:-${resolvedXHTTPPath}}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if ! parsePort "${subscribePort}"; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
resolvedSubscribePort="${resolvedPort}"
|
||||||
|
if installModeHasVision "${resolvedInstallMode}" && [[ "${resolvedSubscribePort}" == "${resolvedVisionPort}" ]]; then
|
||||||
|
echoContent "red" "Subscription port (${resolvedSubscribePort}) is the same as the Vision port — please use different ports"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if installModeHasXHTTP "${resolvedInstallMode}" && [[ "${resolvedSubscribePort}" == "${resolvedXHTTPPort}" ]]; then
|
||||||
|
echoContent "red" "Subscription port (${resolvedSubscribePort}) is the same as the XHTTP port — please use different ports"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
checkPortInUse "${resolvedSubscribePort}"
|
||||||
|
|
||||||
# --- Server name ---
|
# --- Server name ---
|
||||||
parseServerName "${serverName}"
|
parseServerName "${serverName}"
|
||||||
|
|
||||||
@@ -1694,6 +2235,8 @@ EOF
|
|||||||
printf 'xhttpEmail: %s\n' "${resolvedXHTTPEmail}"
|
printf 'xhttpEmail: %s\n' "${resolvedXHTTPEmail}"
|
||||||
fi
|
fi
|
||||||
printf 'shortId: 6ba85179e30d4fc2\n'
|
printf 'shortId: 6ba85179e30d4fc2\n'
|
||||||
|
printf 'subscribePort: %s\n' "${resolvedSubscribePort}"
|
||||||
|
printf 'subscribeSalt: %s\n' "$(initRandomSalt)"
|
||||||
printf 'configPath: %s\n' "${configFile}"
|
printf 'configPath: %s\n' "${configFile}"
|
||||||
} >"${summaryFile}"
|
} >"${summaryFile}"
|
||||||
chmod 600 "${summaryFile}"
|
chmod 600 "${summaryFile}"
|
||||||
@@ -1803,9 +2346,9 @@ startContainer() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# showClientInfo — print the current Reality account in a showAccounts-style layout.
|
# showAccountInfo — print the current Reality account in a showAccounts-style layout.
|
||||||
# Displays only the protocol blocks that are actually installed.
|
# Displays only the protocol blocks that are actually installed.
|
||||||
showClientInfo() {
|
showAccountInfo() {
|
||||||
local displayAddress=""
|
local displayAddress=""
|
||||||
|
|
||||||
loadPersistedAccountInfo
|
loadPersistedAccountInfo
|
||||||
@@ -1838,6 +2381,36 @@ showClientInfo() {
|
|||||||
"${shortId}" \
|
"${shortId}" \
|
||||||
"${renderedPath}"
|
"${renderedPath}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
showSubscriptionInfo() {
|
||||||
|
local displayAddress=""
|
||||||
|
local shortId="${persistedShortId:-6ba85179e30d4fc2}"
|
||||||
|
local mode=""
|
||||||
|
|
||||||
|
loadPersistedAccountInfo
|
||||||
|
ensureSubscribeRuntimeValues
|
||||||
|
displayAddress="$(getPublicIP)"
|
||||||
|
shortId="${persistedShortId:-6ba85179e30d4fc2}"
|
||||||
|
mode="${persistedInstallMode}"
|
||||||
|
|
||||||
|
showEnglishSubscriptionSection \
|
||||||
|
"${displayAddress}" \
|
||||||
|
"${mode}" \
|
||||||
|
"${persistedServerName}" \
|
||||||
|
"${persistedPublicKey}" \
|
||||||
|
"${persistedUUID}" \
|
||||||
|
"${persistedVisionPort}" \
|
||||||
|
"${persistedXHTTPPort}" \
|
||||||
|
"${persistedXHTTPPath}" \
|
||||||
|
"${persistedVisionEmail}" \
|
||||||
|
"${persistedXHTTPEmail}" \
|
||||||
|
"${shortId}"
|
||||||
|
}
|
||||||
|
|
||||||
|
showClientInfo() {
|
||||||
|
showAccountInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user