diff --git a/backend/pkg/openvpn/manager.go b/backend/pkg/openvpn/manager.go index cd0e7ca..5b218b5 100644 --- a/backend/pkg/openvpn/manager.go +++ b/backend/pkg/openvpn/manager.go @@ -2,6 +2,7 @@ package openvpn import ( "bufio" + "bytes" "crypto/x509" "encoding/pem" "fmt" @@ -616,35 +617,124 @@ var ( reHdr = regexp.MustCompile(`^Updated,([^,]+),`) // status-version 3 CLIENT_LIST 格式(11个字段): // CLIENT_LIST,CommonName,RealAddress,VirtualAddress,VirtualIPv6,BytesRecv,BytesSent,ConnectedSince,Username,ClientID,PeerID - reCli = regexp.MustCompile(`^CLIENT_LIST,([^,]+),([^,]+),([^,]+),([^,]*),(\d+),(\d+),([^,]+),`) + reCli3 = regexp.MustCompile(`^CLIENT_LIST,([^,]+),([^,]+),([^,]+),([^,]*),(\d+),(\d+),([^,]+),`) + // status-version 1 CLIENT 数据行(5 列): + // Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since + // test,1.2.3.4:1234,1024,2048,"Sat Aug 9 16:14:10 2025" + reCli1 = regexp.MustCompile(`^([^,]+),([^,]+),(\d+),(\d+),"?([^",]+)"?,?`) reTime = regexp.MustCompile(`^Connected Since,([^,]+),`) ) +// ParseStatusReader 解析 OpenVPN status 文件。 +// 同时支持 status-version 1(人类可读列名)和 status-version 3(CSV)。 +// +// v1 格式(默认,OpenVPN 未指定 status-version 时使用): +// Common Name,Real Address,Bytes Received,Bytes Sent,Connected Since +// test,1.2.3.4:1234,1024,2048,Sat Aug 9 16:14:10 2025 +// +// v3 格式(status-version 3,11 字段): +// CLIENT_LIST,CN,RealAddr,VPNAddr,IPv6,BytesRecv,BytesSent,ConnectedSince,Username,ClientID,PeerID +// CLIENT_LIST,test,1.2.3.4:1234,10.8.0.2,,1024,2048,Sat Aug 9 16:14:10 2025,UNDEF,0,0 func ParseStatusReader(r io.Reader) ([]StatusEntry, error) { - sc := bufio.NewScanner(r) + // 先全读入内存,方便做两次扫描(v1 需要从 ROUTING_TABLE 取 VPN IP) + b, err := io.ReadAll(r) + if err != nil { + return nil, err + } + sc := bufio.NewScanner(bytes.NewReader(b)) sc.Buffer(make([]byte, 64*1024), 1024*1024) var out []StatusEntry + mode := "" // "" / "v1" / "v3" for sc.Scan() { line := sc.Text() - switch { - case strings.HasPrefix(line, "CLIENT_LIST,"): - m := reCli.FindStringSubmatch(line) + if mode == "" { + if strings.HasPrefix(line, "TITLE,") { + mode = "v3" + } else if strings.HasPrefix(line, "OpenVPN CLIENT LIST") { + mode = "v1" + } else { + continue + } + continue + } + switch mode { + case "v3": + if !strings.HasPrefix(line, "CLIENT_LIST,") { + continue + } + m := reCli3.FindStringSubmatch(line) if m == nil { continue } out = append(out, StatusEntry{ CommonName: m[1], - RealAddress: m[2], // RealAddress (IP:port) - VPNAddress: m[3], // VirtualAddress - BytesRecv: atoi64(m[5]), - BytesSent: atoi64(m[6]), - ConnectedAt: parseConnTime(m[7]), + RealAddress: m[2], + VPNAddress: m[3], + BytesRecv: atoi64(m[5]), + BytesSent: atoi64(m[6]), + ConnectedAt: parseConnTime(m[7]), + }) + case "v1": + // 跳过表头和节标记 + if strings.HasPrefix(line, "Common Name,") || strings.HasPrefix(line, "HEADER,") || + strings.HasPrefix(line, "ROUTING TABLE") || strings.HasPrefix(line, "Virtual Address,") || + strings.HasPrefix(line, "GLOBAL STATS") || strings.HasPrefix(line, "END") || + strings.HasPrefix(line, "Updated,") { + continue + } + m := reCli1.FindStringSubmatch(line) + if m == nil { + continue + } + out = append(out, StatusEntry{ + CommonName: m[1], + RealAddress: m[2], + VPNAddress: "", + BytesRecv: atoi64(m[3]), + BytesSent: atoi64(m[4]), + ConnectedAt: parseConnTime(m[5]), }) } } + // v1 模式:用 ROUTING_TABLE 补充 VPNAddress + if mode == "v1" { + v1VPN := parseV1RoutingTable(bytes.NewReader(b)) + for i := range out { + if ip, ok := v1VPN[out[i].CommonName]; ok { + out[i].VPNAddress = ip + } + } + } return out, sc.Err() } +// parseV1RoutingTable 从 v1 status 中解析 ROUTING_TABLE 部分,返回 CN→VPNIP。 +func parseV1RoutingTable(r io.Reader) map[string]string { + sc := bufio.NewScanner(r) + sc.Buffer(make([]byte, 64*1024), 1024*1024) + out := map[string]string{} + inRouting := false + for sc.Scan() { + line := sc.Text() + if strings.HasPrefix(line, "ROUTING TABLE") { + inRouting = true + continue + } + if !inRouting { + continue + } + if strings.HasPrefix(line, "GLOBAL STATS") || strings.HasPrefix(line, "END") { + break + } + parts := strings.Split(line, ",") + if len(parts) < 2 { + continue + } + out[parts[1]] = parts[0] + } + return out +} + // IsRunning 通过 TCP 探测 openvpn 端口是否可连,仅供参考。 func (m *Manager) IsRunning(host string, port int) bool { addr := fmt.Sprintf("%s:%d", host, port)