1069 lines
33 KiB
Bash
1069 lines
33 KiB
Bash
#!/bin/bash
|
||
|
||
# GRE/VXLAN 隧道生成器
|
||
# 生成 GRE 或 VXLAN 隧道的 systemd 服务文件
|
||
# 支持 IPv4 和 IPv6,IPv6 支持 PBR 防止 bird 路由冲突
|
||
|
||
set -e
|
||
|
||
# 检测是否通过管道运行
|
||
if [[ ! -t 0 ]]; then
|
||
echo "========================================"
|
||
echo " GRE/VXLAN 隧道服务生成器"
|
||
echo "========================================"
|
||
echo ""
|
||
echo "检测到您正在通过管道运行此脚本。"
|
||
echo "由于需要交互式输入,请使用以下方式运行:"
|
||
echo ""
|
||
echo " # 下载脚本"
|
||
echo " curl -fsSL https://raw.githubusercontent.com/yanglc721/tunnelgen/main/tunnel-generator.sh -o tunnel-generator.sh"
|
||
echo ""
|
||
echo " # 添加执行权限"
|
||
echo " chmod +x tunnel-generator.sh"
|
||
echo ""
|
||
echo " # 运行脚本"
|
||
echo " sudo ./tunnel-generator.sh"
|
||
echo ""
|
||
exit 1
|
||
fi
|
||
|
||
# 输出颜色
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
NC='\033[0m' # 无颜色
|
||
|
||
print_banner() {
|
||
echo -e "${BLUE}"
|
||
echo "========================================"
|
||
echo " GRE/VXLAN 隧道服务生成器"
|
||
echo "========================================"
|
||
echo -e "${NC}"
|
||
}
|
||
|
||
print_info() {
|
||
echo -e "${GREEN}[信息]${NC} $1"
|
||
}
|
||
|
||
print_warn() {
|
||
echo -e "${YELLOW}[警告]${NC} $1"
|
||
}
|
||
|
||
print_error() {
|
||
echo -e "${RED}[错误]${NC} $1"
|
||
}
|
||
|
||
# 检查是否以 root 运行
|
||
check_root() {
|
||
if [[ $EUID -ne 0 ]]; then
|
||
print_error "此脚本必须以 root 权限运行"
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# 验证 IPv4 地址
|
||
validate_ipv4() {
|
||
local ip=$1
|
||
local regex='^([0-9]{1,3}\.){3}[0-9]{1,3}$'
|
||
|
||
if [[ ! $ip =~ $regex ]]; then
|
||
return 1
|
||
fi
|
||
|
||
# 验证每个八位组
|
||
IFS='.' read -ra octets <<< "$ip"
|
||
for octet in "${octets[@]}"; do
|
||
if [[ $octet -lt 0 ]] || [[ $octet -gt 255 ]]; then
|
||
return 1
|
||
fi
|
||
done
|
||
return 0
|
||
}
|
||
|
||
# 验证 IPv6 地址
|
||
validate_ipv6() {
|
||
local ip=$1
|
||
local regex='^([0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}$|^::$|^::1$|^([0-9a-fA-F]{0,4}:){1,7}:$|^:([0-9a-fA-F]{0,4}:){1,7}$'
|
||
|
||
if [[ $ip =~ $regex ]]; then
|
||
return 0
|
||
fi
|
||
return 1
|
||
}
|
||
|
||
validate_ip() {
|
||
local ip=$1
|
||
local type=$2
|
||
|
||
if [[ $type == "ipv4" ]]; then
|
||
validate_ipv4 "$ip"
|
||
else
|
||
validate_ipv6 "$ip"
|
||
fi
|
||
}
|
||
|
||
# 获取指定 IP 的网络接口
|
||
get_interface_for_ip() {
|
||
local ip=$1
|
||
local ip_type=$2
|
||
|
||
if [[ $ip_type == "ipv4" ]]; then
|
||
ip -4 route get "$ip" 2>/dev/null | sed -n 's/.*dev \([^ ]*\).*/\1/p' | head -1
|
||
else
|
||
ip -6 route get "$ip" 2>/dev/null | sed -n 's/.*dev \([^ ]*\).*/\1/p' | head -1
|
||
fi
|
||
}
|
||
|
||
# 列出可用的网络接口
|
||
list_interfaces() {
|
||
local ip_type=$1
|
||
|
||
echo ""
|
||
echo "可用的网络接口:"
|
||
echo "====================================="
|
||
|
||
local idx=0
|
||
|
||
while IFS= read -r line; do
|
||
# 提取接口名称 (格式: "2: eth0: <...")
|
||
local iface=$(echo "$line" | awk -F': ' '{print $2}')
|
||
if [[ -z "$iface" ]]; then
|
||
continue
|
||
fi
|
||
|
||
# 跳过回环接口
|
||
if [[ "$iface" == "lo" ]]; then
|
||
continue
|
||
fi
|
||
|
||
# 获取接口状态
|
||
local state=$(ip link show "$iface" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="state") print $(i+1)}' | head -1)
|
||
|
||
# 跳过未启用的接口
|
||
if [[ "$state" != "UP" && "$state" != "UNKNOWN" ]]; then
|
||
continue
|
||
fi
|
||
|
||
# 获取 IP 地址
|
||
local ipv4_addr=$(ip -4 addr show "$iface" 2>/dev/null | awk '/inet /{print $2}' | cut -d'/' -f1 | head -1)
|
||
local ipv6_addr=$(ip -6 addr show "$iface" 2>/dev/null | awk '/inet6 / && !/fe80::/{print $2}' | cut -d'/' -f1 | head -1)
|
||
|
||
idx=$((idx + 1))
|
||
|
||
printf " [%d] %-12s 状态:%-8s" "$idx" "$iface" "$state"
|
||
|
||
if [[ $ip_type == "ipv4" ]]; then
|
||
if [[ -n "$ipv4_addr" ]]; then
|
||
printf " IPv4: %s" "$ipv4_addr"
|
||
fi
|
||
else
|
||
if [[ -n "$ipv6_addr" ]]; then
|
||
printf " IPv6: %s" "$ipv6_addr"
|
||
fi
|
||
fi
|
||
|
||
echo ""
|
||
done < <(ip link show 2>/dev/null | grep -E '^[0-9]+:')
|
||
|
||
echo "====================================="
|
||
echo ""
|
||
}
|
||
|
||
# 根据索引获取接口
|
||
get_interface_by_idx() {
|
||
local idx=$1
|
||
shift
|
||
local ifaces=("$@")
|
||
|
||
echo "${ifaces[$((idx-1))]}"
|
||
}
|
||
|
||
# 检查路由表是否存在
|
||
check_rt_table() {
|
||
local table_name=$1
|
||
grep -q "^.*${table_name}$" /etc/iproute2/rt_tables 2>/dev/null
|
||
}
|
||
|
||
# 添加路由表
|
||
add_rt_table() {
|
||
local table_id=$1
|
||
local table_name=$2
|
||
|
||
if ! check_rt_table "$table_name"; then
|
||
echo "${table_id} ${table_name}" >> /etc/iproute2/rt_tables
|
||
print_info "已创建路由表: ${table_name} (ID: ${table_id})"
|
||
else
|
||
print_info "路由表已存在: ${table_name}"
|
||
fi
|
||
}
|
||
|
||
# 获取接口的默认网关
|
||
get_default_gateway() {
|
||
local interface=$1
|
||
local ip_type=$2
|
||
|
||
if [[ $ip_type == "ipv4" ]]; then
|
||
local gw=$(ip -4 route show default dev "$interface" 2>/dev/null | awk '{print $3}')
|
||
if [[ -n "$gw" ]]; then
|
||
echo "$gw"
|
||
return
|
||
fi
|
||
else
|
||
local gw=$(ip -6 route show default dev "$interface" 2>/dev/null | awk '{print $3}')
|
||
if [[ -n "$gw" ]]; then
|
||
echo "$gw"
|
||
return
|
||
fi
|
||
fi
|
||
}
|
||
|
||
# 获取指定目标 IP 的网关(更准确)
|
||
get_gateway_for_ip() {
|
||
local dest_ip=$1
|
||
local ip_type=$2
|
||
|
||
if [[ $ip_type == "ipv4" ]]; then
|
||
ip -4 route get "$dest_ip" 2>/dev/null | sed -n 's/.*via \([^ ]*\).*/\1/p' | head -1
|
||
else
|
||
ip -6 route get "$dest_ip" 2>/dev/null | sed -n 's/.*via \([^ ]*\).*/\1/p' | head -1
|
||
fi
|
||
}
|
||
|
||
# 生成 GRE 隧道服务文件
|
||
generate_gre_service() {
|
||
local tunnel_name=$1
|
||
local remote_ip=$2
|
||
local local_ip=$3
|
||
local ip_type=$4
|
||
local interface=$5
|
||
local gateway=$6
|
||
local pbr_table="${tunnel_name}_underlay"
|
||
local pbr_table_id=100
|
||
|
||
local service_file="/etc/systemd/system/${tunnel_name}.service"
|
||
local description="GRE 隧道 ${tunnel_name}"
|
||
|
||
# 确定隧道模式
|
||
local tunnel_mode="gre"
|
||
if [[ $ip_type == "ipv6" ]]; then
|
||
tunnel_mode="ip6gre"
|
||
fi
|
||
|
||
print_info "正在生成 GRE 隧道服务: $service_file"
|
||
|
||
# IPv6 需要 PBR,先创建路由表
|
||
if [[ $ip_type == "ipv6" ]]; then
|
||
# 查找可用的表 ID
|
||
while grep -q "^${pbr_table_id} " /etc/iproute2/rt_tables 2>/dev/null; do
|
||
pbr_table_id=$((pbr_table_id + 1))
|
||
done
|
||
add_rt_table "$pbr_table_id" "$pbr_table"
|
||
|
||
# 创建 PBR 设置脚本
|
||
local prestart_script="/usr/local/sbin/${tunnel_name}-pbr-setup.sh"
|
||
cat > "$prestart_script" << 'PBREOF'
|
||
#!/bin/bash
|
||
# GRE 隧道 PBR 设置脚本
|
||
# 创建干净的路由表用于隧道底层流量
|
||
|
||
set -e
|
||
|
||
TUNNEL_NAME="__TUNNEL_NAME__"
|
||
REMOTE_IP="__REMOTE_IP__"
|
||
LOCAL_IP="__LOCAL_IP__"
|
||
INTERFACE="__INTERFACE__"
|
||
GATEWAY="__GATEWAY__"
|
||
TABLE_NAME="__TABLE_NAME__"
|
||
|
||
# 在干净的路由表中添加路由
|
||
# 通过物理接口路由到远端隧道端点
|
||
if [[ -n "$GATEWAY" ]]; then
|
||
ip -6 route replace "$REMOTE_IP/128" via "$GATEWAY" dev "$INTERFACE" table "$TABLE_NAME" 2>/dev/null || true
|
||
else
|
||
# 如果没有网关,尝试直接使用接口(点对点链路)
|
||
ip -6 route replace "$REMOTE_IP/128" dev "$INTERFACE" table "$TABLE_NAME" 2>/dev/null || true
|
||
fi
|
||
|
||
# 添加本地地址路由
|
||
ip -6 route replace "$LOCAL_IP/128" dev "$INTERFACE" table "$TABLE_NAME" 2>/dev/null || true
|
||
|
||
# 添加 PBR 规则(优先级 50,在默认 main 表查询 32766 之前)
|
||
ip -6 rule replace to "$REMOTE_IP" table "$TABLE_NAME" priority 50 2>/dev/null || \
|
||
ip -6 rule add to "$REMOTE_IP" table "$TABLE_NAME" priority 50
|
||
|
||
ip -6 rule replace from "$LOCAL_IP" table "$TABLE_NAME" priority 51 2>/dev/null || \
|
||
ip -6 rule add from "$LOCAL_IP" table "$TABLE_NAME" priority 51
|
||
|
||
echo "已为 $TUNNEL_NAME 安装 PBR 规则"
|
||
PBREOF
|
||
|
||
# 替换占位符
|
||
sed -i "s/__TUNNEL_NAME__/$tunnel_name/g" "$prestart_script"
|
||
sed -i "s/__REMOTE_IP__/$remote_ip/g" "$prestart_script"
|
||
sed -i "s/__LOCAL_IP__/$local_ip/g" "$prestart_script"
|
||
sed -i "s/__INTERFACE__/$interface/g" "$prestart_script"
|
||
sed -i "s/__GATEWAY__/$gateway/g" "$prestart_script"
|
||
sed -i "s/__TABLE_NAME__/$pbr_table/g" "$prestart_script"
|
||
chmod +x "$prestart_script"
|
||
|
||
print_info "已创建 PBR 设置脚本: $prestart_script"
|
||
fi
|
||
|
||
cat > "$service_file" << EOF
|
||
[Unit]
|
||
Description=$description
|
||
After=network.target network-online.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=oneshot
|
||
RemainAfterExit=yes
|
||
EOF
|
||
|
||
# 构建 ExecStart 命令
|
||
local exec_start="ip tunnel add $tunnel_name mode $tunnel_mode remote $remote_ip local $local_ip ttl 255 && ip link set $tunnel_name up"
|
||
|
||
if [[ $ip_type == "ipv6" ]]; then
|
||
# 在创建隧道前添加 PBR 设置
|
||
cat >> "$service_file" << EOF
|
||
ExecStartPre=/usr/local/sbin/${tunnel_name}-pbr-setup.sh
|
||
EOF
|
||
fi
|
||
|
||
cat >> "$service_file" << EOF
|
||
ExecStart=/bin/sh -c "$exec_start"
|
||
EOF
|
||
|
||
# 构建 ExecStop 命令
|
||
local exec_stop="ip link set $tunnel_name down; ip tunnel del $tunnel_name"
|
||
|
||
if [[ $ip_type == "ipv6" ]]; then
|
||
# 停止时删除 PBR 规则
|
||
exec_stop="ip link set $tunnel_name down; ip tunnel del $tunnel_name; ip -6 rule del to $remote_ip table $pbr_table priority 50 || true; ip -6 rule del from $local_ip table $pbr_table priority 51 || true"
|
||
fi
|
||
|
||
cat >> "$service_file" << EOF
|
||
ExecStop=/bin/sh -c "$exec_stop"
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
|
||
print_info "服务文件已创建: $service_file"
|
||
}
|
||
|
||
# 生成 VXLAN 隧道服务文件
|
||
generate_vxlan_service() {
|
||
local tunnel_name=$1
|
||
local remote_ip=$2
|
||
local local_ip=$3
|
||
local dst_port=$4
|
||
local vni=$5
|
||
local ip_type=$6
|
||
local interface=$7
|
||
local gateway=$8
|
||
local pbr_table="${tunnel_name}_underlay"
|
||
local pbr_table_id=100
|
||
|
||
local service_file="/etc/systemd/system/${tunnel_name}.service"
|
||
local description="VXLAN 隧道 ${tunnel_name}"
|
||
|
||
print_info "正在生成 VXLAN 隧道服务: $service_file"
|
||
|
||
# IPv6 需要 PBR,先创建路由表
|
||
if [[ $ip_type == "ipv6" ]]; then
|
||
# 查找可用的表 ID
|
||
while grep -q "^${pbr_table_id} " /etc/iproute2/rt_tables 2>/dev/null; do
|
||
pbr_table_id=$((pbr_table_id + 1))
|
||
done
|
||
add_rt_table "$pbr_table_id" "$pbr_table"
|
||
|
||
# 创建 PBR 设置脚本
|
||
local prestart_script="/usr/local/sbin/${tunnel_name}-pbr-setup.sh"
|
||
cat > "$prestart_script" << 'PBREOF'
|
||
#!/bin/bash
|
||
# VXLAN 隧道 PBR 设置脚本
|
||
# 创建干净的路由表用于隧道底层流量
|
||
|
||
set -e
|
||
|
||
TUNNEL_NAME="__TUNNEL_NAME__"
|
||
REMOTE_IP="__REMOTE_IP__"
|
||
LOCAL_IP="__LOCAL_IP__"
|
||
INTERFACE="__INTERFACE__"
|
||
GATEWAY="__GATEWAY__"
|
||
TABLE_NAME="__TABLE_NAME__"
|
||
|
||
# 在干净的路由表中添加路由
|
||
# 通过物理接口路由到远端隧道端点
|
||
if [[ -n "$GATEWAY" ]]; then
|
||
ip -6 route replace "$REMOTE_IP/128" via "$GATEWAY" dev "$INTERFACE" table "$TABLE_NAME" 2>/dev/null || true
|
||
else
|
||
ip -6 route replace "$REMOTE_IP/128" dev "$INTERFACE" table "$TABLE_NAME" 2>/dev/null || true
|
||
fi
|
||
|
||
# 添加本地地址路由
|
||
ip -6 route replace "$LOCAL_IP/128" dev "$INTERFACE" table "$TABLE_NAME" 2>/dev/null || true
|
||
|
||
# 添加 PBR 规则(优先级 50,在默认 main 表查询 32766 之前)
|
||
ip -6 rule replace to "$REMOTE_IP" table "$TABLE_NAME" priority 50 2>/dev/null || \
|
||
ip -6 rule add to "$REMOTE_IP" table "$TABLE_NAME" priority 50
|
||
|
||
ip -6 rule replace from "$LOCAL_IP" table "$TABLE_NAME" priority 51 2>/dev/null || \
|
||
ip -6 rule add from "$LOCAL_IP" table "$TABLE_NAME" priority 51
|
||
|
||
echo "已为 $TUNNEL_NAME 安装 PBR 规则"
|
||
PBREOF
|
||
|
||
# 替换占位符
|
||
sed -i "s/__TUNNEL_NAME__/$tunnel_name/g" "$prestart_script"
|
||
sed -i "s/__REMOTE_IP__/$remote_ip/g" "$prestart_script"
|
||
sed -i "s/__LOCAL_IP__/$local_ip/g" "$prestart_script"
|
||
sed -i "s/__INTERFACE__/$interface/g" "$prestart_script"
|
||
sed -i "s/__GATEWAY__/$gateway/g" "$prestart_script"
|
||
sed -i "s/__TABLE_NAME__/$pbr_table/g" "$prestart_script"
|
||
chmod +x "$prestart_script"
|
||
|
||
print_info "已创建 PBR 设置脚本: $prestart_script"
|
||
fi
|
||
|
||
cat > "$service_file" << EOF
|
||
[Unit]
|
||
Description=$description
|
||
After=network.target network-online.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=oneshot
|
||
RemainAfterExit=yes
|
||
EOF
|
||
|
||
# 构建 ExecStart 命令
|
||
local exec_start="ip link add $tunnel_name type vxlan id $vni dev $interface remote $remote_ip dstport $dst_port learning && ip link set $tunnel_name up"
|
||
|
||
if [[ $ip_type == "ipv6" ]]; then
|
||
# 在创建隧道前添加 PBR 设置
|
||
cat >> "$service_file" << EOF
|
||
ExecStartPre=/usr/local/sbin/${tunnel_name}-pbr-setup.sh
|
||
EOF
|
||
fi
|
||
|
||
cat >> "$service_file" << EOF
|
||
ExecStart=/bin/sh -c "$exec_start"
|
||
EOF
|
||
|
||
# 构建 ExecStop 命令
|
||
local exec_stop="ip link set $tunnel_name down; ip link del $tunnel_name"
|
||
|
||
if [[ $ip_type == "ipv6" ]]; then
|
||
# 停止时删除 PBR 规则
|
||
exec_stop="ip link set $tunnel_name down; ip link del $tunnel_name; ip -6 rule del to $remote_ip table $pbr_table priority 50 || true; ip -6 rule del from $local_ip table $pbr_table priority 51 || true"
|
||
fi
|
||
|
||
cat >> "$service_file" << EOF
|
||
ExecStop=/bin/sh -c "$exec_stop"
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
|
||
print_info "服务文件已创建: $service_file"
|
||
}
|
||
|
||
# 生成 SSH TUN 隧道服务文件
|
||
generate_sshtun_service() {
|
||
local tunnel_name=$1
|
||
local remote_ip=$2
|
||
local local_ip=$3
|
||
local tunnel_ip_local=$4
|
||
local tunnel_ip_remote=$5
|
||
local ssh_port=$6
|
||
local ssh_user=$7
|
||
local role=$8 # "server" or "client"
|
||
local ip_type=$9 # "ipv4" or "ipv6"
|
||
|
||
local service_file="/etc/systemd/system/${tunnel_name}.service"
|
||
local description="SSH TUN 隧道 ${tunnel_name}"
|
||
|
||
# 确定掩码
|
||
local prefix="24"
|
||
if [[ "$ip_type" == "ipv6" ]]; then
|
||
prefix="64"
|
||
fi
|
||
|
||
print_info "正在生成 SSH TUN 隧道服务: $service_file"
|
||
|
||
if [[ "$role" == "server" ]]; then
|
||
# 服务端配置
|
||
# 检查并配置 SSH PermitTunnel
|
||
local sshd_config="/etc/ssh/sshd_config"
|
||
if ! grep -q "^PermitTunnel[[:space:]]*yes" "$sshd_config" 2>/dev/null; then
|
||
print_info "正在配置 SSH PermitTunnel..."
|
||
if grep -q "^#*PermitTunnel" "$sshd_config" 2>/dev/null; then
|
||
sed -i 's/^#*PermitTunnel.*/PermitTunnel yes/' "$sshd_config"
|
||
else
|
||
echo "PermitTunnel yes" >> "$sshd_config"
|
||
fi
|
||
# 重启 SSH 服务
|
||
systemctl restart sshd || systemctl restart ssh
|
||
print_info "已启用 SSH PermitTunnel 并重启 SSH 服务"
|
||
fi
|
||
|
||
# 服务端服务文件 - 等待客户端连接
|
||
cat > "$service_file" << EOF
|
||
[Unit]
|
||
Description=$description (服务端)
|
||
After=network.target network-online.target sshd.service
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
# 服务端通过 SSH 连接的自动触发,不需要额外服务
|
||
# 当客户端连接时会自动创建 tun 设备
|
||
# 此服务主要用于记录状态
|
||
|
||
ExecStartPre=/bin/sh -c 'echo "SSH TUN 服务端已就绪,等待客户端连接..."'
|
||
ExecStart=/bin/sleep infinity
|
||
ExecStop=/bin/true
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
|
||
# 创建 IP 配置脚本(连接建立后手动或自动执行)
|
||
local ip_config_script="/usr/local/sbin/${tunnel_name}-ip-config.sh"
|
||
cat > "$ip_config_script" << EOF
|
||
#!/bin/bash
|
||
# SSH TUN 隧道 IP 配置脚本
|
||
# 当 SSH 连接建立后,tun 设备会自动创建
|
||
# 运行此脚本配置 IP 地址
|
||
|
||
# 查找 tun 设备(通常为 tun0)
|
||
TUN_DEV=""
|
||
for i in {0..9}; do
|
||
if ip link show tun\$i &>/dev/null; then
|
||
TUN_DEV="tun\$i"
|
||
break
|
||
fi
|
||
done
|
||
|
||
if [[ -z "\$TUN_DEV" ]]; then
|
||
echo "未找到 tun 设备,请确保 SSH 客户端已连接"
|
||
exit 1
|
||
fi
|
||
|
||
echo "配置 \$TUN_DEV IP 地址..."
|
||
ip addr add ${tunnel_ip_local}/${prefix} dev \$TUN_DEV
|
||
ip link set \$TUN_DEV up
|
||
|
||
echo "SSH TUN 隧道 IP 配置完成"
|
||
echo "本地隧道 IP: ${tunnel_ip_local}"
|
||
echo "对端隧道 IP: ${tunnel_ip_remote}"
|
||
EOF
|
||
chmod +x "$ip_config_script"
|
||
print_info "已创建 IP 配置脚本: $ip_config_script"
|
||
|
||
else
|
||
# 客户端配置
|
||
# 检查 SSH 密钥
|
||
local ssh_key=""
|
||
if [[ -f "/root/.ssh/id_rsa" ]]; then
|
||
ssh_key="/root/.ssh/id_rsa"
|
||
elif [[ -f "/root/.ssh/id_ed25519" ]]; then
|
||
ssh_key="/root/.ssh/id_ed25519"
|
||
elif [[ -f "/root/.ssh/id_ecdsa" ]]; then
|
||
ssh_key="/root/.ssh/id_ecdsa"
|
||
fi
|
||
|
||
if [[ -z "$ssh_key" ]]; then
|
||
print_warn "未找到 SSH 密钥,将生成新的密钥..."
|
||
ssh-keygen -t ed25519 -f /root/.ssh/id_ed25519 -N ""
|
||
ssh_key="/root/.ssh/id_ed25519"
|
||
print_info "已生成 SSH 密钥: $ssh_key"
|
||
print_warn "请将公钥添加到服务端:"
|
||
echo ""
|
||
cat "${ssh_key}.pub"
|
||
echo ""
|
||
read -p "已将公钥添加到服务端? [y/N]: " key_added
|
||
if [[ ! "$key_added" =~ ^[Yy]$ ]]; then
|
||
print_info "请先添加公钥后再运行此脚本"
|
||
exit 0
|
||
fi
|
||
fi
|
||
|
||
# 客户端服务文件
|
||
cat > "$service_file" << EOF
|
||
[Unit]
|
||
Description=$description (客户端)
|
||
After=network.target network-online.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=root
|
||
# SSH TUN 连接命令
|
||
# -w 0:0 创建 tun 设备
|
||
# -o Tunnel=point-to-point 点对点模式
|
||
# -o ServerAliveInterval 保持连接
|
||
# -o ExitOnForwardFailure 连接失败时退出
|
||
ExecStart=/usr/bin/ssh -i ${ssh_key} \\
|
||
-w 0:0 \\
|
||
-o Tunnel=point-to-point \\
|
||
-o ServerAliveInterval=30 \\
|
||
-o ServerAliveCountMax=3 \\
|
||
-o ExitOnForwardFailure=yes \\
|
||
-o StrictHostKeyChecking=accept-new \\
|
||
-p ${ssh_port} \\
|
||
${ssh_user}@${remote_ip} \\
|
||
-N
|
||
|
||
# 连接建立后配置 IP
|
||
ExecStartPost=/bin/sh -c 'sleep 2 && ip addr add ${tunnel_ip_local}/${prefix} dev tun0 && ip link set tun0 up'
|
||
|
||
# 重启策略
|
||
Restart=always
|
||
RestartSec=10
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
EOF
|
||
fi
|
||
|
||
print_info "服务文件已创建: $service_file"
|
||
}
|
||
|
||
# 主脚本
|
||
main() {
|
||
print_banner
|
||
|
||
# 检查 root
|
||
check_root
|
||
|
||
# 选择 IP 版本
|
||
echo ""
|
||
echo "选择 IP 版本:"
|
||
echo " 1) IPv4"
|
||
echo " 2) IPv6"
|
||
echo ""
|
||
read -p "请输入选项 [1-2]: " ip_choice
|
||
|
||
case $ip_choice in
|
||
1) ip_type="ipv4" ;;
|
||
2) ip_type="ipv6" ;;
|
||
*) print_error "无效的选项"; exit 1 ;;
|
||
esac
|
||
|
||
print_info "已选择: $ip_type"
|
||
|
||
# SSH TUN 使用 IPv4 隧道 IP,跳过 IPv6 特有配置
|
||
# 但仍需要 ip_type 变量
|
||
|
||
# 选择隧道类型
|
||
echo ""
|
||
echo "选择隧道类型:"
|
||
echo " 1) GRE"
|
||
echo " 2) VXLAN"
|
||
echo " 3) SSH TUN (TCP 隧道,适用于 UDP 受限环境)"
|
||
echo ""
|
||
read -p "请输入选项 [1-3]: " tunnel_choice
|
||
|
||
case $tunnel_choice in
|
||
1) tunnel_type="gre" ;;
|
||
2) tunnel_type="vxlan" ;;
|
||
3) tunnel_type="sshtun" ;;
|
||
*) print_error "无效的选项"; exit 1 ;;
|
||
esac
|
||
|
||
print_info "已选择: $tunnel_type"
|
||
|
||
# 获取隧道名称
|
||
echo ""
|
||
read -p "请输入隧道名称 (例如: gre-hk1, vxlan-hk4, ssh-ca): " tunnel_name
|
||
|
||
if [[ -z "$tunnel_name" ]]; then
|
||
print_error "隧道名称不能为空"
|
||
exit 1
|
||
fi
|
||
|
||
# 检查服务是否已存在
|
||
if [[ -f "/etc/systemd/system/${tunnel_name}.service" ]]; then
|
||
print_warn "服务文件已存在: /etc/systemd/system/${tunnel_name}.service"
|
||
read -p "是否覆盖? [y/N]: " overwrite
|
||
if [[ ! "$overwrite" =~ ^[Yy]$ ]]; then
|
||
print_info "已取消"
|
||
exit 0
|
||
fi
|
||
fi
|
||
|
||
# 获取本地 IP
|
||
echo ""
|
||
if [[ $tunnel_type == "sshtun" ]]; then
|
||
# SSH TUN 需要知道角色
|
||
echo "选择本机角色:"
|
||
echo " 1) 服务端 (有公网 IP,等待客户端连接)"
|
||
echo " 2) 客户端 (在 NAT 内,主动连接服务端)"
|
||
echo ""
|
||
read -p "请输入选项 [1-2]: " role_choice
|
||
|
||
case $role_choice in
|
||
1) ssh_role="server" ;;
|
||
2) ssh_role="client" ;;
|
||
*) print_error "无效的选项"; exit 1 ;;
|
||
esac
|
||
print_info "已选择角色: $ssh_role"
|
||
|
||
# 获取对端 IP
|
||
echo ""
|
||
if [[ $ssh_role == "client" ]]; then
|
||
read -p "请输入服务端 IP 地址: " remote_ip
|
||
else
|
||
read -p "请输入客户端公网 IP 地址 (可选,用于防火墙规则): " remote_ip
|
||
fi
|
||
|
||
# SSH 参数
|
||
echo ""
|
||
read -p "请输入 SSH 端口 [默认: 22]: " ssh_port
|
||
ssh_port=${ssh_port:-22}
|
||
|
||
read -p "请输入 SSH 用户 [默认: root]: " ssh_user
|
||
ssh_user=${ssh_user:-root}
|
||
|
||
# 隧道 IP 类型
|
||
echo ""
|
||
echo "选择隧道内部 IP 类型:"
|
||
echo " 1) IPv4 (10.0.0.x)"
|
||
echo " 2) IPv6 (fd00::x)"
|
||
echo ""
|
||
read -p "请输入选项 [1-2,默认 2]: " tunnel_ip_choice
|
||
tunnel_ip_choice=${tunnel_ip_choice:-2}
|
||
|
||
case $tunnel_ip_choice in
|
||
1)
|
||
tunnel_ip_type="ipv4"
|
||
default_local="10.0.0.1"
|
||
default_remote="10.0.0.2"
|
||
;;
|
||
2)
|
||
tunnel_ip_type="ipv6"
|
||
default_local="fd00::1"
|
||
default_remote="fd00::2"
|
||
;;
|
||
*) print_error "无效的选项"; exit 1 ;;
|
||
esac
|
||
print_info "已选择隧道 IP 类型: $tunnel_ip_type"
|
||
|
||
# 隧道 IP
|
||
echo ""
|
||
print_info "配置隧道内部 IP 地址:"
|
||
read -p "请输入本地隧道 IP [默认: $default_local]: " tunnel_ip_local
|
||
tunnel_ip_local=${tunnel_ip_local:-$default_local}
|
||
|
||
read -p "请输入对端隧道 IP [默认: $default_remote]: " tunnel_ip_remote
|
||
tunnel_ip_remote=${tunnel_ip_remote:-$default_remote}
|
||
|
||
# 验证 IP
|
||
if [[ $tunnel_ip_type == "ipv4" ]]; then
|
||
if ! validate_ipv4 "$tunnel_ip_local" || ! validate_ipv4 "$tunnel_ip_remote"; then
|
||
print_error "无效的 IPv4 地址"
|
||
exit 1
|
||
fi
|
||
else
|
||
if ! validate_ipv6 "$tunnel_ip_local" || ! validate_ipv6 "$tunnel_ip_remote"; then
|
||
print_error "无效的 IPv6 地址"
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
# 如果是客户端,交换 IP
|
||
if [[ $ssh_role == "client" ]]; then
|
||
local temp=$tunnel_ip_local
|
||
tunnel_ip_local=$tunnel_ip_remote
|
||
tunnel_ip_remote=$temp
|
||
fi
|
||
|
||
else
|
||
# GRE/VXLAN 原有逻辑
|
||
if [[ $ip_type == "ipv4" ]]; then
|
||
read -p "请输入本地 IPv4 地址: " local_ip
|
||
if ! validate_ipv4 "$local_ip"; then
|
||
print_error "无效的 IPv4 地址"
|
||
exit 1
|
||
fi
|
||
else
|
||
read -p "请输入本地 IPv6 地址: " local_ip
|
||
if ! validate_ipv6 "$local_ip"; then
|
||
print_error "无效的 IPv6 地址"
|
||
exit 1
|
||
fi
|
||
fi
|
||
|
||
# 获取远端 IP
|
||
echo ""
|
||
if [[ $ip_type == "ipv4" ]]; then
|
||
read -p "请输入远端 IPv4 地址: " remote_ip
|
||
if ! validate_ipv4 "$remote_ip"; then
|
||
print_error "无效的 IPv4 地址"
|
||
exit 1
|
||
fi
|
||
else
|
||
read -p "请输入远端 IPv6 地址: " remote_ip
|
||
if ! validate_ipv6 "$remote_ip"; then
|
||
print_error "无效的 IPv6 地址"
|
||
exit 1
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# 获取接口 - GRE/VXLAN 需要
|
||
local interface=""
|
||
local gateway=""
|
||
|
||
if [[ $tunnel_type != "sshtun" ]]; then
|
||
echo ""
|
||
print_info "选择隧道底层网络接口:"
|
||
|
||
# 列出可用接口
|
||
list_interfaces "$ip_type"
|
||
|
||
# 根据远端 IP 尝试自动检测接口
|
||
auto_interface=$(get_interface_for_ip "$remote_ip" "$ip_type")
|
||
|
||
if [[ -n "$auto_interface" ]]; then
|
||
print_info "自动检测到 $remote_ip 的接口: $auto_interface"
|
||
echo ""
|
||
read -p "按回车使用 [$auto_interface],或输入其他接口名: " manual_iface
|
||
if [[ -n "$manual_iface" ]]; then
|
||
interface="$manual_iface"
|
||
else
|
||
interface="$auto_interface"
|
||
fi
|
||
else
|
||
read -p "请输入接口名称 (例如: ens18, eth0): " interface
|
||
fi
|
||
|
||
if [[ -z "$interface" ]]; then
|
||
print_error "接口不能为空"
|
||
exit 1
|
||
fi
|
||
|
||
# 验证接口存在
|
||
if ! ip link show "$interface" &>/dev/null; then
|
||
print_error "接口 '$interface' 不存在"
|
||
exit 1
|
||
fi
|
||
|
||
print_info "已选择接口: $interface"
|
||
|
||
# IPv6 需要网关用于 PBR
|
||
if [[ $ip_type == "ipv6" ]]; then
|
||
echo ""
|
||
# 方法1: 获取特定远端 IP 的网关(最准确)
|
||
gateway=$(get_gateway_for_ip "$remote_ip" "$ip_type")
|
||
|
||
# 方法2: 回退到默认网关
|
||
if [[ -z "$gateway" ]]; then
|
||
gateway=$(get_default_gateway "$interface" "$ip_type")
|
||
fi
|
||
|
||
if [[ -n "$gateway" ]]; then
|
||
print_info "自动检测到网关: $gateway"
|
||
read -p "按回车使用此网关,或输入其他网关: " custom_gw
|
||
if [[ -n "$custom_gw" ]]; then
|
||
gateway="$custom_gw"
|
||
fi
|
||
else
|
||
print_warn "无法自动检测网关"
|
||
read -p "请输入网关 IPv6 地址 (点对点链路可留空): " gateway
|
||
fi
|
||
print_info "PBR 网关: ${gateway:-<无,直接使用接口>}"
|
||
fi
|
||
fi
|
||
|
||
if [[ $tunnel_type == "vxlan" ]]; then
|
||
# 获取 VXLAN 端口
|
||
echo ""
|
||
read -p "请输入 VXLAN 目标端口 [默认: 4789]: " dst_port
|
||
dst_port=${dst_port:-4789}
|
||
|
||
# 获取 VNI
|
||
echo ""
|
||
read -p "请输入 VXLAN 网络标识符 (VNI) [默认: 100]: " vni
|
||
vni=${vni:-100}
|
||
fi
|
||
|
||
# 确认设置
|
||
echo ""
|
||
echo "========================================"
|
||
echo "配置摘要:"
|
||
echo "========================================"
|
||
echo "隧道名称: $tunnel_name"
|
||
echo "隧道类型: $tunnel_type"
|
||
if [[ $tunnel_type == "sshtun" ]]; then
|
||
echo "角色: $ssh_role"
|
||
echo "对端 IP: ${remote_ip:-<可选>}"
|
||
echo "SSH 端口: $ssh_port"
|
||
echo "SSH 用户: $ssh_user"
|
||
echo "隧道 IP 类型: $tunnel_ip_type"
|
||
echo "本地隧道 IP: $tunnel_ip_local"
|
||
echo "对端隧道 IP: $tunnel_ip_remote"
|
||
else
|
||
echo "IP 版本: $ip_type"
|
||
echo "本地 IP: $local_ip"
|
||
echo "远端 IP: $remote_ip"
|
||
echo "接口: $interface"
|
||
if [[ $tunnel_type == "vxlan" ]]; then
|
||
echo "目标端口: $dst_port"
|
||
echo "VNI: $vni"
|
||
fi
|
||
if [[ $ip_type == "ipv6" ]]; then
|
||
echo "网关: ${gateway:-<无>}"
|
||
echo "PBR: 已启用 (使用专用路由表)"
|
||
fi
|
||
fi
|
||
echo "========================================"
|
||
echo ""
|
||
read -p "生成服务文件? [Y/n]: " confirm
|
||
|
||
if [[ "$confirm" =~ ^[Nn]$ ]]; then
|
||
print_info "已取消"
|
||
exit 0
|
||
fi
|
||
|
||
# 生成服务文件
|
||
if [[ $tunnel_type == "gre" ]]; then
|
||
generate_gre_service "$tunnel_name" "$remote_ip" "$local_ip" "$ip_type" "$interface" "$gateway"
|
||
elif [[ $tunnel_type == "vxlan" ]]; then
|
||
generate_vxlan_service "$tunnel_name" "$remote_ip" "$local_ip" "$dst_port" "$vni" "$ip_type" "$interface" "$gateway"
|
||
elif [[ $tunnel_type == "sshtun" ]]; then
|
||
local local_ip=""
|
||
[[ -z "$remote_ip" ]] && remote_ip="none"
|
||
generate_sshtun_service "$tunnel_name" "$remote_ip" "$local_ip" "$tunnel_ip_local" "$tunnel_ip_remote" "$ssh_port" "$ssh_user" "$ssh_role" "$tunnel_ip_type"
|
||
fi
|
||
|
||
# 显示生成的文件
|
||
echo ""
|
||
echo "========================================"
|
||
echo "生成的服务文件:"
|
||
echo "========================================"
|
||
cat "/etc/systemd/system/${tunnel_name}.service"
|
||
echo "========================================"
|
||
echo ""
|
||
|
||
# 询问是否启用并启动
|
||
read -p "立即启用并启动服务? [Y/n]: " enable_now
|
||
|
||
if [[ ! "$enable_now" =~ ^[Nn]$ ]]; then
|
||
systemctl daemon-reload
|
||
systemctl enable "${tunnel_name}.service"
|
||
systemctl start "${tunnel_name}.service"
|
||
|
||
echo ""
|
||
systemctl status "${tunnel_name}.service" --no-pager
|
||
|
||
# 显示隧道接口
|
||
echo ""
|
||
if [[ $tunnel_type == "sshtun" ]]; then
|
||
if [[ $ssh_role == "server" ]]; then
|
||
print_info "SSH TUN 服务端已就绪"
|
||
echo ""
|
||
echo "下一步操作:"
|
||
echo " 1. 确保客户端的 SSH 公钥已添加到本机"
|
||
echo " 客户端运行: cat ~/.ssh/id_ed25519.pub"
|
||
echo " 本机执行: echo '<公钥>' >> /root/.ssh/authorized_keys"
|
||
echo ""
|
||
echo " 2. 客户端连接后,运行以下命令配置 IP:"
|
||
echo " /usr/local/sbin/${tunnel_name}-ip-config.sh"
|
||
echo ""
|
||
echo " 3. 验证隧道:"
|
||
echo " ping $tunnel_ip_remote"
|
||
else
|
||
print_info "SSH TUN 客户端服务已启动"
|
||
echo ""
|
||
# 检查 tun0 是否存在
|
||
sleep 3
|
||
if ip link show tun0 &>/dev/null; then
|
||
print_info "隧道接口 tun0 已创建"
|
||
ip addr show tun0
|
||
echo ""
|
||
echo "验证隧道:"
|
||
echo " ping $tunnel_ip_remote"
|
||
else
|
||
print_warn "隧道接口尚未创建,请检查 SSH 连接"
|
||
fi
|
||
fi
|
||
else
|
||
print_info "隧道接口状态:"
|
||
ip link show "$tunnel_name" 2>/dev/null || print_warn "接口未找到 (可能需要手动配置)"
|
||
|
||
if [[ $ip_type == "ipv6" ]]; then
|
||
echo ""
|
||
print_info "IPv6 PBR 规则 (专用表: ${tunnel_name}_underlay):"
|
||
ip -6 rule list priority 50 2>/dev/null || print_warn "优先级 50 没有 PBR 规则"
|
||
ip -6 rule list priority 51 2>/dev/null || print_warn "优先级 51 没有 PBR 规则"
|
||
echo ""
|
||
print_info "${tunnel_name}_underlay 表中的路由:"
|
||
ip -6 route show table "${tunnel_name}_underlay" 2>/dev/null || print_warn "表中没有路由"
|
||
fi
|
||
fi
|
||
else
|
||
print_info "手动启用和启动命令:"
|
||
echo " systemctl daemon-reload"
|
||
echo " systemctl enable ${tunnel_name}.service"
|
||
echo " systemctl start ${tunnel_name}.service"
|
||
fi
|
||
|
||
# IPv6 与 bird 的额外说明 (仅 GRE/VXLAN)
|
||
if [[ $tunnel_type != "sshtun" ]] && [[ $ip_type == "ipv6" ]]; then
|
||
echo ""
|
||
print_info "IPv6 PBR 说明 (bird 兼容性):"
|
||
echo ""
|
||
echo " 已创建专用路由表 '${tunnel_name}_underlay'"
|
||
echo " 用于防止 bird 覆盖隧道端点路由。"
|
||
echo ""
|
||
echo " 工作原理:"
|
||
echo " 1. 到/从隧道端点的流量使用干净的路由表"
|
||
echo " 2. 此表不受 bird 路由通告的影响"
|
||
echo " 3. PBR 规则优先级 50-51 (在 main 表 32766 之前)"
|
||
echo ""
|
||
echo " 验证 PBR 是否工作:"
|
||
echo " ip -6 route get $remote_ip"
|
||
echo " # 应显示: dev $interface (物理接口),而不是 dev $tunnel_name"
|
||
echo ""
|
||
echo " 检查 PBR 规则:"
|
||
echo " ip -6 rule show | grep priority"
|
||
echo ""
|
||
echo " 检查干净表中的路由:"
|
||
echo " ip -6 route show table ${tunnel_name}_underlay"
|
||
echo ""
|
||
echo " 重要: 如果物理网关变更,请更新:"
|
||
echo " /usr/local/sbin/${tunnel_name}-pbr-setup.sh"
|
||
echo " 然后重启服务: systemctl restart $tunnel_name"
|
||
fi
|
||
|
||
# SSH TUN 额外说明
|
||
if [[ $tunnel_type == "sshtun" ]]; then
|
||
echo ""
|
||
print_info "SSH TUN 隧道说明:"
|
||
echo ""
|
||
echo " 特点:"
|
||
echo " - 基于 SSH 协议,使用 TCP"
|
||
echo " - 适用于 UDP 受限的环境"
|
||
echo " - 客户端主动连接服务端"
|
||
echo ""
|
||
echo " 性能:"
|
||
echo " - 由于 SSH 加密,性能低于 GRE/VXLAN"
|
||
echo " - 典型速度: 100-500 Mbps"
|
||
echo ""
|
||
echo " 注意事项:"
|
||
echo " - 客户端服务会自动重连"
|
||
echo " - 服务端需要手动配置 IP (运行 IP 配置脚本)"
|
||
echo " - 确保 SSH 密钥认证已配置"
|
||
fi
|
||
|
||
print_info "完成!"
|
||
}
|
||
|
||
# 运行主函数
|
||
main "$@"
|