mirror of
https://github.com/JetSprow/J-Board-Lite.git
synced 2026-05-01 01:14:10 +05:30
175 lines
4.0 KiB
Go
175 lines
4.0 KiB
Go
package probe
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"os/exec"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jboard/jboard-agent/internal/config"
|
|
)
|
|
|
|
// Traceroute targets — same as latency targets
|
|
var traceTargets = []struct {
|
|
Carrier string
|
|
IP string
|
|
}{
|
|
{"telecom", "219.141.136.12"},
|
|
{"mobile", "211.136.25.153"},
|
|
{"unicom", "210.22.70.3"},
|
|
}
|
|
|
|
type hopDetail struct {
|
|
Hop int `json:"hop"`
|
|
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 {
|
|
Carrier string `json:"carrier"`
|
|
Hops []hopDetail `json:"hops"`
|
|
Summary string `json:"summary"`
|
|
HopCount int `json:"hopCount"`
|
|
}
|
|
|
|
type tracePayload struct {
|
|
Traces []traceResult `json:"traces"`
|
|
}
|
|
|
|
// nexttrace JSON output structures
|
|
type ntHop struct {
|
|
Success bool `json:"Success"`
|
|
Address *struct {
|
|
IP string `json:"IP"`
|
|
} `json:"Address"`
|
|
Geo *struct {
|
|
Asnumber string `json:"asnumber"`
|
|
Country string `json:"country"`
|
|
Prov string `json:"prov"`
|
|
City string `json:"city"`
|
|
Owner string `json:"owner"`
|
|
Isp string `json:"isp"`
|
|
} `json:"Geo"`
|
|
TTL int `json:"TTL"`
|
|
RTT int64 `json:"RTT"` // nanoseconds
|
|
}
|
|
|
|
type ntOutput struct {
|
|
Hops [][]ntHop `json:"Hops"`
|
|
}
|
|
|
|
// TraceLoop periodically runs traceroute to three carriers and pushes to J-Board.
|
|
func TraceLoop(ctx context.Context, cfg *config.Config) {
|
|
ticker := time.NewTicker(cfg.TraceInterval)
|
|
defer ticker.Stop()
|
|
|
|
// Run immediately
|
|
traceAndPush(cfg)
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
traceAndPush(cfg)
|
|
}
|
|
}
|
|
}
|
|
|
|
func traceAndPush(cfg *config.Config) {
|
|
log.Println("[trace] starting trace cycle")
|
|
|
|
var results []traceResult
|
|
for _, target := range traceTargets {
|
|
hops, summary, err := runTrace(target.IP)
|
|
if err != nil {
|
|
log.Printf("[trace] %s (%s): %v", target.Carrier, target.IP, err)
|
|
continue
|
|
}
|
|
results = append(results, traceResult{
|
|
Carrier: target.Carrier,
|
|
Hops: hops,
|
|
Summary: summary,
|
|
HopCount: len(hops),
|
|
})
|
|
log.Printf("[trace] %s: %s (%d hops)", target.Carrier, summary, len(hops))
|
|
}
|
|
|
|
if len(results) == 0 {
|
|
log.Println("[trace] no results, skipping upload")
|
|
return
|
|
}
|
|
|
|
payload := tracePayload{Traces: results}
|
|
body, _ := json.Marshal(payload)
|
|
|
|
if err := postToServer(cfg, "/api/agent/trace", body); err != nil {
|
|
log.Printf("[trace] push error: %v — retrying in 10s", err)
|
|
time.Sleep(10 * time.Second)
|
|
if err := postToServer(cfg, "/api/agent/trace", body); err != nil {
|
|
log.Printf("[trace] retry failed: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func runTrace(ip string) ([]hopDetail, string, error) {
|
|
cmd := exec.Command("nexttrace", "-j", "--no-color", "-n", ip)
|
|
out, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("nexttrace failed for %s: %w", ip, err)
|
|
}
|
|
|
|
var parsed ntOutput
|
|
if err := json.Unmarshal(out, &parsed); err != nil {
|
|
return nil, "", fmt.Errorf("parse nexttrace output for %s: %w", ip, err)
|
|
}
|
|
|
|
var hops []hopDetail
|
|
for i, hopGroup := range parsed.Hops {
|
|
hop := hopDetail{Hop: i + 1}
|
|
for _, probe := range hopGroup {
|
|
if probe.Success && probe.Address != nil && probe.Address.IP != "" {
|
|
hop.IP = probe.Address.IP
|
|
hop.Latency = float64(probe.RTT) / 1e6
|
|
if probe.Geo != nil {
|
|
var parts []string
|
|
if probe.Geo.Country != "" {
|
|
parts = append(parts, probe.Geo.Country)
|
|
}
|
|
if probe.Geo.Prov != "" {
|
|
parts = append(parts, probe.Geo.Prov)
|
|
}
|
|
if probe.Geo.City != "" {
|
|
parts = append(parts, probe.Geo.City)
|
|
}
|
|
if probe.Geo.Owner != "" {
|
|
parts = append(parts, probe.Geo.Owner)
|
|
}
|
|
hop.Geo = strings.Join(parts, " ")
|
|
hop.ASN = probe.Geo.Asnumber
|
|
hop.Owner = probe.Geo.Owner
|
|
hop.ISP = probe.Geo.Isp
|
|
}
|
|
break
|
|
}
|
|
}
|
|
hops = append(hops, hop)
|
|
}
|
|
|
|
// Hide the first hop (server gateway IP) for security
|
|
if len(hops) > 0 {
|
|
hops[0].IP = "*"
|
|
hops[0].Geo = ""
|
|
}
|
|
|
|
summary := detectSummary(hops)
|
|
return hops, summary, nil
|
|
}
|