diff --git a/l2tp/go.mod b/l2tp/go.mod new file mode 100644 index 0000000..107993a --- /dev/null +++ b/l2tp/go.mod @@ -0,0 +1,3 @@ +module l2tp + +go 1.25.1 diff --git a/l2tp/l2tp b/l2tp/l2tp new file mode 100644 index 0000000..5504d49 Binary files /dev/null and b/l2tp/l2tp differ diff --git a/l2tp/l2tp.go b/l2tp/l2tp.go new file mode 100644 index 0000000..67d05fc --- /dev/null +++ b/l2tp/l2tp.go @@ -0,0 +1,954 @@ +package main + +import ( + "bufio" + "context" + "crypto/tls" + "fmt" + "io" + "net/http" + "os" + "os/exec" + "regexp" + "runtime" + "strconv" + "strings" + "sync" + "time" +) + +// ========================================== +// 全局常量与变量 +// ========================================== + +const ( + ExpireDate = "2025-12-30 23:59:59" +) + +var ( + // 颜色代码 + Red = "\033[31m" + Green = "\033[32m" + Yellow = "\033[33m" + Blue = "\033[34m" + Nc = "\033[0m" + RedGloba = "\033[41;37m" + GreenGloba = "\033[42;37m" + YellowGloba = "\033[43;37m" + BlueGloba = "\033[44;37m" + + // 日志前缀 + Info = fmt.Sprintf("%s[信息]%s", Green, Nc) + Error = fmt.Sprintf("%s[错误]%s", Red, Nc) + Tip = fmt.Sprintf("%s[提示]%s", Yellow, Nc) + + reader = bufio.NewReader(os.Stdin) +) + +// ========================================== +// 工具函数 +// ========================================== + +func printColor(color, text string) { + fmt.Printf("%s%s%s\n", color, text, Nc) +} + +// runCommand 执行 Shell 命令,增加超时控制和错误检查 +func runCommand(name string, args ...string) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) + defer cancel() + + cmd := exec.CommandContext(ctx, name, args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + if err := cmd.Run(); err != nil { + if ctx.Err() == context.DeadlineExceeded { + return fmt.Errorf("命令执行超时: %s %v", name, args) + } + return fmt.Errorf("命令执行失败: %v", err) + } + return nil +} + +// runCommandOutput 执行命令并获取输出,增加超时 +func runCommandOutput(name string, args ...string) (string, error) { + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute) + defer cancel() + + cmd := exec.CommandContext(ctx, name, args...) + out, err := cmd.CombinedOutput() + if err != nil { + if ctx.Err() == context.DeadlineExceeded { + return "", fmt.Errorf("命令执行超时") + } + return "", err + } + return strings.TrimSpace(string(out)), nil +} + +func fileExists(filename string) bool { + info, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} + +func dirExists(dirname string) bool { + info, err := os.Stat(dirname) + if os.IsNotExist(err) { + return false + } + return info.IsDir() +} + +func readInput(prompt string, defaultValue string) string { + if defaultValue != "" { + fmt.Printf("%s (默认: %s): ", prompt, defaultValue) + } else { + fmt.Printf("%s: ", prompt) + } + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(input) + if input == "" { + return defaultValue + } + return input +} + +func askYesNo(prompt string) bool { + for { + fmt.Printf("%s [y/N]: ", prompt) + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(strings.ToLower(input)) + if input == "y" || input == "yes" { + return true + } + if input == "n" || input == "no" || input == "" { + return false + } + } +} + +// 随机字符串生成 +func randString(length int) string { + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + b := make([]byte, length) + for i := range b { + b[i] = charset[time.Now().UnixNano()%int64(len(charset))] + } + return string(b) +} + +// ========================================== +// 文件操作辅助函数 +// ========================================== + +// updateConfigFile 类似于 sed -i,如果 key 存在则更新,不存在则追加 +func updateConfigFile(filePath string, configs map[string]string, separator string) error { + content, err := os.ReadFile(filePath) + if err != nil && !os.IsNotExist(err) { + return err + } + + lines := strings.Split(string(content), "\n") + newLines := make([]string, 0, len(lines)+len(configs)) + processedKeys := make(map[string]bool) + + // 遍历现有行进行更新 + for _, line := range lines { + trimmedLine := strings.TrimSpace(line) + updated := false + for key, value := range configs { + // 简单的匹配逻辑:行以 "key=" 开头(忽略空格) + // 注意:这里需要根据具体配置文件格式调整,sysctl 是 key = value + + if strings.HasPrefix(trimmedLine, "#") { + continue + } + + // 检查是否匹配 key (去掉空格后) + cleanLine := strings.ReplaceAll(trimmedLine, " ", "") + cleanKey := strings.ReplaceAll(key, " ", "") + + if strings.HasPrefix(cleanLine, cleanKey+strings.TrimSpace(separator)) { + newLines = append(newLines, key+separator+value) + processedKeys[key] = true + updated = true + break + } + } + if !updated { + newLines = append(newLines, line) + } + } + + // 追加未找到的配置 + for key, value := range configs { + if !processedKeys[key] { + newLines = append(newLines, key+separator+value) + } + } + + // 移除末尾可能的空行并重新组合 + output := strings.Join(newLines, "\n") + // 确保文件末尾有换行 + if !strings.HasSuffix(output, "\n") { + output += "\n" + } + + return os.WriteFile(filePath, []byte(output), 0644) +} + +// ========================================== +// 1. 过期检查模块 (proxy/time.go) +// ========================================== + +func checkExpiration() error { + urls := []string{ + "https://www.cloudflare.com/cdn-cgi/trace", + "https://www.visa.cn/cdn-cgi/trace", + } + + beijingLocation := time.FixedZone("Asia/Shanghai", 8*3600) + pattern := regexp.MustCompile(`ts=(\d+)`) + + client := &http.Client{ + Timeout: 5 * time.Second, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + } + + var beijingTime time.Time + success := false + + for _, url := range urls { + resp, err := client.Get(url) + if err != nil { + continue + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + continue + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + continue + } + + match := pattern.FindStringSubmatch(string(body)) + if len(match) < 2 { + continue + } + + timestamp, err := strconv.ParseInt(match[1], 10, 64) + if err != nil { + continue + } + + utcTime := time.Unix(timestamp, 0).UTC() + beijingTime = utcTime.In(beijingLocation) + success = true + break + } + + if !success { + return fmt.Errorf("无法验证有效期") + } + + expireTime, err := time.ParseInLocation("2006-01-02 15:04:05", ExpireDate, beijingLocation) + if err != nil { + return fmt.Errorf("解析时间失败: %v", err) + } + + if beijingTime.After(expireTime) { + return fmt.Errorf("当前脚本已过期,请联系管理员获取更新") + } + + return nil +} + +// ========================================== +// 2. 内核切换模块 (dev/image.sh) +// ========================================== + +func detectRegion() bool { + fmt.Printf("%s 检测网络位置...\n", Blue) + urls := []string{ + "https://www.cloudflare.com/cdn-cgi/trace", + "https://www.visa.cn/cdn-cgi/trace", + } + client := &http.Client{Timeout: 5 * time.Second} + + for _, url := range urls { + resp, err := client.Get(url) + if err != nil { + continue + } + body, _ := io.ReadAll(resp.Body) + resp.Body.Close() + + if strings.Contains(string(body), "loc=CN") { + fmt.Printf("%s CN 网络环境\n", Green) + return true + } + } + fmt.Printf("%s 非 CN 网络环境\n", Green) + return false +} + +func changeMirrors() { + fmt.Printf("%s [0/5] 配置软件源\n", Yellow) + isCN := detectRegion() + + var cmdStr string + if isCN { + fmt.Println("使用阿里云镜像...") + cmdStr = `bash <(curl -sSL https://gitee.com/SuperManito/LinuxMirrors/raw/main/ChangeMirrors.sh) --source mirrors.aliyun.com --protocol http --use-intranet-source false --install-epel true --backup true --upgrade-software false --clean-cache false --ignore-backup-tips --pure-mode` + } else { + fmt.Println("使用官方源...") + cmdStr = `bash <(curl -sSL https://raw.githubusercontent.com/SuperManito/LinuxMirrors/main/ChangeMirrors.sh) --use-official-source true --protocol http --use-intranet-source false --install-epel true --backup true --upgrade-software false --clean-cache false --ignore-backup-tips --pure-mode` + } + + // 这里调用 bash 执行脚本 + cmd := exec.Command("bash", "-c", cmdStr) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + fmt.Printf("%s 警告:软件源切换失败,继续使用当前源\n", Yellow) + } else { + exec.Command("apt", "update", "-qq").Run() + } +} + +func checkCloudKernel() (bool, []string) { + out, _ := runCommandOutput("uname", "-r") + isCloud := strings.Contains(out, "cloud") + + // 查找 cloud 包 + dpkgOut, _ := runCommandOutput("bash", "-c", "dpkg -l | awk '/linux-(image|headers)-[0-9].*cloud/ {print $2}'") + pkgs := strings.Fields(dpkgOut) + + return isCloud, pkgs +} + +func installStandardKernel() error { + fmt.Printf("%s [1/5] 安装标准内核\n", Yellow) + + imagePkg := "linux-image-amd64" + headersPkg := "linux-headers-amd64" + + // 简单判断 Ubuntu + if releaseOut, _ := os.ReadFile("/etc/os-release"); strings.Contains(string(releaseOut), "Ubuntu") { + imagePkg = "linux-image-generic" + headersPkg = "linux-headers-generic" + } + + fmt.Printf("正在安装 %s %s ...\n", imagePkg, headersPkg) + + // 设置 DEBIAN_FRONTEND=noninteractive + cmd := exec.Command("apt", "install", "-y", "--reinstall", imagePkg, headersPkg) + cmd.Env = append(os.Environ(), "DEBIAN_FRONTEND=noninteractive") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return fmt.Errorf("标准内核安装失败") + } + + // 更新 initramfs + // 查找最新非 cloud 内核 + cmdStr := `ls /boot/vmlinuz-* 2>/dev/null | grep -v cloud | sort -V | tail -1 | sed 's|/boot/vmlinuz-||'` + stdKernel, _ := runCommandOutput("bash", "-c", cmdStr) + if stdKernel != "" { + fmt.Printf("更新 initramfs: %s\n", stdKernel) + runCommand("update-initramfs", "-u", "-k", stdKernel) + } + + fmt.Printf("%s ✓ 标准内核安装完成: %s\n", Green, stdKernel) + return nil +} + +func removeCloudKernels(pkgs []string) { + fmt.Printf("%s [2/5] 卸载所有 Cloud 内核\n", Yellow) + if len(pkgs) == 0 { + fmt.Printf("%s 未找到 Cloud 内核包\n", Yellow) + return + } + + fmt.Println("正在卸载以下包:", pkgs) + + // unhold + args := append([]string{"unhold"}, pkgs...) + exec.Command("apt-mark", args...).Run() + + // purge + purgeArgs := append([]string{"purge", "-y"}, pkgs...) + cmd := exec.Command("apt", purgeArgs...) + cmd.Env = append(os.Environ(), "DEBIAN_FRONTEND=noninteractive") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Run() + + exec.Command("apt", "autoremove", "-y", "--purge").Run() + fmt.Printf("%s ✓ Cloud 内核清理流程结束\n", Green) +} + +func updateGrub() { + fmt.Printf("%s [3/5] 配置与更新 GRUB\n", Yellow) + + grubConfig := `GRUB_DEFAULT=0 +GRUB_TIMEOUT=5 +GRUB_DISTRIBUTOR=$(lsb_release -i -s 2> /dev/null || echo Debian) +GRUB_CMDLINE_LINUX_DEFAULT="quiet" +GRUB_CMDLINE_LINUX="" +GRUB_DISABLE_OS_PROBER=true +` + // 备份 + os.MkdirAll("/root/grub_backup", 0755) + runCommand("cp", "/etc/default/grub", fmt.Sprintf("/root/grub_backup/grub.default.%d", time.Now().Unix())) + + distributor := "Debian" + if out, err := runCommandOutput("lsb_release", "-i", "-s"); err == nil { + distributor = out + } + + finalGrubConfig := strings.Replace(grubConfig, "$(lsb_release -i -s 2> /dev/null || echo Debian)", distributor, 1) + + os.WriteFile("/etc/default/grub", []byte(finalGrubConfig), 0644) + + fmt.Println("重新生成 GRUB 配置...") + runCommand("update-grub") + runCommand("grub-set-default", "0") + + if dirExists("/sys/firmware/efi") { + fmt.Println("更新 UEFI 引导...") + runCommand("grub-install", "--target=x86_64-efi", "--efi-directory=/boot/efi", "--bootloader-id=debian", "--recheck") + } + + fmt.Printf("%s ✓ GRUB 更新完成\n", Green) +} + +func performKernelSwap() { + osInfo := getOSInfo() + if osInfo.ID != "debian" && osInfo.ID != "ubuntu" && osInfo.ID != "kali" { + fmt.Printf("%s 错误: 内核切换功能仅支持 Debian/Ubuntu 系统 (当前检测为: %s)\n", Error, osInfo.ID) + return + } + + fmt.Printf("\n%s⚠️ 高危操作警告 ⚠️%s\n", Red, Nc) + fmt.Println("即将执行:更换软件源 -> 安装标准内核 -> 卸载 Cloud 内核 -> 更新引导") + if !askYesNo("确认继续?") { + fmt.Println("操作已取消") + return + } + + // 确保基础工具存在 (curl, ca-certificates) + // 仅针对 Debian/Ubuntu,因为只有它们涉及此内核切换逻辑 + runCommand("apt-get", "update", "-qq") + runCommand("apt-get", "install", "-y", "-qq", "curl", "ca-certificates") + + changeMirrors() + if err := installStandardKernel(); err != nil { + fmt.Println(Error, err) + return + } + + _, pkgs := checkCloudKernel() + removeCloudKernels(pkgs) + + updateGrub() + + fmt.Printf("\n%s内核切换操作完成!需要重启生效。%s\n", Green, Nc) + if askYesNo("立即重启?") { + runCommand("reboot") + } else { + fmt.Println("请稍后手动重启。") + } + os.Exit(0) // 无论是否重启,都应退出当前脚本 +} + +// ========================================== +// 3. L2TP VPN 安装模块 (proxy/l2tp.sh) +// ========================================== + +type OSInfo struct { + ID string + VersionID string +} + +func getOSInfo() OSInfo { + content, err := os.ReadFile("/etc/os-release") + if err != nil { + // 尝试 /usr/lib/os-release + content, _ = os.ReadFile("/usr/lib/os-release") + } + + info := OSInfo{} + lines := strings.Split(string(content), "\n") + for _, line := range lines { + if strings.HasPrefix(line, "ID=") { + info.ID = strings.Trim(strings.TrimPrefix(line, "ID="), "\"") + } + if strings.HasPrefix(line, "VERSION_ID=") { + info.VersionID = strings.Trim(strings.TrimPrefix(line, "VERSION_ID="), "\"") + } + } + return info +} + +func installDependencies(osInfo OSInfo) { + fmt.Printf("%s 正在检查并安装依赖...%s\n", Tip, Nc) + + var updateCmd, installCmd string + apps := []string{"curl", "xl2tpd", "strongswan", "pptpd", "nftables"} + + switch osInfo.ID { + case "debian", "ubuntu", "kali": + updateCmd = "apt update -y" + installCmd = "apt install -y" + case "alpine": + updateCmd = "apk update -f" + installCmd = "apk add -f" + case "centos", "almalinux", "rocky", "oracle", "fedora": + updateCmd = "dnf update -y" + installCmd = "dnf install -y" + if osInfo.ID == "centos" { + updateCmd = "yum update -y" + installCmd = "yum install -y" + } + default: + fmt.Printf("%s 不支持的操作系统: %s\n", Error, osInfo.ID) + os.Exit(1) + } + + // 执行更新 + if err := runCommand("bash", "-c", updateCmd); err != nil { + fmt.Printf("%s 警告: 系统更新失败,尝试继续安装...\n", Tip) + } + + // 安装软件包 + // 这里简化处理,直接拼接命令 + fullInstallCmd := fmt.Sprintf("%s %s ppp", installCmd, strings.Join(apps, " ")) + + fmt.Printf("%s 执行安装命令: %s\n", Tip, fullInstallCmd) + if err := runCommand("bash", "-c", fullInstallCmd); err != nil { + fmt.Printf("%s 错误: 依赖安装失败,脚本退出。\n", Error) + os.Exit(1) + } +} + +// getPublicIP 并发获取公网IP,取最快响应 +func getPublicIP() string { + apis := []string{ + "http://api64.ipify.org", + "http://4.ipw.cn", + "http://ip.sb", + "http://checkip.amazonaws.com", + "http://icanhazip.com", + "http://ipinfo.io/ip", + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + resultChan := make(chan string, 1) + var wg sync.WaitGroup + + for _, url := range apis { + wg.Add(1) + go func(apiURL string) { + defer wg.Done() + + req, err := http.NewRequestWithContext(ctx, "GET", apiURL, nil) + if err != nil { + return + } + + client := &http.Client{} + resp, err := client.Do(req) + if err == nil { + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + ip := strings.TrimSpace(string(body)) + + // 简单的 IP 格式校验 + if ip != "" && !strings.Contains(ip, "<") { + select { + case resultChan <- ip: + cancel() // 找到一个就取消其他请求 + default: + } + } + } + }(url) + } + + go func() { + wg.Wait() + close(resultChan) + }() + + select { + case ip := <-resultChan: + if ip != "" { + return ip + } + case <-ctx.Done(): + } + + return "127.0.0.1" // Fallback +} + +func setupSysctl() { + configs := map[string]string{ + "net.ipv4.ip_forward": "1", + "net.ipv4.conf.all.send_redirects": "0", + "net.ipv4.conf.default.send_redirects": "0", + "net.ipv4.conf.all.accept_redirects": "0", + "net.ipv4.conf.default.accept_redirects": "0", + } + + fmt.Println(Tip, "正在配置 Sysctl 参数...") + if err := updateConfigFile("/etc/sysctl.conf", configs, " = "); err != nil { + fmt.Printf("%s 警告: 更新 sysctl.conf 失败: %v\n", Tip, err) + } + + runCommand("sysctl", "-p") +} + +func setupNftables(l2tpPort, pptpPort, l2tpLocIP, pptpLocIP string) { + // 备份 + if fileExists("/etc/nftables.conf") { + runCommand("cp", "/etc/nftables.conf", fmt.Sprintf("/etc/nftables.conf.old.%d", time.Now().Unix())) + } + + interfaceName := "eth0" + // 获取默认网卡接口 + out, err := runCommandOutput("bash", "-c", "ip route get 8.8.8.8 | awk '{print $5; exit}'") + if err == nil && out != "" { + interfaceName = out + } + + config := fmt.Sprintf(`#!/usr/sbin/nft -f + +flush ruleset + +table inet filter { + chain input { + type filter hook input priority 0; + ct state established,related accept + ip protocol icmp accept + iif lo accept + udp dport {500,4500,%s,%s} accept + accept + } + chain forward { + type filter hook forward priority 0; + ct state established,related accept + ip saddr %s.0/24 accept + ip saddr %s.0/24 accept + accept + } + chain output { + type filter hook output priority 0; + accept + } +} + +table ip nat { + chain prerouting { + type nat hook prerouting priority 0; + accept + } + chain postrouting { + type nat hook postrouting priority 100; + oif "%s" masquerade + } + chain output { + type nat hook output priority 0; + accept + } +} +`, l2tpPort, pptpPort, l2tpLocIP, pptpLocIP, interfaceName) + + os.WriteFile("/etc/nftables.conf", []byte(config), 0755) + runCommand("systemctl", "daemon-reload") + runCommand("systemctl", "enable", "nftables") + runCommand("systemctl", "restart", "nftables") +} + +func installVPN() { + // 获取公网IP + publicIP := getPublicIP() + + fmt.Println() + // L2TP 配置 + fmt.Println(Tip, "请输入 L2TP IP范围:") + l2tpLocIP := readInput("(默认范围: 10.10.10)", "10.10.10") + + fmt.Println(Tip, "请输入 L2TP 端口:") + l2tpPort := readInput("(默认端口: 1701)", "1701") + + l2tpUser := randString(5) + fmt.Printf("%s 请输入 L2TP 用户名:\n", Tip) + l2tpUser = readInput(fmt.Sprintf("(默认用户名: %s)", l2tpUser), l2tpUser) + + l2tpPass := randString(7) + fmt.Printf("%s 请输入 %s 的密码:\n", Tip, l2tpUser) + l2tpPass = readInput(fmt.Sprintf("(默认密码: %s)", l2tpPass), l2tpPass) + + l2tpPSK := randString(20) + fmt.Printf("%s 请输入 L2TP PSK 密钥:\n", Tip) + l2tpPSK = readInput(fmt.Sprintf("(默认PSK: %s)", l2tpPSK), l2tpPSK) + + // PPTP 配置 + fmt.Println(Tip, "请输入 PPTP IP范围:") + pptpLocIP := readInput("(默认范围: 192.168.30)", "192.168.30") + + fmt.Println(Tip, "请输入 PPTP 端口:") + pptpPort := readInput("(默认端口: 1723)", "1723") + + pptpUser := randString(5) + fmt.Printf("%s 请输入 PPTP 用户名:\n", Tip) + pptpUser = readInput(fmt.Sprintf("(默认用户名: %s)", pptpUser), pptpUser) + + pptpPass := randString(7) + fmt.Printf("%s 请输入 %s 的密码:\n", Tip, pptpUser) + pptpPass = readInput(fmt.Sprintf("(默认密码: %s)", pptpPass), pptpPass) + + // 展示配置信息 + fmt.Println() + fmt.Printf("%s L2TP服务器本地IP: %s%s.1%s\n", Info, Green, l2tpLocIP, Nc) + fmt.Printf("%s L2TP客户端IP范围: %s%s.11-%s.255%s\n", Info, Green, l2tpLocIP, l2tpLocIP, Nc) + fmt.Printf("%s L2TP端口 : %s%s%s\n", Info, Green, l2tpPort, Nc) + fmt.Printf("%s L2TP用户名 : %s%s%s\n", Info, Green, l2tpUser, Nc) + fmt.Printf("%s L2TP密码 : %s%s%s\n", Info, Green, l2tpPass, Nc) + fmt.Printf("%s L2TPPSK密钥 : %s%s%s\n", Info, Green, l2tpPSK, Nc) + fmt.Println() + fmt.Printf("%s PPTP服务器本地IP: %s%s.1%s\n", Info, Green, pptpLocIP, Nc) + fmt.Printf("%s PPTP客户端IP范围: %s%s.11-%s.255%s\n", Info, Green, pptpLocIP, pptpLocIP, Nc) + fmt.Printf("%s PPTP端口 : %s%s%s\n", Info, Green, pptpPort, Nc) + fmt.Printf("%s PPTP用户名 : %s%s%s\n", Info, Green, pptpUser, Nc) + fmt.Printf("%s PPTP密码 : %s%s%s\n", Info, Green, pptpPass, Nc) + fmt.Println() + + fmt.Println("正在生成配置文件...") + + // /etc/ipsec.conf + ipsecConf := fmt.Sprintf(`config setup + charondebug="ike 2, knl 2, cfg 2" + uniqueids=no + +conn %%default + keyexchange=ikev1 + authby=secret + ike=aes256-sha1-modp1024,aes128-sha1-modp1024,3des-sha1-modp1024! + esp=aes256-sha1,aes128-sha1,3des-sha1! + keyingtries=3 + ikelifetime=8h + lifetime=1h + dpdaction=clear + dpddelay=30s + dpdtimeout=120s + rekey=no + forceencaps=yes + fragmentation=yes + +conn L2TP-PSK + left=%%any + leftid=%s + leftfirewall=yes + leftprotoport=17/%s + right=%%any + rightprotoport=17/%%any + type=transport + auto=add + also=%%default +`, publicIP, l2tpPort) + os.WriteFile("/etc/ipsec.conf", []byte(ipsecConf), 0644) + + // /etc/ipsec.secrets + ipsecSecrets := fmt.Sprintf(`%%any %%any : PSK "%s" +`, l2tpPSK) + os.WriteFile("/etc/ipsec.secrets", []byte(ipsecSecrets), 0600) + + // /etc/xl2tpd/xl2tpd.conf + xl2tpdConf := fmt.Sprintf(`[global] +port = %s + +[lns default] +ip range = %s.11-%s.255 +local ip = %s.1 +require chap = yes +refuse pap = yes +require authentication = yes +name = l2tpd +ppp debug = yes +pppoptfile = /etc/ppp/options.xl2tpd +length bit = yes +`, l2tpPort, l2tpLocIP, l2tpLocIP, l2tpLocIP) + os.MkdirAll("/etc/xl2tpd", 0755) + os.WriteFile("/etc/xl2tpd/xl2tpd.conf", []byte(xl2tpdConf), 0644) + + // /etc/ppp/options.xl2tpd + pppOptXl2tpd := `ipcp-accept-local +ipcp-accept-remote +require-mschap-v2 +ms-dns 8.8.8.8 +ms-dns 114.114.114.114 +noccp +auth +hide-password +idle 1800 +mtu 1410 +mru 1410 +nodefaultroute +debug +proxyarp +connect-delay 5000 +` + os.MkdirAll("/etc/ppp", 0755) + os.WriteFile("/etc/ppp/options.xl2tpd", []byte(pppOptXl2tpd), 0644) + + // /etc/pptpd.conf + pptpdConf := fmt.Sprintf(`option /etc/ppp/pptpd-options +debug +localip %s.1 +remoteip %s.11-255 +`, pptpLocIP, pptpLocIP) + os.WriteFile("/etc/pptpd.conf", []byte(pptpdConf), 0644) + + // /etc/ppp/pptpd-options + pptpdOptions := `name pptpd +refuse-pap +refuse-chap +refuse-mschap +require-mschap-v2 +require-mppe-128 +ms-dns 8.8.8.8 +ms-dns 114.114.114.114 +proxyarp +lock +nobsdcomp +novj +novjccomp +nologfd +` + os.WriteFile("/etc/ppp/pptpd-options", []byte(pptpdOptions), 0644) + + // /etc/ppp/chap-secrets + chapSecrets := "# Secrets for authentication using CHAP\n# client server secret IP addresses\n" + + // 1. 添加主用户 (绑定静态 IP .10,避免与动态地址池 11-255 冲突) + chapSecrets += fmt.Sprintf("%s l2tpd %s %s.10\n", l2tpUser, l2tpPass, l2tpLocIP) + chapSecrets += fmt.Sprintf("%s pptpd %s %s.10\n", pptpUser, pptpPass, pptpLocIP) + + // 2. 批量生成用户 (IP 11-255),还原 shell 脚本中的逻辑 + for i := 11; i <= 255; i++ { + chapSecrets += fmt.Sprintf("%s%d l2tpd %s%d %s.%d\n", l2tpUser, i, l2tpPass, i, l2tpLocIP, i) + chapSecrets += fmt.Sprintf("%s%d pptpd %s%d %s.%d\n", pptpUser, i, pptpPass, i, pptpLocIP, i) + } + + os.WriteFile("/etc/ppp/chap-secrets", []byte(chapSecrets), 0600) + + // 设置系统和防火墙 + setupSysctl() + setupNftables(l2tpPort, pptpPort, l2tpLocIP, pptpLocIP) + + // 启动服务 + fmt.Println("正在启动服务...") + services := []string{"ipsec", "xl2tpd", "pptpd"} + + // 检查 ipsec 服务名兼容性 + if _, err := runCommandOutput("systemctl", "list-unit-files", "strongswan.service"); err == nil { + // 如果存在 strongswan.service,且没有 ipsec.service,则替换 + if _, err := runCommandOutput("systemctl", "list-unit-files", "ipsec.service"); err != nil { + services[0] = "strongswan" + } + } + runCommand("systemctl", "daemon-reload") + // 再次确保 IP 转发开启 + if err := os.WriteFile("/proc/sys/net/ipv4/ip_forward", []byte("1\n"), 0644); err != nil { + fmt.Printf("%s 警告: 无法写入 ip_forward: %v\n", Tip, err) + } + + for _, svc := range services { + runCommand("systemctl", "enable", svc) + runCommand("systemctl", "restart", svc) + } + + fmt.Println() + fmt.Printf("%s===============================================%s\n", Green, Nc) + fmt.Printf("%sVPN 安装完成%s\n", Green, Nc) + fmt.Printf("%s===============================================%s\n", Green, Nc) + fmt.Printf("请保留好以下信息:\n") + fmt.Printf("服务器IP: %s\n", publicIP) + fmt.Printf("L2TP PSK: %s\n", l2tpPSK) + fmt.Printf("L2TP 主账号: %s / 密码: %s\n", l2tpUser, l2tpPass) + fmt.Printf("PPTP 主账号: %s / 密码: %s\n", pptpUser, pptpPass) + fmt.Printf("\n%s 已自动生成批量账号,详情请查看 /etc/ppp/chap-secrets 文件%s\n", Tip, Nc) +} + +// ========================================== +// 主函数 +// ========================================== + +func main() { + // 清屏 + if runtime.GOOS == "linux" { + fmt.Print("\033[H\033[2J") + } + + fmt.Printf("%s###############################################################%s\n", Green, Nc) + fmt.Printf("%s# L2TP/IPSec & PPTP VPN 一键安装脚本 #%s\n", Green, Nc) + fmt.Printf("%s###############################################################%s\n", Green, Nc) + fmt.Println() + + // 1. 检查 Root + if os.Geteuid() != 0 { + fmt.Printf("%s 错误: 必须使用 root 权限运行此脚本\n", Error) + os.Exit(1) + } + + // 2. 时间过期检查 + if err := checkExpiration(); err != nil { + fmt.Printf("%s %v\n", Error, err) + os.Exit(1) + } + + // 3. 检查 OpenVZ + if dirExists("/proc/vz") { + fmt.Printf("%s 警告: 你的VPS基于OpenVZ,内核可能不支持IPSec。L2TP安装已取消。\n", Error) + os.Exit(1) + } + + // 4. 检查 PPP 支持与内核切换逻辑 + if !fileExists("/dev/ppp") { + fmt.Printf("%s 警告: 未检测到 /dev/ppp 设备,当前内核可能不支持 PPP。\n", Error) + uname, _ := runCommandOutput("uname", "-r") + fmt.Printf("%s 当前内核版本: %s\n", Tip, uname) + + if askYesNo("是否尝试切换到标准内核 (将卸载Cloud内核并重置GRUB)?") { + performKernelSwap() // 注意:此函数内可能会重启或退出 + } else { + fmt.Printf("%s 用户取消操作,无法继续安装 VPN。\n", Error) + os.Exit(1) + } + } else { + // 即使有 /dev/ppp,也可以检查一下是不是 cloud 内核,提示一下用户 + isCloud, _ := checkCloudKernel() + if isCloud { + fmt.Printf("%s 提示: 检测到当前运行在 Cloud 内核上,但 /dev/ppp 存在,可以继续。\n", Tip) + fmt.Printf("如果安装后无法连接,建议重新运行脚本并选择切换内核。\n") + } + } + + // 5. 安装 VPN + osInfo := getOSInfo() + installDependencies(osInfo) + installVPN() +}