918 lines
24 KiB
Go
918 lines
24 KiB
Go
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()
|
|
}
|