Compare commits
23 Commits
6f6f640f4f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb65230932 | ||
| ba84fd3811 | |||
| 7de15a73e8 | |||
| bbc07051b6 | |||
| fd9cb824b9 | |||
| 09ec13d0b3 | |||
| 2f5e5b7a59 | |||
| a800541c36 | |||
| 3d99fc13ba | |||
| 575332e78d | |||
| 0ff27faa45 | |||
|
|
d919c0f93d | ||
|
|
45399fc8c6 | ||
|
|
c020494cb5 | ||
|
|
a51ddcbd42 | ||
|
|
1476c1a52c | ||
|
|
ef31ad3409 | ||
|
|
1e32102f89 | ||
|
|
add7e4f1a3 | ||
|
|
d4f3d96837 | ||
|
|
7d16d2112f | ||
|
|
7d0c441bc6 | ||
|
|
618bd89a33 |
55
app.py
55
app.py
@@ -608,6 +608,12 @@ def format_price(node: Node) -> str:
|
|||||||
return f"{amount:.2f} {currency}/{cycle_label(node.price_cycle)}"
|
return f"{amount:.2f} {currency}/{cycle_label(node.price_cycle)}"
|
||||||
|
|
||||||
|
|
||||||
|
def next_renewal_date_text(node: Node) -> str:
|
||||||
|
if not int(node.expires_at or 0):
|
||||||
|
return "未设置"
|
||||||
|
return format_expiry(node.expires_at)
|
||||||
|
|
||||||
|
|
||||||
def annual_cost_total():
|
def annual_cost_total():
|
||||||
return round(monthly_cost_total() * 12, 2)
|
return round(monthly_cost_total() * 12, 2)
|
||||||
|
|
||||||
@@ -657,7 +663,7 @@ async def monitor_once(app: Application):
|
|||||||
set_node_state(node.id, fail_count, is_online, str(e))
|
set_node_state(node.id, fail_count, is_online, str(e))
|
||||||
|
|
||||||
if int(node.expires_at or 0) > 0 and expire_days > 0:
|
if int(node.expires_at or 0) > 0 and expire_days > 0:
|
||||||
days_left = math.floor((int(node.expires_at) - now) / 86400)
|
days_left = math.ceil((int(node.expires_at) - now) / 86400)
|
||||||
exp_state = get_expiry_state(node.id)
|
exp_state = get_expiry_state(node.id)
|
||||||
remind_points = sorted({expire_days, 7, 3, 0}, reverse=True)
|
remind_points = sorted({expire_days, 7, 3, 0}, reverse=True)
|
||||||
should_notify = False
|
should_notify = False
|
||||||
@@ -1169,6 +1175,7 @@ async def show_billing_menu(query, node_id: int):
|
|||||||
[InlineKeyboardButton("改备注", callback_data=f"node:editfield:{node_id}:remark"), InlineKeyboardButton("改金额", callback_data=f"node:editfield:{node_id}:monthly_price")],
|
[InlineKeyboardButton("改备注", callback_data=f"node:editfield:{node_id}:remark"), InlineKeyboardButton("改金额", callback_data=f"node:editfield:{node_id}:monthly_price")],
|
||||||
[InlineKeyboardButton("周期", callback_data=f"node:cycle:{node_id}"), InlineKeyboardButton("货币", callback_data=f"node:currency:{node_id}")],
|
[InlineKeyboardButton("周期", callback_data=f"node:cycle:{node_id}"), InlineKeyboardButton("货币", callback_data=f"node:currency:{node_id}")],
|
||||||
[InlineKeyboardButton("改到期日", callback_data=f"node:editfield:{node_id}:expires_at")],
|
[InlineKeyboardButton("改到期日", callback_data=f"node:editfield:{node_id}:expires_at")],
|
||||||
|
[InlineKeyboardButton("💳 标记已续费", callback_data=f"node:renew:{node_id}")],
|
||||||
[InlineKeyboardButton("⬅️ 返回节点", callback_data=f"node:view:{node_id}")],
|
[InlineKeyboardButton("⬅️ 返回节点", callback_data=f"node:view:{node_id}")],
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
@@ -1208,6 +1215,50 @@ async def show_currency_menu(query, node_id: int):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def renew_node(query, node_id: int):
|
||||||
|
node = load_node(node_id)
|
||||||
|
if not node:
|
||||||
|
await query.edit_message_text("节点不存在", reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("🏠 首页", callback_data="home")]]))
|
||||||
|
return
|
||||||
|
if not int(node.expires_at or 0):
|
||||||
|
await query.edit_message_text(
|
||||||
|
f"<b>{esc(node.name)}</b> 还没设置到期日,没法直接续费。",
|
||||||
|
parse_mode=ParseMode.HTML,
|
||||||
|
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ 返回账单", callback_data=f"node:billing:{node_id}")]]),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
cycle = (node.price_cycle or "month").lower()
|
||||||
|
months_map = {"month": 1, "quarter": 3, "year": 12}
|
||||||
|
if cycle not in months_map:
|
||||||
|
await query.edit_message_text(
|
||||||
|
f"<b>{esc(node.name)}</b> 的账单周期 <code>{esc(cycle)}</code> 暂不支持自动续费。",
|
||||||
|
parse_mode=ParseMode.HTML,
|
||||||
|
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ 返回账单", callback_data=f"node:billing:{node_id}")]]),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
current = datetime.fromtimestamp(int(node.expires_at))
|
||||||
|
import calendar
|
||||||
|
month_index = current.month - 1 + months_map[cycle]
|
||||||
|
year = current.year + month_index // 12
|
||||||
|
month = month_index % 12 + 1
|
||||||
|
day = min(current.day, calendar.monthrange(year, month)[1])
|
||||||
|
new_dt = current.replace(year=year, month=month, day=day)
|
||||||
|
update_node_field(node_id, "expires_at", int(new_dt.timestamp()))
|
||||||
|
conn = db()
|
||||||
|
conn.execute("INSERT INTO expiry_state(node_id,last_days_left,last_notified_at) VALUES(?,?,?) ON CONFLICT(node_id) DO UPDATE SET last_days_left=excluded.last_days_left,last_notified_at=excluded.last_notified_at", (node_id, 999999, 0))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
node = load_node(node_id)
|
||||||
|
await query.edit_message_text(
|
||||||
|
f"✅ <b>{esc(node.name)}</b> 已续费\n\n账单:<code>{esc(format_price(node))}</code>\n新到期:<code>{esc(format_expiry(node.expires_at))}</code>\n剩余:<code>{esc(days_left_text(node.expires_at))}</code>",
|
||||||
|
parse_mode=ParseMode.HTML,
|
||||||
|
reply_markup=InlineKeyboardMarkup([
|
||||||
|
[InlineKeyboardButton("💰 返回账单", callback_data=f"node:billing:{node_id}")],
|
||||||
|
[InlineKeyboardButton("📄 返回节点", callback_data=f"node:view:{node_id}")],
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
async def show_edit_menu(query, node_id: int):
|
async def show_edit_menu(query, node_id: int):
|
||||||
node = load_node(node_id)
|
node = load_node(node_id)
|
||||||
if not node:
|
if not node:
|
||||||
@@ -1550,6 +1601,8 @@ async def on_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
|||||||
_, _, node_id, currency = data.split(":", 3)
|
_, _, node_id, currency = data.split(":", 3)
|
||||||
update_node_field(int(node_id), "price_currency", currency.upper())
|
update_node_field(int(node_id), "price_currency", currency.upper())
|
||||||
await show_billing_menu(q, int(node_id))
|
await show_billing_menu(q, int(node_id))
|
||||||
|
elif data.startswith("node:renew:"):
|
||||||
|
await renew_node(q, int(data.split(":")[-1]))
|
||||||
elif data.startswith("node:edit:"):
|
elif data.startswith("node:edit:"):
|
||||||
await show_edit_menu(q, int(data.split(":")[-1]))
|
await show_edit_menu(q, int(data.split(":")[-1]))
|
||||||
elif data.startswith("node:editfield:"):
|
elif data.startswith("node:editfield:"):
|
||||||
|
|||||||
570
projects/gitea/gitea-backup/stream-unlock.sh
Normal file
570
projects/gitea/gitea-backup/stream-unlock.sh
Normal file
@@ -0,0 +1,570 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Stream Unlock Installer
|
||||||
|
# 流媒体/AI 解锁一键脚本
|
||||||
|
#
|
||||||
|
# 解锁机:安装 sniproxy,提供 DNS 解锁服务
|
||||||
|
# 被解锁机:安装 smartdns,分流指定服务到解锁机
|
||||||
|
#
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
# 颜色定义
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m'
|
||||||
|
|
||||||
|
# 服务域名配置
|
||||||
|
declare -A SERVICE_DOMAINS
|
||||||
|
|
||||||
|
# 流媒体全家桶
|
||||||
|
SERVICE_DOMAINS["stream"]="netflix.com netflix.net nflximg.com nflximg.net nflxvideo.net nflxext.com nflxso.net
|
||||||
|
disneyplus.com disney-plus.net dssott.com bamgrid.com
|
||||||
|
hbomax.com hbo.com hbogo.com hbonow.com
|
||||||
|
primevideo.com amazon.com amazon.co.jp amazon.co.uk
|
||||||
|
paramountplus.com cbs.com
|
||||||
|
peacocktv.com nbc.com
|
||||||
|
apple.com apple-tv.com
|
||||||
|
crunchyroll.com funimation.com
|
||||||
|
tvb.com tvbanywhere.com
|
||||||
|
bilibili.com bilibili.tv
|
||||||
|
iq.com iqiyi.com
|
||||||
|
youku.com"
|
||||||
|
|
||||||
|
# AI 服务
|
||||||
|
SERVICE_DOMAINS["ai"]="openai.com chatgpt.com ai.com
|
||||||
|
anthropic.com claude.ai
|
||||||
|
gemini.google.com generativelanguage.googleapis.com
|
||||||
|
copilot.microsoft.com
|
||||||
|
perplexity.ai
|
||||||
|
midjourney.com
|
||||||
|
character.ai
|
||||||
|
poe.com"
|
||||||
|
|
||||||
|
# 短视频
|
||||||
|
SERVICE_DOMAINS["shorts"]="tiktok.com tiktokv.com tiktokcdn.com tiktokcdn-us.com byteoversea.com musical.ly
|
||||||
|
youtube.com youtu.be ytimg.com googlevideo.com youtubei.googleapis.com youtube-nocookie.com"
|
||||||
|
|
||||||
|
# Spotify
|
||||||
|
SERVICE_DOMAINS["spotify"]="spotify.com scdn.co spotifycdn.com spotifycdn.net"
|
||||||
|
|
||||||
|
# 游戏相关
|
||||||
|
SERVICE_DOMAINS["game"]="playstation.com playstation.net psn.com
|
||||||
|
xbox.com xboxlive.com
|
||||||
|
nintendo.com nintendo.net
|
||||||
|
steam.com steampowered.com steamcommunity.com steamstatic.com
|
||||||
|
epicgames.com epicgames.net"
|
||||||
|
|
||||||
|
# ChatGPT 详细域名
|
||||||
|
SERVICE_DOMAINS["chatgpt"]="openai.com chatgpt.com ai.com oaistatic.com oaiusercontent.com auth0.openai.com"
|
||||||
|
|
||||||
|
# Claude 详细域名
|
||||||
|
SERVICE_DOMAINS["claude"]="anthropic.com claude.ai statsig.anthropic.com sentry.io"
|
||||||
|
|
||||||
|
# ============ 工具函数 ============
|
||||||
|
|
||||||
|
print_banner() {
|
||||||
|
echo -e "${BLUE}"
|
||||||
|
echo "╔═══════════════════════════════════════════╗"
|
||||||
|
echo "║ Stream Unlock Installer v1.0 ║"
|
||||||
|
echo "║ 流媒体/AI 解锁一键脚本 ║"
|
||||||
|
echo "╚═══════════════════════════════════════════╝"
|
||||||
|
echo -e "${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
check_root() {
|
||||||
|
if [[ $EUID -ne 0 ]]; then
|
||||||
|
echo -e "${RED}请使用 root 用户运行此脚本${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
detect_os() {
|
||||||
|
if [[ -f /etc/debian_version ]]; then
|
||||||
|
OS="debian"
|
||||||
|
PKG_MANAGER="apt"
|
||||||
|
elif [[ -f /etc/redhat-release ]]; then
|
||||||
|
OS="centos"
|
||||||
|
PKG_MANAGER="yum"
|
||||||
|
else
|
||||||
|
echo -e "${RED}不支持的系统${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
get_public_ip() {
|
||||||
|
local ip=""
|
||||||
|
ip=$(curl -s4 ip.sb 2>/dev/null) || \
|
||||||
|
ip=$(curl -s4 ifconfig.me 2>/dev/null) || \
|
||||||
|
ip=$(curl -s4 api.ipify.org 2>/dev/null)
|
||||||
|
echo "$ip"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============ 解锁机安装 ============
|
||||||
|
|
||||||
|
install_sniproxy() {
|
||||||
|
echo -e "${GREEN}[解锁机] 安装 sniproxy...${NC}"
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
if [[ "$OS" == "debian" ]]; then
|
||||||
|
apt update
|
||||||
|
apt install -y sniproxy dnsmasq ufw
|
||||||
|
else
|
||||||
|
yum install -y epel-release
|
||||||
|
yum install -y sniproxy dnsmasq firewalld
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 备份原配置
|
||||||
|
[[ -f /etc/sniproxy.conf ]] && cp /etc/sniproxy.conf /etc/sniproxy.conf.bak
|
||||||
|
|
||||||
|
# 创建 sniproxy 配置
|
||||||
|
cat > /etc/sniproxy.conf << 'EOF'
|
||||||
|
user daemon
|
||||||
|
pidfile /var/run/sniproxy.pid
|
||||||
|
|
||||||
|
listener 80 {
|
||||||
|
proto http
|
||||||
|
access_log {
|
||||||
|
filename /var/log/sniproxy/http_access.log
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
listener 443 {
|
||||||
|
proto tls
|
||||||
|
access_log {
|
||||||
|
filename /var/log/sniproxy/https_access.log
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
# 流媒体
|
||||||
|
netflix.* *
|
||||||
|
disneyplus.* *
|
||||||
|
hbo.* *
|
||||||
|
primevideo.* *
|
||||||
|
|
||||||
|
# AI
|
||||||
|
openai.* *
|
||||||
|
chatgpt.* *
|
||||||
|
anthropic.* *
|
||||||
|
claude.* *
|
||||||
|
gemini.* *
|
||||||
|
|
||||||
|
# 短视频
|
||||||
|
tiktok.* *
|
||||||
|
youtube.* *
|
||||||
|
ytimg.* *
|
||||||
|
|
||||||
|
# 默认
|
||||||
|
.* *
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 创建日志目录
|
||||||
|
mkdir -p /var/log/sniproxy
|
||||||
|
chmod 755 /var/log/sniproxy
|
||||||
|
|
||||||
|
# 配置防火墙
|
||||||
|
configure_firewall_unlocker
|
||||||
|
|
||||||
|
# 启动服务
|
||||||
|
systemctl enable sniproxy
|
||||||
|
systemctl restart sniproxy
|
||||||
|
|
||||||
|
echo -e "${GREEN}[解锁机] sniproxy 安装完成${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_firewall_unlocker() {
|
||||||
|
echo -e "${GREEN}[解锁机] 配置防火墙白名单...${NC}"
|
||||||
|
|
||||||
|
local allowed_ips=""
|
||||||
|
while true; do
|
||||||
|
read -p "输入要放行的被解锁机 IP(输入 done 结束): " ip
|
||||||
|
[[ "$ip" == "done" ]] && break
|
||||||
|
[[ -z "$ip" ]] && continue
|
||||||
|
|
||||||
|
if [[ "$OS" == "debian" ]]; then
|
||||||
|
ufw allow from "$ip" to any port 80
|
||||||
|
ufw allow from "$ip" to any port 443
|
||||||
|
ufw allow from "$ip" to any port 53
|
||||||
|
else
|
||||||
|
firewall-cmd --permanent --add-rich-rule="rule family=ipv4 source address=$ip port protocol=tcp port=80 accept"
|
||||||
|
firewall-cmd --permanent --add-rich-rule="rule family=ipv4 source address=$ip port protocol=tcp port=443 accept"
|
||||||
|
firewall-cmd --permanent --add-rich-rule="rule family=ipv4 source address=$ip port protocol=udp port=53 accept"
|
||||||
|
fi
|
||||||
|
|
||||||
|
allowed_ips="$allowed_ips $ip"
|
||||||
|
echo -e "${GREEN}已添加: $ip${NC}"
|
||||||
|
done
|
||||||
|
|
||||||
|
# 开放本地 DNS
|
||||||
|
if [[ "$OS" == "debian" ]]; then
|
||||||
|
ufw allow 53/udp
|
||||||
|
ufw allow 53/tcp
|
||||||
|
ufw --force enable
|
||||||
|
else
|
||||||
|
firewall-cmd --permanent --add-port=53/udp
|
||||||
|
firewall-cmd --permanent --add-port=53/tcp
|
||||||
|
firewall-cmd --reload
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}[解锁机] 防火墙配置完成,已放行:$allowed_ips${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
add_service_to_sniproxy() {
|
||||||
|
local service_type="$1"
|
||||||
|
local domains="${SERVICE_DOMAINS[$service_type]}"
|
||||||
|
|
||||||
|
if [[ -z "$domains" ]]; then
|
||||||
|
echo -e "${RED}未知服务类型: $service_type${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}添加服务到 sniproxy: $service_type${NC}"
|
||||||
|
|
||||||
|
# 为每个域名添加规则
|
||||||
|
for domain in $domains; do
|
||||||
|
# 提取主域名作为模式
|
||||||
|
local pattern="${domain%%.*}.*"
|
||||||
|
|
||||||
|
# 检查是否已存在
|
||||||
|
if ! grep -q "$pattern" /etc/sniproxy.conf 2>/dev/null; then
|
||||||
|
# 在 table 块中添加
|
||||||
|
sed -i "/table {/a\\ $pattern *" /etc/sniproxy.conf
|
||||||
|
echo " 添加: $pattern"
|
||||||
|
else
|
||||||
|
echo " 已存在: $pattern"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# 重启服务
|
||||||
|
systemctl restart sniproxy
|
||||||
|
echo -e "${GREEN}sniproxy 已重启${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============ 被解锁机安装 ============
|
||||||
|
|
||||||
|
install_smartdns() {
|
||||||
|
echo -e "${GREEN}[被解锁机] 安装 smartdns...${NC}"
|
||||||
|
|
||||||
|
# 下载并安装 smartdns
|
||||||
|
local smartdns_url="https://github.com/pymumu/smartdns/releases/download/Release42/smartdns.1.2024.02.24-2227.x86_64-linux-all.tar.gz"
|
||||||
|
local tmp_dir="/tmp/smartdns"
|
||||||
|
|
||||||
|
mkdir -p "$tmp_dir"
|
||||||
|
cd "$tmp_dir"
|
||||||
|
|
||||||
|
if ! curl -sL "$smartdns_url" -o smartdns.tar.gz; then
|
||||||
|
echo -e "${RED}下载 smartdns 失败,尝试备用方式...${NC}"
|
||||||
|
# 尝试 apt 安装
|
||||||
|
if [[ "$OS" == "debian" ]]; then
|
||||||
|
apt update
|
||||||
|
apt install -y smartdns || {
|
||||||
|
echo -e "${RED}apt 安装 smartdns 失败,请手动安装${NC}"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
tar -xzf smartdns.tar.gz
|
||||||
|
chmod +x smartdns
|
||||||
|
./smartdns install -u
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd -
|
||||||
|
rm -rf "$tmp_dir"
|
||||||
|
|
||||||
|
# 备份原配置
|
||||||
|
[[ -f /etc/smartdns/smartdns.conf ]] && cp /etc/smartdns/smartdns.conf /etc/smartdns/smartdns.conf.bak
|
||||||
|
|
||||||
|
# 创建基础配置
|
||||||
|
mkdir -p /etc/smartdns
|
||||||
|
cat > /etc/smartdns/smartdns.conf << 'EOF'
|
||||||
|
# SmartDNS 配置
|
||||||
|
|
||||||
|
# 监听端口
|
||||||
|
bind :53
|
||||||
|
|
||||||
|
# 上游 DNS
|
||||||
|
server 8.8.8.8 -group default
|
||||||
|
server 1.1.1.1 -group default
|
||||||
|
server 223.5.5.5 -group default
|
||||||
|
|
||||||
|
# 缓存配置
|
||||||
|
cache-size 4096
|
||||||
|
cache-persist yes
|
||||||
|
prefetch-domain yes
|
||||||
|
|
||||||
|
# 日志
|
||||||
|
log-level info
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo -e "${GREEN}[被解锁机] smartdns 安装完成${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_smartdns_unlocker() {
|
||||||
|
local unlocker_ip="$1"
|
||||||
|
local service_type="$2"
|
||||||
|
local domains="${SERVICE_DOMAINS[$service_type]}"
|
||||||
|
|
||||||
|
if [[ -z "$domains" ]]; then
|
||||||
|
echo -e "${RED}未知服务类型: $service_type${NC}"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}配置分流规则: $service_type -> $unlocker_ip${NC}"
|
||||||
|
|
||||||
|
local conf_file="/etc/smartdns/smartdns.conf"
|
||||||
|
|
||||||
|
# 添加注释
|
||||||
|
echo "" >> "$conf_file"
|
||||||
|
echo "# $service_type 解锁规则 - $(date)" >> "$conf_file"
|
||||||
|
|
||||||
|
# 为每个域名添加 address 规则
|
||||||
|
for domain in $domains; do
|
||||||
|
echo "address /$domain/$unlocker_ip" >> "$conf_file"
|
||||||
|
echo " 添加: $domain -> $unlocker_ip"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo -e "${GREEN}分流规则已添加${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
restart_smartdns() {
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable smartdns
|
||||||
|
systemctl restart smartdns
|
||||||
|
|
||||||
|
# 设置系统 DNS
|
||||||
|
if command -v resolvconf &>/dev/null; then
|
||||||
|
echo "nameserver 127.0.0.1" | resolvconf -a lo.smartdns
|
||||||
|
else
|
||||||
|
# 备份原 DNS 配置
|
||||||
|
cp /etc/resolv.conf /etc/resolv.conf.bak 2>/dev/null || true
|
||||||
|
echo "nameserver 127.0.0.1" > /etc/resolv.conf
|
||||||
|
# 防止被覆盖
|
||||||
|
chattr +i /etc/resolv.conf 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${GREEN}smartdns 已启动并设为系统 DNS${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============ 服务选择菜单 ============
|
||||||
|
|
||||||
|
select_services() {
|
||||||
|
local selected=()
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}选择要解锁的服务(可多选,空格分隔):${NC}"
|
||||||
|
echo " 1) 流媒体全家桶 (Netflix, Disney+, HBO, Prime...)"
|
||||||
|
echo " 2) AI 服务 (ChatGPT, Claude, Gemini...)"
|
||||||
|
echo " 3) 短视频 (TikTok, YouTube)"
|
||||||
|
echo " 4) Spotify"
|
||||||
|
echo " 5) 游戏服务 (Steam, PlayStation, Xbox...)"
|
||||||
|
echo " 6) ChatGPT 详细"
|
||||||
|
echo " 7) Claude 详细"
|
||||||
|
echo " 8) 全部"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
read -p "请输入选项 (如: 1 2 3): " choices
|
||||||
|
|
||||||
|
for choice in $choices; do
|
||||||
|
case $choice in
|
||||||
|
1) selected+=("stream") ;;
|
||||||
|
2) selected+=("ai") ;;
|
||||||
|
3) selected+=("shorts") ;;
|
||||||
|
4) selected+=("spotify") ;;
|
||||||
|
5) selected+=("game") ;;
|
||||||
|
6) selected+=("chatgpt") ;;
|
||||||
|
7) selected+=("claude") ;;
|
||||||
|
8) selected=("stream" "ai" "shorts" "spotify" "game" "chatgpt" "claude"); break ;;
|
||||||
|
*) echo -e "${RED}无效选项: $choice${NC}" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "${selected[@]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============ 主菜单 ============
|
||||||
|
|
||||||
|
menu_unlocker() {
|
||||||
|
clear
|
||||||
|
print_banner
|
||||||
|
|
||||||
|
local my_ip=$(get_public_ip)
|
||||||
|
echo -e "本机 IP: ${GREEN}$my_ip${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}[解锁机模式]${NC}"
|
||||||
|
echo " 1) 安装 sniproxy(首次安装)"
|
||||||
|
echo " 2) 添加被解锁机 IP 白名单"
|
||||||
|
echo " 3) 添加解锁服务"
|
||||||
|
echo " 4) 查看当前配置"
|
||||||
|
echo " 5) 卸载 sniproxy"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
read -p "请选择: " choice
|
||||||
|
|
||||||
|
case $choice in
|
||||||
|
1)
|
||||||
|
install_sniproxy
|
||||||
|
echo ""
|
||||||
|
read -p "是否现在添加解锁服务?(y/n): " add_service
|
||||||
|
[[ "$add_service" == "y" ]] && {
|
||||||
|
local services=$(select_services)
|
||||||
|
for svc in $services; do
|
||||||
|
add_service_to_sniproxy "$svc"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
configure_firewall_unlocker
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
local services=$(select_services)
|
||||||
|
for svc in $services; do
|
||||||
|
add_service_to_sniproxy "$svc"
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
echo -e "${GREEN}sniproxy 配置:${NC}"
|
||||||
|
cat /etc/sniproxy.conf 2>/dev/null || echo "配置文件不存在"
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}防火墙规则:${NC}"
|
||||||
|
if [[ "$OS" == "debian" ]]; then
|
||||||
|
ufw status
|
||||||
|
else
|
||||||
|
firewall-cmd --list-all
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
5)
|
||||||
|
echo -e "${RED}确定要卸载 sniproxy?(y/n): ${NC}"
|
||||||
|
read -p "" confirm
|
||||||
|
[[ "$confirm" == "y" ]] && {
|
||||||
|
systemctl stop sniproxy
|
||||||
|
systemctl disable sniproxy
|
||||||
|
apt remove -y sniproxy 2>/dev/null || yum remove -y sniproxy 2>/dev/null
|
||||||
|
rm -f /etc/sniproxy.conf
|
||||||
|
echo -e "${GREEN}sniproxy 已卸载${NC}"
|
||||||
|
}
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}无效选项${NC}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
menu_client() {
|
||||||
|
clear
|
||||||
|
print_banner
|
||||||
|
|
||||||
|
local my_ip=$(get_public_ip)
|
||||||
|
echo -e "本机 IP: ${GREEN}$my_ip${NC}"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}[被解锁机模式]${NC}"
|
||||||
|
echo " 1) 安装 smartdns 并配置分流(首次安装)"
|
||||||
|
echo " 2) 添加新的分流服务"
|
||||||
|
echo " 3) 查看当前配置"
|
||||||
|
echo " 4) 测试解锁"
|
||||||
|
echo " 5) 卸载 smartdns"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
read -p "请选择: " choice
|
||||||
|
|
||||||
|
case $choice in
|
||||||
|
1)
|
||||||
|
install_smartdns
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
read -p "输入解锁机 IP: " unlocker_ip
|
||||||
|
[[ -z "$unlocker_ip" ]] && {
|
||||||
|
echo -e "${RED}解锁机 IP 不能为空${NC}"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
local services=$(select_services)
|
||||||
|
for svc in $services; do
|
||||||
|
configure_smartdns_unlocker "$unlocker_ip" "$svc"
|
||||||
|
done
|
||||||
|
|
||||||
|
restart_smartdns
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "${GREEN}配置完成!${NC}"
|
||||||
|
echo "解锁机: $unlocker_ip"
|
||||||
|
echo "分流服务: $services"
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
local current_unlocker=$(grep "address /netflix.com/" /etc/smartdns/smartdns.conf 2>/dev/null | head -1 | awk -F'/' '{print $3}')
|
||||||
|
|
||||||
|
if [[ -z "$current_unlocker" ]]; then
|
||||||
|
read -p "输入解锁机 IP: " unlocker_ip
|
||||||
|
else
|
||||||
|
read -p "解锁机 IP [$current_unlocker]: " unlocker_ip
|
||||||
|
unlocker_ip=${unlocker_ip:-$current_unlocker}
|
||||||
|
fi
|
||||||
|
|
||||||
|
local services=$(select_services)
|
||||||
|
for svc in $services; do
|
||||||
|
configure_smartdns_unlocker "$unlocker_ip" "$svc"
|
||||||
|
done
|
||||||
|
|
||||||
|
restart_smartdns
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
echo -e "${GREEN}smartdns 配置:${NC}"
|
||||||
|
cat /etc/smartdns/smartdns.conf 2>/dev/null || echo "配置文件不存在"
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
echo -e "${GREEN}测试 DNS 解析...${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "Netflix: $(dig +short netflix.com @127.0.0.1 2>/dev/null | head -1)"
|
||||||
|
echo "ChatGPT: $(dig +short chatgpt.com @127.0.0.1 2>/dev/null | head -1)"
|
||||||
|
echo "TikTok: $(dig +short tiktok.com @127.0.0.1 2>/dev/null | head -1)"
|
||||||
|
;;
|
||||||
|
5)
|
||||||
|
echo -e "${RED}确定要卸载 smartdns?(y/n): ${NC}"
|
||||||
|
read -p "" confirm
|
||||||
|
[[ "$confirm" == "y" ]] && {
|
||||||
|
systemctl stop smartdns
|
||||||
|
systemctl disable smartdns
|
||||||
|
chattr -i /etc/resolv.conf 2>/dev/null || true
|
||||||
|
[[ -f /etc/resolv.conf.bak ]] && mv /etc/resolv.conf.bak /etc/resolv.conf
|
||||||
|
rm -rf /etc/smartdns
|
||||||
|
echo -e "${GREEN}smartdns 已卸载${NC}"
|
||||||
|
}
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${RED}无效选项${NC}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============ 主入口 ============
|
||||||
|
|
||||||
|
main() {
|
||||||
|
check_root
|
||||||
|
detect_os
|
||||||
|
|
||||||
|
clear
|
||||||
|
print_banner
|
||||||
|
|
||||||
|
local my_ip=$(get_public_ip)
|
||||||
|
echo -e "本机 IP: ${GREEN}$my_ip${NC}"
|
||||||
|
echo ""
|
||||||
|
echo "请选择本机角色:"
|
||||||
|
echo " 1) 解锁机(我能解锁,帮别人解锁)"
|
||||||
|
echo " 2) 被解锁机(我不能解锁,用别人的解锁机)"
|
||||||
|
echo " 3) 退出"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
read -p "请选择: " mode
|
||||||
|
|
||||||
|
case $mode in
|
||||||
|
1) menu_unlocker ;;
|
||||||
|
2) menu_client ;;
|
||||||
|
3) exit 0 ;;
|
||||||
|
*) echo -e "${RED}无效选项${NC}" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
480
scripts/hkt.sh
Executable file
480
scripts/hkt.sh
Executable file
@@ -0,0 +1,480 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -u
|
||||||
|
|
||||||
|
# =========================================================
|
||||||
|
# 默认出站源地址管理器(精简美化版)
|
||||||
|
# 目标:
|
||||||
|
# - 默认新建连接优先使用内网IP作为源地址
|
||||||
|
# - 实际下一跳仍走当前公网网关
|
||||||
|
# - 保留公网IP独立策略,避免SSH/公网入站回包异常
|
||||||
|
# =========================================================
|
||||||
|
|
||||||
|
[[ $EUID -eq 0 ]] || { echo "请使用 root 运行"; exit 1; }
|
||||||
|
|
||||||
|
STATE_DIR="/var/lib/default-src-ip"
|
||||||
|
STATE_FILE="$STATE_DIR/state.env"
|
||||||
|
APPLY_BIN="/usr/local/sbin/default-src-ip-apply"
|
||||||
|
SERVICE_NAME="default-src-ip.service"
|
||||||
|
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}"
|
||||||
|
SYSCTL_FILE="/etc/sysctl.d/90-default-src-ip.conf"
|
||||||
|
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
|
||||||
|
# ---------- 颜色(不使用红色) ----------
|
||||||
|
C_RESET='\033[0m'
|
||||||
|
C_BOLD='\033[1m'
|
||||||
|
C_DIM='\033[2m'
|
||||||
|
C_WHITE='\033[1;37m'
|
||||||
|
C_CYAN='\033[1;36m'
|
||||||
|
C_BLUE='\033[1;34m'
|
||||||
|
C_GREEN='\033[1;32m'
|
||||||
|
C_YELLOW='\033[1;33m'
|
||||||
|
C_GRAY='\033[0;37m'
|
||||||
|
|
||||||
|
# ---------- UI ----------
|
||||||
|
line() {
|
||||||
|
printf "%b%s%b\n" "$C_GRAY" "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" "$C_RESET"
|
||||||
|
}
|
||||||
|
|
||||||
|
subline() {
|
||||||
|
printf "%b%s%b\n" "$C_GRAY" "────────────────────────────────────────────────────────────" "$C_RESET"
|
||||||
|
}
|
||||||
|
|
||||||
|
header() {
|
||||||
|
clear 2>/dev/null || true
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
section() {
|
||||||
|
echo
|
||||||
|
printf " %b%s%b\n" "$C_CYAN$C_BOLD" "$1" "$C_RESET"
|
||||||
|
subline
|
||||||
|
}
|
||||||
|
|
||||||
|
ok() { printf "%b[OK]%b %s\n" "$C_GREEN" "$C_RESET" "$*"; }
|
||||||
|
info() { printf "%b[INFO]%b %s\n" "$C_CYAN" "$C_RESET" "$*"; }
|
||||||
|
warn() { printf "%b[WARN]%b %s\n" "$C_YELLOW" "$C_RESET" "$*"; }
|
||||||
|
|
||||||
|
kv() {
|
||||||
|
printf " %b%-10s%b %s\n" "$C_GRAY" "$1" "$C_RESET" "$2"
|
||||||
|
}
|
||||||
|
|
||||||
|
menu_item() {
|
||||||
|
local num="$1"
|
||||||
|
local title="$2"
|
||||||
|
printf " %b[%s]%b %s\n" "$C_BLUE$C_BOLD" "$num" "$C_RESET" "$title"
|
||||||
|
}
|
||||||
|
|
||||||
|
pause() {
|
||||||
|
echo
|
||||||
|
read -r -p "按回车继续..." _
|
||||||
|
}
|
||||||
|
|
||||||
|
need_cmd() {
|
||||||
|
command -v "$1" >/dev/null 2>&1 || {
|
||||||
|
echo "缺少命令: $1"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for c in ip awk grep cut sed head tr sysctl systemctl curl ping; do
|
||||||
|
need_cmd "$c"
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---------- IP 类型判断 ----------
|
||||||
|
is_private_ipv4() {
|
||||||
|
local ip="$1"
|
||||||
|
[[ "$ip" =~ ^10\. ]] && return 0
|
||||||
|
[[ "$ip" =~ ^192\.168\. ]] && return 0
|
||||||
|
[[ "$ip" =~ ^172\.(1[6-9]|2[0-9]|3[0-1])\. ]] && return 0
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 自动检测 ----------
|
||||||
|
detect_env() {
|
||||||
|
IFACE="$(ip -4 route show default | awk 'NR==1{print $5}')"
|
||||||
|
PUBLIC_GW="$(ip -4 route show default | awk 'NR==1{print $3}')"
|
||||||
|
|
||||||
|
PUBLIC_IP=""
|
||||||
|
PUBLIC_CIDR=""
|
||||||
|
PRIVATE_IP=""
|
||||||
|
PRIVATE_CIDR=""
|
||||||
|
|
||||||
|
while read -r linebuf; do
|
||||||
|
local cidr ip prefix
|
||||||
|
cidr="$(awk '{print $4}' <<<"$linebuf")"
|
||||||
|
ip="${cidr%/*}"
|
||||||
|
prefix="${cidr#*/}"
|
||||||
|
|
||||||
|
if is_private_ipv4 "$ip"; then
|
||||||
|
if [[ -z "$PRIVATE_IP" ]]; then
|
||||||
|
PRIVATE_IP="$ip"
|
||||||
|
PRIVATE_CIDR="$prefix"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
if [[ -z "$PUBLIC_IP" ]]; then
|
||||||
|
PUBLIC_IP="$ip"
|
||||||
|
PUBLIC_CIDR="$prefix"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done < <(ip -o -4 addr show dev "$IFACE" scope global 2>/dev/null)
|
||||||
|
|
||||||
|
[[ -n "${IFACE:-}" ]] || return 1
|
||||||
|
[[ -n "${PUBLIC_GW:-}" ]] || return 1
|
||||||
|
[[ -n "${PUBLIC_IP:-}" ]] || return 1
|
||||||
|
[[ -n "${PRIVATE_IP:-}" ]] || return 1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
save_state() {
|
||||||
|
cat > "$STATE_FILE" <<EOF_STATE
|
||||||
|
IFACE="$IFACE"
|
||||||
|
PUBLIC_GW="$PUBLIC_GW"
|
||||||
|
PUBLIC_IP="$PUBLIC_IP"
|
||||||
|
PUBLIC_CIDR="$PUBLIC_CIDR"
|
||||||
|
PRIVATE_IP="$PRIVATE_IP"
|
||||||
|
PRIVATE_CIDR="$PRIVATE_CIDR"
|
||||||
|
EOF_STATE
|
||||||
|
}
|
||||||
|
|
||||||
|
load_state_or_detect() {
|
||||||
|
if [[ -f "$STATE_FILE" ]]; then
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$STATE_FILE"
|
||||||
|
if [[ -n "${IFACE:-}" && -n "${PUBLIC_GW:-}" && -n "${PUBLIC_IP:-}" && -n "${PRIVATE_IP:-}" ]]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
detect_env || return 1
|
||||||
|
save_state
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
refresh_state() {
|
||||||
|
detect_env || return 1
|
||||||
|
save_state
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 当前模式 ----------
|
||||||
|
current_mode() {
|
||||||
|
local def
|
||||||
|
def="$(ip route show default 2>/dev/null | head -n1)"
|
||||||
|
|
||||||
|
if [[ -n "${PRIVATE_IP:-}" ]] && grep -q "src ${PRIVATE_IP}" <<<"$def"; then
|
||||||
|
echo "默认源地址 = 内网IP(172 优先)"
|
||||||
|
elif [[ -n "${PUBLIC_IP:-}" ]] && grep -q "src ${PUBLIC_IP}" <<<"$def"; then
|
||||||
|
echo "默认源地址 = 公网IP"
|
||||||
|
else
|
||||||
|
echo "默认源地址 = 未识别"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 显示摘要 ----------
|
||||||
|
show_summary() {
|
||||||
|
load_state_or_detect || {
|
||||||
|
warn "自动识别失败,请检查默认路由、网卡与 IP 配置。"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
local mode
|
||||||
|
mode="$(current_mode)"
|
||||||
|
|
||||||
|
section "当前环境"
|
||||||
|
kv "网卡" "${IFACE}"
|
||||||
|
kv "公网IP" "${PUBLIC_IP}"
|
||||||
|
kv "内网IP" "${PRIVATE_IP}"
|
||||||
|
kv "公网网关" "${PUBLIC_GW}"
|
||||||
|
kv "当前模式" "${mode}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 清理策略 ----------
|
||||||
|
clean_policy_only() {
|
||||||
|
ip rule del pref 100 2>/dev/null || true
|
||||||
|
ip rule del pref 110 2>/dev/null || true
|
||||||
|
ip route flush table 100 2>/dev/null || true
|
||||||
|
ip route flush table 200 2>/dev/null || true
|
||||||
|
ip route flush cache 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 应用策略 ----------
|
||||||
|
apply_private_as_default_src() {
|
||||||
|
refresh_state || {
|
||||||
|
warn "自动识别失败,无法应用。"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
info "正在应用:默认新连接优先使用 ${PRIVATE_IP} 出站"
|
||||||
|
info "下一跳保持:${PUBLIC_GW}"
|
||||||
|
|
||||||
|
clean_policy_only
|
||||||
|
|
||||||
|
# 主路由表:默认新连接使用内网IP为源
|
||||||
|
ip route replace default via "${PUBLIC_GW}" dev "${IFACE}" src "${PRIVATE_IP}"
|
||||||
|
|
||||||
|
# 表100:源地址=内网IP,明确走公网网关,源保持内网IP
|
||||||
|
ip route replace default via "${PUBLIC_GW}" dev "${IFACE}" src "${PRIVATE_IP}" table 100
|
||||||
|
|
||||||
|
# 表200:源地址=公网IP,明确走公网网关,源保持公网IP
|
||||||
|
ip route replace default via "${PUBLIC_GW}" dev "${IFACE}" src "${PUBLIC_IP}" table 200
|
||||||
|
|
||||||
|
# 策略规则
|
||||||
|
ip rule add pref 100 from "${PRIVATE_IP}/32" table 100
|
||||||
|
ip rule add pref 110 from "${PUBLIC_IP}/32" table 200
|
||||||
|
|
||||||
|
ip route flush cache 2>/dev/null || true
|
||||||
|
|
||||||
|
ok "应用完成"
|
||||||
|
echo
|
||||||
|
show_route_status
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 回滚 ----------
|
||||||
|
rollback_public_as_default_src() {
|
||||||
|
refresh_state || {
|
||||||
|
warn "自动识别失败,无法回滚。"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
info "正在恢复:默认新连接优先使用 ${PUBLIC_IP} 出站"
|
||||||
|
|
||||||
|
clean_policy_only
|
||||||
|
ip route replace default via "${PUBLIC_GW}" dev "${IFACE}" src "${PUBLIC_IP}"
|
||||||
|
ip route flush cache 2>/dev/null || true
|
||||||
|
|
||||||
|
ok "已恢复为公网IP默认出站"
|
||||||
|
echo
|
||||||
|
show_route_status
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 状态 ----------
|
||||||
|
show_route_status() {
|
||||||
|
load_state_or_detect || {
|
||||||
|
warn "自动识别失败"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
section "当前详细状态"
|
||||||
|
kv "网卡" "${IFACE}"
|
||||||
|
kv "公网IP" "${PUBLIC_IP}/${PUBLIC_CIDR}"
|
||||||
|
kv "内网IP" "${PRIVATE_IP}/${PRIVATE_CIDR}"
|
||||||
|
kv "公网网关" "${PUBLIC_GW}"
|
||||||
|
kv "当前模式" "$(current_mode)"
|
||||||
|
echo
|
||||||
|
|
||||||
|
printf "%b主默认路由%b\n" "$C_CYAN" "$C_RESET"
|
||||||
|
ip route show default | sed 's/^/ /'
|
||||||
|
echo
|
||||||
|
|
||||||
|
printf "%b策略规则%b\n" "$C_CYAN" "$C_RESET"
|
||||||
|
ip rule | sed 's/^/ /'
|
||||||
|
echo
|
||||||
|
|
||||||
|
printf "%b表100(内网IP源)%b\n" "$C_CYAN" "$C_RESET"
|
||||||
|
ip route show table 100 2>/dev/null | sed 's/^/ /'
|
||||||
|
echo
|
||||||
|
|
||||||
|
printf "%b表200(公网IP源)%b\n" "$C_CYAN" "$C_RESET"
|
||||||
|
ip route show table 200 2>/dev/null | sed 's/^/ /'
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 连通性测试 ----------
|
||||||
|
test_now() {
|
||||||
|
load_state_or_detect || {
|
||||||
|
warn "自动识别失败"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
section "测试当前出站效果"
|
||||||
|
|
||||||
|
printf "%b默认新连接选路%b\n" "$C_CYAN" "$C_RESET"
|
||||||
|
ip route get 1.1.1.1 | sed 's/^/ /'
|
||||||
|
echo
|
||||||
|
|
||||||
|
printf "%b从内网IP出站选路%b\n" "$C_CYAN" "$C_RESET"
|
||||||
|
ip route get 1.1.1.1 from "${PRIVATE_IP}" | sed 's/^/ /'
|
||||||
|
echo
|
||||||
|
|
||||||
|
printf "%b从公网IP出站选路%b\n" "$C_CYAN" "$C_RESET"
|
||||||
|
ip route get 1.1.1.1 from "${PUBLIC_IP}" | sed 's/^/ /'
|
||||||
|
echo
|
||||||
|
|
||||||
|
printf "%bPing(绑定内网IP)%b\n" "$C_CYAN" "$C_RESET"
|
||||||
|
ping -I "${PRIVATE_IP}" -c 3 1.1.1.1 || true
|
||||||
|
echo
|
||||||
|
|
||||||
|
printf "%b公网IP查询(绑定内网IP)%b\n" "$C_CYAN" "$C_RESET"
|
||||||
|
curl -4 --interface "${PRIVATE_IP}" --connect-timeout 5 --max-time 10 https://api.ipify.org ; echo
|
||||||
|
echo
|
||||||
|
|
||||||
|
printf "%b公网IP查询(默认新连接)%b\n" "$C_CYAN" "$C_RESET"
|
||||||
|
curl -4 --connect-timeout 5 --max-time 10 https://api.ipify.org ; echo
|
||||||
|
echo
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 启动脚本 ----------
|
||||||
|
install_apply_bin() {
|
||||||
|
cat > "$APPLY_BIN" <<'EOF_APPLY'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -u
|
||||||
|
|
||||||
|
STATE_FILE="/var/lib/default-src-ip/state.env"
|
||||||
|
[[ -f "$STATE_FILE" ]] || exit 1
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$STATE_FILE"
|
||||||
|
|
||||||
|
ip rule del pref 100 2>/dev/null || true
|
||||||
|
ip rule del pref 110 2>/dev/null || true
|
||||||
|
ip route flush table 100 2>/dev/null || true
|
||||||
|
ip route flush table 200 2>/dev/null || true
|
||||||
|
|
||||||
|
ip route replace default via "${PUBLIC_GW}" dev "${IFACE}" src "${PRIVATE_IP}"
|
||||||
|
ip route replace default via "${PUBLIC_GW}" dev "${IFACE}" src "${PRIVATE_IP}" table 100
|
||||||
|
ip route replace default via "${PUBLIC_GW}" dev "${IFACE}" src "${PUBLIC_IP}" table 200
|
||||||
|
|
||||||
|
ip rule add pref 100 from "${PRIVATE_IP}/32" table 100
|
||||||
|
ip rule add pref 110 from "${PUBLIC_IP}/32" table 200
|
||||||
|
|
||||||
|
ip route flush cache 2>/dev/null || true
|
||||||
|
EOF_APPLY
|
||||||
|
|
||||||
|
chmod +x "$APPLY_BIN"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_sysctl() {
|
||||||
|
cat > "$SYSCTL_FILE" <<'EOF_SYSCTL'
|
||||||
|
net.ipv4.conf.all.rp_filter = 2
|
||||||
|
net.ipv4.conf.default.rp_filter = 2
|
||||||
|
EOF_SYSCTL
|
||||||
|
sysctl --system >/dev/null 2>&1 || true
|
||||||
|
}
|
||||||
|
|
||||||
|
install_service() {
|
||||||
|
refresh_state || {
|
||||||
|
warn "自动识别失败,无法安装开机自启。"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
install_apply_bin
|
||||||
|
install_sysctl
|
||||||
|
|
||||||
|
cat > "$SERVICE_FILE" <<EOF_SERVICE
|
||||||
|
[Unit]
|
||||||
|
Description=Use private IP as default source for outbound traffic
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart=${APPLY_BIN}
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOF_SERVICE
|
||||||
|
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl enable --now "${SERVICE_NAME}"
|
||||||
|
|
||||||
|
ok "已安装开机自动应用"
|
||||||
|
echo
|
||||||
|
systemctl status "${SERVICE_NAME}" --no-pager || true
|
||||||
|
}
|
||||||
|
|
||||||
|
remove_service() {
|
||||||
|
systemctl disable --now "${SERVICE_NAME}" 2>/dev/null || true
|
||||||
|
rm -f "$SERVICE_FILE"
|
||||||
|
rm -f "$APPLY_BIN"
|
||||||
|
rm -f "$SYSCTL_FILE"
|
||||||
|
systemctl daemon-reload
|
||||||
|
sysctl --system >/dev/null 2>&1 || true
|
||||||
|
ok "已移除开机自动应用"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 卸载 ----------
|
||||||
|
full_uninstall() {
|
||||||
|
warn "开始卸载并恢复为公网IP默认出站"
|
||||||
|
remove_service
|
||||||
|
rollback_public_as_default_src || true
|
||||||
|
rm -f "$STATE_FILE"
|
||||||
|
ok "卸载完成"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------- 菜单 ----------
|
||||||
|
menu() {
|
||||||
|
while true; do
|
||||||
|
header
|
||||||
|
show_summary
|
||||||
|
|
||||||
|
section "功能菜单"
|
||||||
|
menu_item 1 "重新自动识别环境"
|
||||||
|
menu_item 2 "应用内网IP默认出站"
|
||||||
|
menu_item 3 "测试当前出站效果"
|
||||||
|
menu_item 4 "查看当前详细状态"
|
||||||
|
menu_item 5 "回滚为公网IP默认出站"
|
||||||
|
menu_item 6 "安装开机自动应用"
|
||||||
|
menu_item 7 "移除开机自动应用"
|
||||||
|
menu_item 8 "仅清理策略规则"
|
||||||
|
menu_item 9 "卸载并恢复默认"
|
||||||
|
menu_item 0 "退出"
|
||||||
|
|
||||||
|
echo
|
||||||
|
read -r -p "请输入编号 [0-9]: " choice
|
||||||
|
echo
|
||||||
|
|
||||||
|
case "$choice" in
|
||||||
|
1)
|
||||||
|
if refresh_state; then
|
||||||
|
ok "自动识别完成"
|
||||||
|
show_summary
|
||||||
|
else
|
||||||
|
warn "自动识别失败"
|
||||||
|
fi
|
||||||
|
pause
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
apply_private_as_default_src
|
||||||
|
pause
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
test_now
|
||||||
|
pause
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
show_route_status
|
||||||
|
pause
|
||||||
|
;;
|
||||||
|
5)
|
||||||
|
rollback_public_as_default_src
|
||||||
|
pause
|
||||||
|
;;
|
||||||
|
6)
|
||||||
|
install_service
|
||||||
|
pause
|
||||||
|
;;
|
||||||
|
7)
|
||||||
|
remove_service
|
||||||
|
pause
|
||||||
|
;;
|
||||||
|
8)
|
||||||
|
clean_policy_only
|
||||||
|
ok "策略规则已清理"
|
||||||
|
pause
|
||||||
|
;;
|
||||||
|
9)
|
||||||
|
full_uninstall
|
||||||
|
pause
|
||||||
|
;;
|
||||||
|
0)
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
warn "无效选项,请输入 0-9"
|
||||||
|
pause
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
menu
|
||||||
1033
scripts/stream-unlock.sh
Normal file
1033
scripts/stream-unlock.sh
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user