commit 4f374c54a31c5f8d1a6d14904e9d05e7736d9072 Author: yanglc Date: Wed May 20 16:21:04 2026 +0800 first commit diff --git a/tunnel-generator.sh b/tunnel-generator.sh new file mode 100644 index 0000000..9627554 --- /dev/null +++ b/tunnel-generator.sh @@ -0,0 +1,1068 @@ +#!/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 "$@"