mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
feat: release v3.0.0 risk telemetry
This commit is contained in:
124
agent/jboard-agent/internal/probe/route_classify.go
Normal file
124
agent/jboard-agent/internal/probe/route_classify.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package probe
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var asnPattern = regexp.MustCompile(`(?i)(?:^|\b)AS?\s*(\d{2,10})(?:\b|$)`)
|
||||
|
||||
func normalizeRouteText(value string) string {
|
||||
return strings.ToUpper(strings.TrimSpace(value))
|
||||
}
|
||||
|
||||
func normalizeASN(value string) string {
|
||||
text := normalizeRouteText(value)
|
||||
if text == "" {
|
||||
return ""
|
||||
}
|
||||
match := asnPattern.FindStringSubmatch(text)
|
||||
if len(match) > 1 {
|
||||
return match[1]
|
||||
}
|
||||
for _, r := range text {
|
||||
if r < '0' || r > '9' {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func hasASN(asns map[string]struct{}, values ...string) bool {
|
||||
for _, value := range values {
|
||||
if _, ok := asns[value]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasText(combined string, values ...string) bool {
|
||||
for _, value := range values {
|
||||
if strings.Contains(combined, value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func hasIPPrefix(ips []string, prefixes ...string) bool {
|
||||
for _, ip := range ips {
|
||||
for _, prefix := range prefixes {
|
||||
if strings.HasPrefix(ip, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func detectSummary(hops []hopDetail) string {
|
||||
var texts []string
|
||||
var ips []string
|
||||
asns := make(map[string]struct{})
|
||||
|
||||
for _, hop := range hops {
|
||||
if hop.IP != "" && hop.IP != "*" {
|
||||
ips = append(ips, hop.IP)
|
||||
}
|
||||
parts := []string{hop.Geo, hop.ASN, hop.Owner, hop.ISP}
|
||||
text := normalizeRouteText(strings.Join(parts, " "))
|
||||
texts = append(texts, text)
|
||||
|
||||
if asn := normalizeASN(hop.ASN); asn != "" {
|
||||
asns[asn] = struct{}{}
|
||||
}
|
||||
for _, match := range asnPattern.FindAllStringSubmatch(text, -1) {
|
||||
if len(match) > 1 {
|
||||
asns[match[1]] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
combined := strings.Join(texts, " ")
|
||||
cn2Evidence := hasASN(asns, "4809") ||
|
||||
hasIPPrefix(ips, "59.43.") ||
|
||||
hasText(combined, "CN2", "CTGNET", "CHINANET NEXT CARRYING NETWORK", "CHINA TELECOM GLOBAL")
|
||||
cn2GIAText := hasText(combined, "CN2 GIA", "CN2GIA", "GIA", "GLOBAL INTERNET ACCESS")
|
||||
ordinaryTelecomHops := 0
|
||||
for _, text := range texts {
|
||||
if strings.Contains(text, "AS4134") ||
|
||||
strings.Contains(text, "CHINANET BACKBONE") ||
|
||||
strings.Contains(text, "CHINANET 163") ||
|
||||
strings.Contains(text, "163骨干") {
|
||||
ordinaryTelecomHops++
|
||||
}
|
||||
}
|
||||
for _, ip := range ips {
|
||||
if strings.HasPrefix(ip, "202.97.") {
|
||||
ordinaryTelecomHops++
|
||||
}
|
||||
}
|
||||
|
||||
if cn2Evidence {
|
||||
if cn2GIAText || ordinaryTelecomHops <= 1 {
|
||||
return "CN2 GIA"
|
||||
}
|
||||
return "CN2 GT"
|
||||
}
|
||||
|
||||
if hasASN(asns, "9929", "10099") || hasText(combined, "CUII", "A网", "AS9929") {
|
||||
return "AS9929"
|
||||
}
|
||||
if hasText(combined, "CMIN2") || hasASN(asns, "58807", "58809", "58813", "58819", "59807") {
|
||||
return "CMIN2"
|
||||
}
|
||||
if hasText(combined, "CMI") || hasASN(asns, "58453") {
|
||||
return "CMI"
|
||||
}
|
||||
if hasASN(asns, "4837") || hasText(combined, "AS4837") {
|
||||
return "AS4837"
|
||||
}
|
||||
|
||||
return "普通线路"
|
||||
}
|
||||
35
agent/jboard-agent/internal/probe/route_classify_test.go
Normal file
35
agent/jboard-agent/internal/probe/route_classify_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package probe
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDetectSummaryCN2GIAFromAS4809And59_43(t *testing.T) {
|
||||
hops := []hopDetail{
|
||||
{Hop: 1, IP: "*"},
|
||||
{Hop: 2, IP: "59.43.246.237", ASN: "AS4809", Geo: "中国 上海 China Telecom CN2"},
|
||||
{Hop: 3, IP: "219.141.136.12", ASN: "AS4134", Geo: "中国 北京 电信"},
|
||||
}
|
||||
if got := detectSummary(hops); got != "CN2 GIA" {
|
||||
t.Fatalf("detectSummary() = %q, want CN2 GIA", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectSummaryCN2GTWhenCN2FallsBackTo163(t *testing.T) {
|
||||
hops := []hopDetail{
|
||||
{Hop: 1, IP: "59.43.248.1", ASN: "AS4809", Geo: "CN2"},
|
||||
{Hop: 2, IP: "202.97.12.1", ASN: "AS4134", Geo: "CHINANET BACKBONE"},
|
||||
{Hop: 3, IP: "202.97.18.1", ASN: "AS4134", Geo: "CHINANET BACKBONE"},
|
||||
}
|
||||
if got := detectSummary(hops); got != "CN2 GT" {
|
||||
t.Fatalf("detectSummary() = %q, want CN2 GT", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDetectSummaryCMIN2BeforeCMI(t *testing.T) {
|
||||
hops := []hopDetail{
|
||||
{Hop: 2, IP: "223.120.20.1", ASN: "AS58807", Geo: "CMIN2 China Mobile"},
|
||||
{Hop: 3, IP: "211.136.25.153", ASN: "AS9808", Geo: "CMI Mobile"},
|
||||
}
|
||||
if got := detectSummary(hops); got != "CMIN2" {
|
||||
t.Fatalf("detectSummary() = %q, want CMIN2", got)
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,9 @@ type hopDetail struct {
|
||||
IP string `json:"ip"`
|
||||
Geo string `json:"geo"`
|
||||
Latency float64 `json:"latency"`
|
||||
ASN string `json:"asn,omitempty"`
|
||||
Owner string `json:"owner,omitempty"`
|
||||
ISP string `json:"isp,omitempty"`
|
||||
}
|
||||
|
||||
type traceResult struct {
|
||||
@@ -129,7 +132,6 @@ func runTrace(ip string) ([]hopDetail, string, error) {
|
||||
}
|
||||
|
||||
var hops []hopDetail
|
||||
var asnumbers []string
|
||||
for i, hopGroup := range parsed.Hops {
|
||||
hop := hopDetail{Hop: i + 1}
|
||||
for _, probe := range hopGroup {
|
||||
@@ -151,9 +153,9 @@ func runTrace(ip string) ([]hopDetail, string, error) {
|
||||
parts = append(parts, probe.Geo.Owner)
|
||||
}
|
||||
hop.Geo = strings.Join(parts, " ")
|
||||
if probe.Geo.Asnumber != "" {
|
||||
asnumbers = append(asnumbers, probe.Geo.Asnumber)
|
||||
}
|
||||
hop.ASN = probe.Geo.Asnumber
|
||||
hop.Owner = probe.Geo.Owner
|
||||
hop.ISP = probe.Geo.Isp
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -167,34 +169,6 @@ func runTrace(ip string) ([]hopDetail, string, error) {
|
||||
hops[0].Geo = ""
|
||||
}
|
||||
|
||||
summary := detectSummary(hops, asnumbers)
|
||||
summary := detectSummary(hops)
|
||||
return hops, summary, nil
|
||||
}
|
||||
|
||||
func detectSummary(hops []hopDetail, asnumbers []string) string {
|
||||
combined := ""
|
||||
for _, h := range hops {
|
||||
combined += " " + strings.ToUpper(h.Geo)
|
||||
}
|
||||
asSet := ""
|
||||
for _, asn := range asnumbers {
|
||||
asSet += " " + asn
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.Contains(combined, "CN2") && strings.Contains(combined, "GIA"):
|
||||
return "CN2 GIA"
|
||||
case strings.Contains(combined, "CN2"):
|
||||
return "CN2 GT"
|
||||
case strings.Contains(asSet, "9929") || strings.Contains(combined, "CUII") || strings.Contains(combined, "A网"):
|
||||
return "AS9929"
|
||||
case strings.Contains(asSet, "4837"):
|
||||
return "AS4837"
|
||||
case strings.Contains(combined, "CMI") || strings.Contains(asSet, "58453"):
|
||||
return "CMI"
|
||||
case strings.Contains(combined, "CMIN2") || strings.Contains(asSet, "59807"):
|
||||
return "CMIN2"
|
||||
default:
|
||||
return "普通线路"
|
||||
}
|
||||
}
|
||||
|
||||
345
agent/jboard-agent/internal/probe/xraylog.go
Normal file
345
agent/jboard-agent/internal/probe/xraylog.go
Normal file
@@ -0,0 +1,345 @@
|
||||
package probe
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/jboard/jboard-agent/internal/config"
|
||||
)
|
||||
|
||||
const maxXrayReadBytes int64 = 2 * 1024 * 1024
|
||||
const maxXrayEventsPerPush = 300
|
||||
|
||||
var xrayAccessLinePattern = regexp.MustCompile(`^(\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2})\s+(\S+)\s+(accepted|rejected)\s+(?:(tcp|udp):)?(\S+)\s+\[([^\]]+)\](?:.*?\bemail:\s*([^\s]+))?`)
|
||||
|
||||
type xrayLogState struct {
|
||||
Path string `json:"path"`
|
||||
Inode uint64 `json:"inode"`
|
||||
Offset int64 `json:"offset"`
|
||||
}
|
||||
|
||||
type nodeAccessEvent struct {
|
||||
ClientEmail string `json:"clientEmail"`
|
||||
SourceIP string `json:"sourceIp"`
|
||||
InboundTag string `json:"inboundTag,omitempty"`
|
||||
Network string `json:"network,omitempty"`
|
||||
TargetHost string `json:"targetHost,omitempty"`
|
||||
TargetPort int `json:"targetPort,omitempty"`
|
||||
Action string `json:"action"`
|
||||
ConnectionCount int `json:"connectionCount"`
|
||||
UniqueTargetCount int `json:"uniqueTargetCount,omitempty"`
|
||||
FirstSeenAt string `json:"firstSeenAt,omitempty"`
|
||||
LastSeenAt string `json:"lastSeenAt,omitempty"`
|
||||
}
|
||||
|
||||
type nodeAccessPayload struct {
|
||||
Events []nodeAccessEvent `json:"events"`
|
||||
}
|
||||
|
||||
type parsedXrayAccess struct {
|
||||
ClientEmail string
|
||||
SourceIP string
|
||||
InboundTag string
|
||||
Network string
|
||||
TargetHost string
|
||||
TargetPort int
|
||||
Action string
|
||||
SeenAt time.Time
|
||||
}
|
||||
|
||||
type accessAggregate struct {
|
||||
event nodeAccessEvent
|
||||
targets map[string]struct{}
|
||||
}
|
||||
|
||||
func XrayAccessLogLoop(ctx context.Context, cfg *config.Config) {
|
||||
if strings.TrimSpace(cfg.XrayAccessLogPath) == "" {
|
||||
log.Println("[xray-log] disabled; set XRAY_ACCESS_LOG_PATH to enable node access risk telemetry")
|
||||
return
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(cfg.XrayLogInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
collectAndPushXrayLogs(cfg)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
collectAndPushXrayLogs(cfg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func collectAndPushXrayLogs(cfg *config.Config) {
|
||||
events, state, err := readNewXrayEvents(cfg)
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
log.Printf("[xray-log] read error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if len(events) == 0 {
|
||||
if err := saveXrayLogState(cfg.XrayLogStateFile, state); err != nil {
|
||||
log.Printf("[xray-log] state save error: %v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
payload := nodeAccessPayload{Events: events}
|
||||
body, _ := json.Marshal(payload)
|
||||
if err := postToServer(cfg, "/api/agent/node-access", body); err != nil {
|
||||
log.Printf("[xray-log] push error: %v", err)
|
||||
return
|
||||
}
|
||||
if err := saveXrayLogState(cfg.XrayLogStateFile, state); err != nil {
|
||||
log.Printf("[xray-log] state save error: %v", err)
|
||||
}
|
||||
log.Printf("[xray-log] pushed %d aggregate access events", len(events))
|
||||
}
|
||||
|
||||
func readNewXrayEvents(cfg *config.Config) ([]nodeAccessEvent, xrayLogState, error) {
|
||||
path := strings.TrimSpace(cfg.XrayAccessLogPath)
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return nil, xrayLogState{}, err
|
||||
}
|
||||
inode := fileInode(info)
|
||||
state := loadXrayLogState(cfg.XrayLogStateFile)
|
||||
|
||||
if state.Path != path || state.Inode != inode || state.Offset > info.Size() {
|
||||
state = xrayLogState{Path: path, Inode: inode}
|
||||
if cfg.XrayLogStartAtEnd {
|
||||
state.Offset = info.Size()
|
||||
}
|
||||
}
|
||||
|
||||
if info.Size() <= state.Offset {
|
||||
return nil, state, nil
|
||||
}
|
||||
|
||||
readBytes := info.Size() - state.Offset
|
||||
if readBytes > maxXrayReadBytes {
|
||||
readBytes = maxXrayReadBytes
|
||||
}
|
||||
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, state, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
buf := make([]byte, readBytes)
|
||||
n, err := file.ReadAt(buf, state.Offset)
|
||||
if err != nil && n == 0 {
|
||||
return nil, state, err
|
||||
}
|
||||
data := string(buf[:n])
|
||||
consumed := int64(n)
|
||||
if lastNewline := strings.LastIndexByte(data, '\n'); lastNewline >= 0 && lastNewline < len(data)-1 {
|
||||
data = data[:lastNewline+1]
|
||||
consumed = int64(len(data))
|
||||
}
|
||||
state.Offset += consumed
|
||||
|
||||
events := aggregateXrayAccessLines(strings.Split(data, "\n"))
|
||||
if len(events) > maxXrayEventsPerPush {
|
||||
events = events[:maxXrayEventsPerPush]
|
||||
}
|
||||
|
||||
return events, state, nil
|
||||
}
|
||||
|
||||
func loadXrayLogState(path string) xrayLogState {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return xrayLogState{}
|
||||
}
|
||||
var state xrayLogState
|
||||
if err := json.Unmarshal(data, &state); err != nil {
|
||||
return xrayLogState{}
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
func saveXrayLogState(path string, state xrayLogState) error {
|
||||
if strings.TrimSpace(path) == "" {
|
||||
return nil
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
data, _ := json.Marshal(state)
|
||||
return os.WriteFile(path, data, 0o600)
|
||||
}
|
||||
|
||||
func fileInode(info os.FileInfo) uint64 {
|
||||
stat, ok := info.Sys().(*syscall.Stat_t)
|
||||
if !ok || stat == nil {
|
||||
return 0
|
||||
}
|
||||
return uint64(stat.Ino)
|
||||
}
|
||||
|
||||
func aggregateXrayAccessLines(lines []string) []nodeAccessEvent {
|
||||
aggregates := make(map[string]*accessAggregate)
|
||||
var order []string
|
||||
|
||||
for _, line := range lines {
|
||||
parsed, ok := parseXrayAccessLine(line)
|
||||
if !ok || parsed.ClientEmail == "" || parsed.SourceIP == "" {
|
||||
continue
|
||||
}
|
||||
key := strings.Join([]string{
|
||||
parsed.ClientEmail,
|
||||
parsed.SourceIP,
|
||||
parsed.InboundTag,
|
||||
parsed.Network,
|
||||
parsed.Action,
|
||||
}, "|")
|
||||
|
||||
agg, ok := aggregates[key]
|
||||
if !ok {
|
||||
agg = &accessAggregate{
|
||||
event: nodeAccessEvent{
|
||||
ClientEmail: parsed.ClientEmail,
|
||||
SourceIP: parsed.SourceIP,
|
||||
InboundTag: parsed.InboundTag,
|
||||
Network: parsed.Network,
|
||||
TargetHost: parsed.TargetHost,
|
||||
TargetPort: parsed.TargetPort,
|
||||
Action: parsed.Action,
|
||||
ConnectionCount: 0,
|
||||
FirstSeenAt: parsed.SeenAt.Format(time.RFC3339),
|
||||
LastSeenAt: parsed.SeenAt.Format(time.RFC3339),
|
||||
},
|
||||
targets: make(map[string]struct{}),
|
||||
}
|
||||
aggregates[key] = agg
|
||||
order = append(order, key)
|
||||
}
|
||||
|
||||
agg.event.ConnectionCount++
|
||||
if parsed.TargetHost != "" {
|
||||
agg.targets[parsed.TargetHost] = struct{}{}
|
||||
}
|
||||
if parsed.SeenAt.After(parseRFC3339OrZero(agg.event.LastSeenAt)) {
|
||||
agg.event.LastSeenAt = parsed.SeenAt.Format(time.RFC3339)
|
||||
}
|
||||
}
|
||||
|
||||
events := make([]nodeAccessEvent, 0, len(order))
|
||||
for _, key := range order {
|
||||
agg := aggregates[key]
|
||||
agg.event.UniqueTargetCount = len(agg.targets)
|
||||
events = append(events, agg.event)
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
func parseXrayAccessLine(line string) (parsedXrayAccess, bool) {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
return parsedXrayAccess{}, false
|
||||
}
|
||||
match := xrayAccessLinePattern.FindStringSubmatch(line)
|
||||
if len(match) == 0 {
|
||||
return parsedXrayAccess{}, false
|
||||
}
|
||||
|
||||
seenAt, err := time.ParseInLocation("2006/01/02 15:04:05", match[1], time.Local)
|
||||
if err != nil {
|
||||
seenAt = time.Now()
|
||||
}
|
||||
network := strings.ToLower(match[4])
|
||||
targetHost, targetPort := splitTarget(match[5])
|
||||
if network == "" {
|
||||
network = inferNetwork(match[5])
|
||||
}
|
||||
|
||||
return parsedXrayAccess{
|
||||
ClientEmail: strings.TrimSpace(match[7]),
|
||||
SourceIP: stripPort(match[2]),
|
||||
InboundTag: normalizeInboundTag(match[6]),
|
||||
Network: network,
|
||||
TargetHost: targetHost,
|
||||
TargetPort: targetPort,
|
||||
Action: strings.ToLower(match[3]),
|
||||
SeenAt: seenAt,
|
||||
}, true
|
||||
}
|
||||
|
||||
func normalizeInboundTag(value string) string {
|
||||
parts := strings.Split(value, ">>")
|
||||
return strings.TrimSpace(parts[0])
|
||||
}
|
||||
|
||||
func inferNetwork(target string) string {
|
||||
if strings.HasPrefix(strings.ToLower(target), "udp:") {
|
||||
return "udp"
|
||||
}
|
||||
return "tcp"
|
||||
}
|
||||
|
||||
func splitTarget(value string) (string, int) {
|
||||
value = trimTransportPrefix(strings.TrimSpace(value))
|
||||
if host, port, err := net.SplitHostPort(value); err == nil {
|
||||
return strings.Trim(host, "[]"), atoiOrZero(port)
|
||||
}
|
||||
idx := strings.LastIndex(value, ":")
|
||||
if idx > 0 && idx < len(value)-1 {
|
||||
port := atoiOrZero(value[idx+1:])
|
||||
if port > 0 {
|
||||
return strings.Trim(value[:idx], "[]"), port
|
||||
}
|
||||
}
|
||||
return strings.Trim(value, "[]"), 0
|
||||
}
|
||||
|
||||
func stripPort(value string) string {
|
||||
value = trimTransportPrefix(strings.TrimSpace(value))
|
||||
if host, _, err := net.SplitHostPort(value); err == nil {
|
||||
return strings.Trim(host, "[]")
|
||||
}
|
||||
idx := strings.LastIndex(value, ":")
|
||||
if idx > 0 && idx < len(value)-1 && atoiOrZero(value[idx+1:]) > 0 {
|
||||
return strings.Trim(value[:idx], "[]")
|
||||
}
|
||||
return strings.Trim(value, "[]")
|
||||
}
|
||||
|
||||
func trimTransportPrefix(value string) string {
|
||||
lower := strings.ToLower(value)
|
||||
if strings.HasPrefix(lower, "tcp:") || strings.HasPrefix(lower, "udp:") {
|
||||
return value[4:]
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func atoiOrZero(value string) int {
|
||||
n, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func parseRFC3339OrZero(value string) time.Time {
|
||||
parsed, err := time.Parse(time.RFC3339, value)
|
||||
if err != nil {
|
||||
return time.Time{}
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
63
agent/jboard-agent/internal/probe/xraylog_test.go
Normal file
63
agent/jboard-agent/internal/probe/xraylog_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package probe
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParseXrayAccessLine(t *testing.T) {
|
||||
line := "2026/04/29 10:11:12 203.0.113.9:51820 accepted tcp:example.com:443 [proxy-in >> freedom] email: user@example.com-cabc1234"
|
||||
got, ok := parseXrayAccessLine(line)
|
||||
if !ok {
|
||||
t.Fatal("parseXrayAccessLine() failed")
|
||||
}
|
||||
if got.SourceIP != "203.0.113.9" {
|
||||
t.Fatalf("SourceIP = %q", got.SourceIP)
|
||||
}
|
||||
if got.ClientEmail != "user@example.com-cabc1234" {
|
||||
t.Fatalf("ClientEmail = %q", got.ClientEmail)
|
||||
}
|
||||
if got.InboundTag != "proxy-in" {
|
||||
t.Fatalf("InboundTag = %q", got.InboundTag)
|
||||
}
|
||||
if got.Network != "tcp" || got.TargetHost != "example.com" || got.TargetPort != 443 {
|
||||
t.Fatalf("target = %s %s:%d", got.Network, got.TargetHost, got.TargetPort)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseXrayAccessLineWithTransportPrefixedSource(t *testing.T) {
|
||||
line := "2026/04/29 10:11:12 tcp:203.0.113.9:51820 accepted tcp:example.com:443 [proxy] email: user@example.com-cabc1234"
|
||||
got, ok := parseXrayAccessLine(line)
|
||||
if !ok {
|
||||
t.Fatal("parseXrayAccessLine() failed")
|
||||
}
|
||||
if got.SourceIP != "203.0.113.9" {
|
||||
t.Fatalf("SourceIP = %q", got.SourceIP)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseXrayAccessLineWithIPv6Source(t *testing.T) {
|
||||
line := "2026/04/29 10:11:12 tcp:[2001:db8::1]:51820 accepted tcp:example.com:443 [proxy] email: user@example.com-cabc1234"
|
||||
got, ok := parseXrayAccessLine(line)
|
||||
if !ok {
|
||||
t.Fatal("parseXrayAccessLine() failed")
|
||||
}
|
||||
if got.SourceIP != "2001:db8::1" {
|
||||
t.Fatalf("SourceIP = %q", got.SourceIP)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAggregateXrayAccessLines(t *testing.T) {
|
||||
lines := []string{
|
||||
"2026/04/29 10:11:12 203.0.113.9:51820 accepted tcp:example.com:443 [proxy] email: user@example.com-cabc1234",
|
||||
"2026/04/29 10:11:13 203.0.113.9:51821 accepted tcp:openai.com:443 [proxy] email: user@example.com-cabc1234",
|
||||
"2026/04/29 10:11:14 198.51.100.2:51821 accepted udp:1.1.1.1:53 [proxy] email: user@example.com-cabc1234",
|
||||
}
|
||||
events := aggregateXrayAccessLines(lines)
|
||||
if len(events) != 2 {
|
||||
t.Fatalf("len(events) = %d, want 2", len(events))
|
||||
}
|
||||
if events[0].ConnectionCount != 2 || events[0].UniqueTargetCount != 2 {
|
||||
t.Fatalf("first aggregate = %+v", events[0])
|
||||
}
|
||||
if events[1].Network != "udp" || events[1].TargetPort != 53 {
|
||||
t.Fatalf("second aggregate = %+v", events[1])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user