Recon network batch 2: fleet relay scan and UDP hints.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
POST /api/v1/recon/relay-scan probes from the dashboard when reachable, otherwise dispatches recon_relay_scan to a same-/24 online agent. Agents reuse probePortsFn for TCP and optionally UDP 53/51820 with dns/wireguard Path Tracer tags.
This commit is contained in:
115
server/internal/api/recon_relay_scan.go
Normal file
115
server/internal/api/recon_relay_scan.go
Normal file
@@ -0,0 +1,115 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"crypto-miner-server/internal/recon"
|
||||
)
|
||||
|
||||
const relayScanTimeout = 30 * time.Second
|
||||
|
||||
func (h *ReconHandler) RelayScan(w http.ResponseWriter, r *http.Request) {
|
||||
var req recon.RelayScanRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
req.Host = strings.TrimSpace(req.Host)
|
||||
if req.Host == "" {
|
||||
http.Error(w, "host required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
host, err := recon.NormalizeHost(req.Host)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
req.Host = host
|
||||
|
||||
if recon.HostReachable(host) {
|
||||
report, err := recon.LocalRelayScan(req)
|
||||
if err != nil {
|
||||
writeJSON(w, map[string]interface{}{"ok": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"ok": true, "report": report.ToScanReport()})
|
||||
return
|
||||
}
|
||||
|
||||
if h == nil || h.wsHub == nil {
|
||||
writeJSON(w, relayScanFallback(host, "", "", "fleet websocket hub unavailable"))
|
||||
return
|
||||
}
|
||||
|
||||
seedID, seedName, ok := pickSpreadSeedAgent(h.wsHub, host, "")
|
||||
if !ok {
|
||||
writeJSON(w, relayScanFallback(host, "", "", "no online fleet agent on nearby /24 to relay scan for "+host))
|
||||
return
|
||||
}
|
||||
|
||||
ch := h.wsHub.AwaitCommandResult(seedID, "recon_relay_scan")
|
||||
args := map[string]interface{}{"command": host}
|
||||
if req.UDPGuess {
|
||||
args["path"] = "true"
|
||||
}
|
||||
if err := h.wsHub.SendAgentCommand(seedID, "recon_relay_scan", args); err != nil {
|
||||
h.wsHub.CancelAwait(seedID, "recon_relay_scan")
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"ok": false, "agent_id": seedID, "agent_name": seedName, "error": err.Error(),
|
||||
"report": relayScanFallbackReport(host, seedName, "relay dispatch failed: "+err.Error()),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case payload := <-ch:
|
||||
success, _ := payload["success"].(bool)
|
||||
msg, _ := payload["message"].(string)
|
||||
if !success {
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"ok": false, "agent_id": seedID, "agent_name": seedName, "error": strings.TrimSpace(msg),
|
||||
"report": relayScanFallbackReport(host, seedName, strings.TrimSpace(msg)),
|
||||
})
|
||||
return
|
||||
}
|
||||
shell := recon.RelayScanReport{
|
||||
Host: host, LocalReachable: false, ScannedVia: "relay",
|
||||
RelayAgentID: seedID, RelayAgentName: seedName,
|
||||
}
|
||||
merged, err := recon.MergeAgentRelayScan(shell, []byte(msg), req.UDPGuess)
|
||||
if err != nil {
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"ok": false, "agent_id": seedID, "agent_name": seedName,
|
||||
"error": "invalid agent relay payload: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"ok": true, "agent_id": seedID, "agent_name": seedName, "report": merged.ToScanReport(),
|
||||
})
|
||||
case <-time.After(relayScanTimeout):
|
||||
h.wsHub.CancelAwait(seedID, "recon_relay_scan")
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"ok": false, "agent_id": seedID, "agent_name": seedName, "error": "relay scan timed out",
|
||||
"report": relayScanFallbackReport(host, seedName, "relay scan timed out after 30s"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func relayScanFallback(host, agentID, agentName, message string) map[string]interface{} {
|
||||
out := map[string]interface{}{"ok": false, "error": message, "report": relayScanFallbackReport(host, agentName, message)}
|
||||
if agentID != "" {
|
||||
out["agent_id"] = agentID
|
||||
}
|
||||
if agentName != "" {
|
||||
out["agent_name"] = agentName
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func relayScanFallbackReport(host, relayVia, message string) *recon.ScanReport {
|
||||
return &recon.ScanReport{Host: host, ScannedAt: time.Now().UTC(), RelayVia: relayVia, Message: message}
|
||||
}
|
||||
123
server/internal/api/recon_relay_scan_test.go
Normal file
123
server/internal/api/recon_relay_scan_test.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"crypto-miner-server/internal/db"
|
||||
"crypto-miner-server/internal/recon"
|
||||
)
|
||||
|
||||
func TestReconRelayScanLocalPath(t *testing.T) {
|
||||
recon.SetHostReachableHook(func(host string) bool { return true })
|
||||
t.Cleanup(func() { recon.SetHostReachableHook(nil) })
|
||||
recon.SetPortDialHook(func(host string, port int, _ time.Duration) bool { return port == 22 })
|
||||
t.Cleanup(func() { recon.SetPortDialHook(nil) })
|
||||
|
||||
h := NewReconHandler(nil, nil)
|
||||
body, _ := json.Marshal(map[string]interface{}{"host": "10.0.0.10"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/recon/relay-scan", bytes.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
h.RelayScan(w, req)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d body=%s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp map[string]interface{}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resp["ok"] != true {
|
||||
t.Fatalf("resp=%v", resp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconRelayScanNoRelayFallback(t *testing.T) {
|
||||
recon.SetHostReachableHook(func(host string) bool { return false })
|
||||
t.Cleanup(func() { recon.SetHostReachableHook(nil) })
|
||||
|
||||
h := NewReconHandler(nil, NewWSHub(nil))
|
||||
body, _ := json.Marshal(map[string]interface{}{"host": "10.99.1.50"})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/recon/relay-scan", bytes.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
h.RelayScan(w, req)
|
||||
var resp map[string]interface{}
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &resp)
|
||||
if resp["ok"] != false || resp["error"] == nil {
|
||||
t.Fatalf("resp=%v", resp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconRelayScanFleetRelay(t *testing.T) {
|
||||
recon.SetHostReachableHook(func(host string) bool { return false })
|
||||
t.Cleanup(func() { recon.SetHostReachableHook(nil) })
|
||||
|
||||
database, err := db.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
|
||||
hub := NewWSHub(database)
|
||||
agentID := "relay-seed"
|
||||
conn := connectTestAgentWithIP(t, hub, agentID, "10.42.1.50")
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
var msg Message
|
||||
if err := conn.ReadJSON(&msg); err != nil {
|
||||
return
|
||||
}
|
||||
if msg.Type != "command" {
|
||||
continue
|
||||
}
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
||||
return
|
||||
}
|
||||
if payload["action"] != "recon_relay_scan" {
|
||||
continue
|
||||
}
|
||||
result := `{"open_ports":[22,445],"udp_hints":[{"port":53,"open":true,"service":"dns"}]}`
|
||||
cmdPayload, _ := json.Marshal(map[string]interface{}{
|
||||
"action": "recon_relay_scan", "success": true, "message": result,
|
||||
})
|
||||
_ = conn.WriteJSON(Message{Type: "command_result", Payload: cmdPayload})
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
h := NewReconHandler(database, hub)
|
||||
body, _ := json.Marshal(map[string]interface{}{"host": "10.42.1.100", "udp_guess": true})
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/recon/relay-scan", bytes.NewReader(body))
|
||||
w := httptest.NewRecorder()
|
||||
h.RelayScan(w, req)
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(6 * time.Second):
|
||||
t.Fatal("agent did not receive relay command")
|
||||
}
|
||||
|
||||
var resp struct {
|
||||
OK bool `json:"ok"`
|
||||
AgentID string `json:"agent_id"`
|
||||
Report map[string]interface{} `json:"report"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !resp.OK || resp.AgentID != agentID {
|
||||
t.Fatalf("resp=%+v", resp)
|
||||
}
|
||||
hints, _ := resp.Report["path_tracer_hints"].([]interface{})
|
||||
if len(hints) != 1 || hints[0] != "dns" {
|
||||
t.Fatalf("report=%v", resp.Report)
|
||||
}
|
||||
}
|
||||
@@ -500,6 +500,8 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
||||
version = serverVersion[0]
|
||||
}
|
||||
|
||||
reconHandler := NewReconHandler(database, wsHub, publicURLOverride)
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
// Middleware (global)
|
||||
@@ -552,8 +554,10 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
||||
vulnHandler := NewVulnHandler()
|
||||
r.Get("/vuln/catalog", vulnHandler.Catalog)
|
||||
|
||||
reconHandler := NewReconHandler(database, wsHub)
|
||||
r.Post("/recon/scan", reconHandler.Scan)
|
||||
r.Post("/recon/relay-scan", reconHandler.RelayScan)
|
||||
subnetDiscoveryHandler := NewSubnetDiscoveryHandler(database, wsHub)
|
||||
r.Get("/recon/discovered-hosts", subnetDiscoveryHandler.GetDiscoveredHosts)
|
||||
|
||||
// Agents
|
||||
r.Get("/agents", h.ListAgents)
|
||||
@@ -820,6 +824,8 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
||||
r.Get("/api/download/agent-mac", serveAgentBinary("mac"))
|
||||
r.Get("/api/download/agent-linux", serveAgentBinary("linux"))
|
||||
|
||||
r.Get("/recon/ping/{scan_id}", reconHandler.CanaryPing)
|
||||
|
||||
// Serve frontend SPA
|
||||
if webRoot != "" {
|
||||
// Check if webroot directory exists
|
||||
|
||||
Reference in New Issue
Block a user