349 lines
8.4 KiB
Go
349 lines
8.4 KiB
Go
package warp
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"os/exec"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
var (
|
|
ErrWARPNotInstalled = errors.New("WARP 未安装")
|
|
ErrWARPNotConnected = errors.New("WARP 未连接")
|
|
ErrIPRefreshFailed = errors.New("IP 刷新失败")
|
|
ErrIPPoolExhausted = errors.New("IP 池耗尽")
|
|
)
|
|
|
|
// WARPStatus WARP 状态
|
|
type WARPStatus struct {
|
|
Connected bool
|
|
AccountID string
|
|
DeviceID string
|
|
ClientID string
|
|
CurrentIP string
|
|
Country string
|
|
ConnectTime time.Time
|
|
}
|
|
|
|
// Client WARP 客户端
|
|
type Client struct {
|
|
socksPort int
|
|
refreshCooldown time.Duration
|
|
maxRefreshRetries int
|
|
retryDelayMin time.Duration
|
|
retryDelayMax time.Duration
|
|
logger *zap.Logger
|
|
mu sync.Mutex
|
|
lastRefresh time.Time
|
|
currentIP string
|
|
ipHistory []string
|
|
}
|
|
|
|
// NewClient 创建 WARP 客户端
|
|
func NewClient(socksPort int, refreshCooldown int, maxRefreshRetries int, retryDelayMin int, retryDelayMax int, logger *zap.Logger) *Client {
|
|
return &Client{
|
|
socksPort: socksPort,
|
|
refreshCooldown: time.Duration(refreshCooldown) * time.Second,
|
|
maxRefreshRetries: maxRefreshRetries,
|
|
retryDelayMin: time.Duration(retryDelayMin) * time.Second,
|
|
retryDelayMax: time.Duration(retryDelayMax) * time.Second,
|
|
logger: logger,
|
|
ipHistory: make([]string, 0, 100),
|
|
}
|
|
}
|
|
|
|
// Connect 连接 WARP
|
|
func (c *Client) Connect(ctx context.Context) error {
|
|
// 检查是否安装
|
|
if !c.isInstalled() {
|
|
return ErrWARPNotInstalled
|
|
}
|
|
|
|
// 连接 WARP
|
|
cmd := exec.CommandContext(ctx, "warp-cli", "connect")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("WARP 连接失败: %w, output: %s", err, string(output))
|
|
}
|
|
|
|
// 等待连接建立
|
|
for i := 0; i < 10; i++ {
|
|
time.Sleep(1 * time.Second)
|
|
status, err := c.Status(ctx)
|
|
if err == nil && status.Connected {
|
|
c.currentIP = status.CurrentIP
|
|
c.lastRefresh = time.Now()
|
|
c.logger.Info("WARP 连接成功", zap.String("ip", status.CurrentIP))
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return ErrWARPNotConnected
|
|
}
|
|
|
|
// Disconnect 断开 WARP
|
|
func (c *Client) Disconnect(ctx context.Context) error {
|
|
cmd := exec.CommandContext(ctx, "warp-cli", "disconnect")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("WARP 断开失败: %w, output: %s", err, string(output))
|
|
}
|
|
c.currentIP = ""
|
|
return nil
|
|
}
|
|
|
|
// Status 获取 WARP 状态
|
|
func (c *Client) Status(ctx context.Context) (*WARPStatus, error) {
|
|
cmd := exec.CommandContext(ctx, "warp-cli", "status")
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
status := &WARPStatus{}
|
|
outputStr := string(output)
|
|
|
|
// 解析状态
|
|
if strings.Contains(outputStr, "Status: Connected") {
|
|
status.Connected = true
|
|
} else if strings.Contains(outputStr, "Status: Disconnected") {
|
|
status.Connected = false
|
|
return status, nil
|
|
}
|
|
|
|
// 获取当前 IP
|
|
ip, err := c.GetCurrentIP(ctx)
|
|
if err == nil {
|
|
status.CurrentIP = ip
|
|
c.currentIP = ip
|
|
}
|
|
|
|
// 获取国家信息
|
|
country, _ := c.GetCountry(ctx)
|
|
status.Country = country
|
|
|
|
return status, nil
|
|
}
|
|
|
|
// RefreshIP 刷新 IP
|
|
func (c *Client) RefreshIP(ctx context.Context) (string, error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
// 检查冷却时间
|
|
if time.Since(c.lastRefresh) < c.refreshCooldown {
|
|
waitTime := c.refreshCooldown - time.Since(c.lastRefresh)
|
|
c.logger.Warn("IP 刷新冷却中", zap.Duration("wait_time", waitTime))
|
|
return "", fmt.Errorf("冷却中,请等待 %v", waitTime)
|
|
}
|
|
|
|
// 记录旧 IP
|
|
oldIP := c.currentIP
|
|
|
|
// 重试刷新
|
|
for attempt := 0; attempt < c.maxRefreshRetries; attempt++ {
|
|
c.logger.Info("尝试刷新 IP",
|
|
zap.Int("attempt", attempt+1),
|
|
zap.Int("max_retries", c.maxRefreshRetries),
|
|
zap.String("old_ip", oldIP),
|
|
)
|
|
|
|
// 断开连接
|
|
if err := c.Disconnect(ctx); err != nil {
|
|
c.logger.Error("断开 WARP 失败", zap.Error(err))
|
|
continue
|
|
}
|
|
|
|
// 随机等待
|
|
delay := c.retryDelayMin + time.Duration(randInt(0, int(c.retryDelayMax-c.retryDelayMin)))
|
|
time.Sleep(delay)
|
|
|
|
// 重新连接
|
|
if err := c.Connect(ctx); err != nil {
|
|
c.logger.Error("连接 WARP 失败", zap.Error(err))
|
|
continue
|
|
}
|
|
|
|
// 获取新 IP
|
|
newIP, err := c.GetCurrentIP(ctx)
|
|
if err != nil {
|
|
c.logger.Error("获取新 IP 失败", zap.Error(err))
|
|
continue
|
|
}
|
|
|
|
// 检查是否真的换到了新 IP
|
|
if newIP != oldIP {
|
|
c.currentIP = newIP
|
|
c.lastRefresh = time.Now()
|
|
c.ipHistory = append(c.ipHistory, newIP)
|
|
c.logger.Info("IP 刷新成功",
|
|
zap.String("old_ip", oldIP),
|
|
zap.String("new_ip", newIP),
|
|
zap.Int("attempt", attempt+1),
|
|
)
|
|
return newIP, nil
|
|
}
|
|
|
|
c.logger.Warn("获取到相同 IP,重试中",
|
|
zap.String("ip", newIP),
|
|
)
|
|
}
|
|
|
|
return "", ErrIPPoolExhausted
|
|
}
|
|
|
|
// GetCurrentIP 获取当前 IP
|
|
func (c *Client) GetCurrentIP(ctx context.Context) (string, error) {
|
|
// 通过 WARP 出口获取 IP
|
|
client := &http.Client{
|
|
Transport: &http.Transport{
|
|
Proxy: http.ProxyURL(nil), // 使用 WARP 接口
|
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
// 使用 WARP 的 SOCKS5 代理
|
|
return net.DialTimeout("tcp", "127.0.0.1:"+strconv.Itoa(c.socksPort), 5*time.Second)
|
|
},
|
|
},
|
|
Timeout: 10 * time.Second,
|
|
}
|
|
|
|
resp, err := client.Get("https://cloudflare.com/cdn-cgi/trace")
|
|
if err != nil {
|
|
// 回退:使用系统默认出口
|
|
return c.getIPFromSystem()
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// 解析 IP
|
|
lines := strings.Split(string(body), "\n")
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, "ip=") {
|
|
return strings.TrimPrefix(line, "ip="), nil
|
|
}
|
|
}
|
|
|
|
return "", errors.New("无法解析 IP")
|
|
}
|
|
|
|
// getIPFromSystem 从系统获取 IP
|
|
func (c *Client) getIPFromSystem() (string, error) {
|
|
resp, err := http.Get("https://cloudflare.com/cdn-cgi/trace")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
lines := strings.Split(string(body), "\n")
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, "ip=") {
|
|
return strings.TrimPrefix(line, "ip="), nil
|
|
}
|
|
}
|
|
|
|
return "", errors.New("无法解析 IP")
|
|
}
|
|
|
|
// GetCountry 获取国家
|
|
func (c *Client) GetCountry(ctx context.Context) (string, error) {
|
|
// 使用 WARP 出口
|
|
resp, err := http.Get("https://cloudflare.com/cdn-cgi/trace")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
body, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
lines := strings.Split(string(body), "\n")
|
|
for _, line := range lines {
|
|
if strings.HasPrefix(line, "loc=") {
|
|
return strings.TrimPrefix(line, "loc="), nil
|
|
}
|
|
}
|
|
|
|
return "??", nil
|
|
}
|
|
|
|
// isInstalled 检查是否安装
|
|
func (c *Client) isInstalled() bool {
|
|
_, err := exec.LookPath("warp-cli")
|
|
return err == nil
|
|
}
|
|
|
|
// GetIPHistory 获取 IP 历史
|
|
func (c *Client) GetIPHistory() []string {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
|
|
result := make([]string, len(c.ipHistory))
|
|
copy(result, c.ipHistory)
|
|
return result
|
|
}
|
|
|
|
// RegisterNewAccount 注册新账号(用于获取新 IP 池)
|
|
func (c *Client) RegisterNewAccount(ctx context.Context) error {
|
|
// 删除当前账号
|
|
cmd := exec.CommandContext(ctx, "warp-cli", "delete")
|
|
_, _ = cmd.CombinedOutput()
|
|
|
|
// 注册新账号
|
|
cmd = exec.CommandContext(ctx, "warp-cli", "register")
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
return fmt.Errorf("注册新账号失败: %w, output: %s", err, string(output))
|
|
}
|
|
|
|
c.logger.Info("注册新 WARP 账号成功")
|
|
return nil
|
|
}
|
|
|
|
// randInt 生成随机整数
|
|
func randInt(min, max int) int {
|
|
return min + int(randFloat()*float64(max-min+1))
|
|
}
|
|
|
|
// randFloat 生成随机浮点数
|
|
func randFloat() float64 {
|
|
return float64(time.Now().UnixNano()%1000) / 1000.0
|
|
}
|
|
|
|
// ParseWARPAccount 从输出解析账号信息
|
|
func ParseWARPAccount(output string) (accountID, deviceID, clientID string) {
|
|
accountRegex := regexp.MustCompile(`Account ID:\s*([a-f0-9-]+)`)
|
|
deviceRegex := regexp.MustCompile(`Device ID:\s*([a-f0-9-]+)`)
|
|
clientRegex := regexp.MustCompile(`Client ID:\s*([a-f0-9-]+)`)
|
|
|
|
if match := accountRegex.FindStringSubmatch(output); len(match) > 1 {
|
|
accountID = match[1]
|
|
}
|
|
if match := deviceRegex.FindStringSubmatch(output); len(match) > 1 {
|
|
deviceID = match[1]
|
|
}
|
|
if match := clientRegex.FindStringSubmatch(output); len(match) > 1 {
|
|
clientID = match[1]
|
|
}
|
|
|
|
return
|
|
} |