gentunnel/tunnel-generator.sh

1069 lines
33 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#!/bin/bash
# GRE/VXLAN 隧道生成器
# 生成 GRE 或 VXLAN 隧道的 systemd 服务文件
# 支持 IPv4 和 IPv6IPv6 支持 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 "$@"