Files
shell/l2tp/l2tp.go
user123 b7c8e95a70 add
2025-12-19 00:17:10 +08:00

918 lines
24 KiB
Go
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 更新或追加配置
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 {
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)
}
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
}
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`
}
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")
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"
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)
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
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
`
// 备份:仅当目录为空时备份
backupDir := "/root/grub_backup"
os.MkdirAll(backupDir, 0755)
files, _ := os.ReadDir(backupDir)
if len(files) == 0 {
runCommand("cp", "/etc/default/grub", fmt.Sprintf("%s/grub.default.bak", backupDir))
}
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
}
// 确保基础工具存在
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)
}
// OSInfo 系统信息
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))
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"
}
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") && !fileExists("/etc/nftables.conf.bak") {
runCommand("cp", "/etc/nftables.conf", "/etc/nftables.conf.bak")
}
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() {
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)
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)
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 {
if _, err := runCommandOutput("systemctl", "list-unit-files", "ipsec.service"); err != nil {
services[0] = "strongswan"
}
}
runCommand("systemctl", "daemon-reload")
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 {
isCloud, _ := checkCloudKernel()
if isCloud {
fmt.Printf("%s 提示: 检测到当前运行在 Cloud 内核上,但 /dev/ppp 存在,可以继续。\n", Tip)
fmt.Printf("如果安装后无法连接,建议重新运行脚本并选择切换内核。\n")
}
}
// 5. 安装 VPN
osInfo := getOSInfo()
installDependencies(osInfo)
installVPN()
}