Add recon network batch 1: stack banners and smart port profiles.
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
Parse technology stack from crawl headers, grab SSH/HTTP/WinRM banners, merge smart port bundles with FleetPorts, and suggest deploy-kit lane plus SSM for EC2 metadata targets.
This commit is contained in:
789
scripts/commit-recon-batch2.py
Normal file
789
scripts/commit-recon-batch2.py
Normal file
@@ -0,0 +1,789 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Write recon batch 2 files, test, commit, and push in one shot."""
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
FILES = {}
|
||||
|
||||
FILES["server/internal/recon/relay_scan.go"] = r'''package recon
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var RelayScanUDPPorts = []int{53, 51820}
|
||||
|
||||
type RelayScanRequest struct {
|
||||
Host string `json:"host"`
|
||||
UDPGuess bool `json:"udp_guess,omitempty"`
|
||||
}
|
||||
|
||||
type UDPHint struct {
|
||||
Port int `json:"port"`
|
||||
Open bool `json:"open"`
|
||||
Service string `json:"service"`
|
||||
}
|
||||
|
||||
type RelayScanReport struct {
|
||||
Host string `json:"host"`
|
||||
ScannedAt time.Time `json:"scanned_at"`
|
||||
LocalReachable bool `json:"local_reachable"`
|
||||
ScannedVia string `json:"scanned_via"`
|
||||
RelayAgentID string `json:"relay_agent_id,omitempty"`
|
||||
RelayAgentName string `json:"relay_agent_name,omitempty"`
|
||||
Ports []PortResult `json:"ports"`
|
||||
UDPHints []UDPHint `json:"udp_hints,omitempty"`
|
||||
PathTracerHints []string `json:"path_tracer_hints,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Recommendations []DeployRecommendation `json:"recommendations,omitempty"`
|
||||
}
|
||||
|
||||
var reachabilityPorts = []int{80, 443, 22, 445, 3389}
|
||||
var hostReachableFn func(host string) bool
|
||||
var udpGuessFn func(host string, ports []int) []UDPHint
|
||||
|
||||
func SetHostReachableHook(fn func(host string) bool) { hostReachableFn = fn }
|
||||
func SetUDPGuesssHook(fn func(host string, ports []int) []UDPHint) { udpGuessFn = fn }
|
||||
|
||||
func HostReachable(host string) bool {
|
||||
if hostReachableFn != nil {
|
||||
return hostReachableFn(host)
|
||||
}
|
||||
host = strings.TrimSpace(host)
|
||||
for _, port := range reachabilityPorts {
|
||||
conn, err := net.DialTimeout("tcp", net.JoinHostPort(host, strconv.Itoa(port)), 1200*time.Millisecond)
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
return true
|
||||
}
|
||||
if isConnRefused(err) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GuessUDPHints(host string, enabled bool) []UDPHint {
|
||||
if !enabled {
|
||||
return nil
|
||||
}
|
||||
if udpGuessFn != nil {
|
||||
return udpGuessFn(host, RelayScanUDPPorts)
|
||||
}
|
||||
var out []UDPHint
|
||||
for _, port := range RelayScanUDPPorts {
|
||||
out = append(out, UDPHint{Port: port, Open: probeUDPQuick(host, port), Service: udpServiceLabel(port)})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func PathTracerHintsFromUDP(hints []UDPHint) []string {
|
||||
var out []string
|
||||
seen := map[string]bool{}
|
||||
for _, h := range hints {
|
||||
if h.Open && h.Service != "" && !seen[h.Service] {
|
||||
seen[h.Service] = true
|
||||
out = append(out, h.Service)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func LocalRelayScan(req RelayScanRequest) (*RelayScanReport, error) {
|
||||
host, err := NormalizeHost(req.Host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ports := ScanPorts(host)
|
||||
udp := GuessUDPHints(host, req.UDPGuess)
|
||||
return &RelayScanReport{
|
||||
Host: host, ScannedAt: time.Now().UTC(), LocalReachable: true, ScannedVia: "server",
|
||||
Ports: ports, UDPHints: udp, PathTracerHints: PathTracerHintsFromUDP(udp),
|
||||
Recommendations: BuildRecommendations(ports, nil),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func MergeAgentRelayScan(shell RelayScanReport, agentPayload []byte, udpGuess bool) (*RelayScanReport, error) {
|
||||
var raw struct {
|
||||
OpenPorts []int `json:"open_ports"`
|
||||
UDPHints []UDPHint `json:"udp_hints"`
|
||||
}
|
||||
if err := json.Unmarshal(agentPayload, &raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
openSet := map[int]bool{}
|
||||
for _, p := range raw.OpenPorts {
|
||||
openSet[p] = true
|
||||
}
|
||||
ports := make([]PortResult, 0, len(FleetPorts))
|
||||
for _, p := range FleetPorts {
|
||||
ports = append(ports, PortResult{Port: p, Open: openSet[p]})
|
||||
}
|
||||
udp := raw.UDPHints
|
||||
if udpGuess && len(udp) == 0 {
|
||||
udp = GuessUDPHints(shell.Host, true)
|
||||
}
|
||||
shell.Ports = ports
|
||||
shell.UDPHints = udp
|
||||
shell.PathTracerHints = PathTracerHintsFromUDP(udp)
|
||||
shell.Recommendations = BuildRecommendations(ports, nil)
|
||||
shell.ScannedAt = time.Now().UTC()
|
||||
return &shell, nil
|
||||
}
|
||||
|
||||
func (r *RelayScanReport) ToScanReport() *ScanReport {
|
||||
if r == nil {
|
||||
return nil
|
||||
}
|
||||
relayVia := ""
|
||||
if r.ScannedVia == "relay" {
|
||||
if r.RelayAgentName != "" {
|
||||
relayVia = r.RelayAgentName
|
||||
} else if r.RelayAgentID != "" {
|
||||
relayVia = r.RelayAgentID
|
||||
}
|
||||
}
|
||||
return &ScanReport{
|
||||
Host: r.Host, ScannedAt: r.ScannedAt, Ports: r.Ports, RelayVia: relayVia,
|
||||
UDPHints: r.UDPHints, PathTracerHints: r.PathTracerHints, Message: r.Message,
|
||||
Recommendations: r.Recommendations,
|
||||
}
|
||||
}
|
||||
|
||||
func udpServiceLabel(port int) string {
|
||||
switch port {
|
||||
case 53:
|
||||
return "dns"
|
||||
case 51820:
|
||||
return "wireguard"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func isConnRefused(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
var opErr *net.OpError
|
||||
if errors.As(err, &opErr) {
|
||||
return strings.Contains(strings.ToLower(opErr.Err.Error()), "refused")
|
||||
}
|
||||
return strings.Contains(strings.ToLower(err.Error()), "refused")
|
||||
}
|
||||
|
||||
func probeUDPQuick(host string, port int) bool {
|
||||
conn, err := net.DialTimeout("udp", net.JoinHostPort(host, strconv.Itoa(port)), 800*time.Millisecond)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer conn.Close()
|
||||
_ = conn.SetDeadline(time.Now().Add(800 * time.Millisecond))
|
||||
if b := udpProbePayload(port); len(b) > 0 {
|
||||
_, _ = conn.Write(b)
|
||||
}
|
||||
buf := make([]byte, 512)
|
||||
n, err := conn.Read(buf)
|
||||
return err == nil && n > 0
|
||||
}
|
||||
|
||||
func udpProbePayload(port int) []byte {
|
||||
switch port {
|
||||
case 53:
|
||||
return []byte{0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x07, 'v', 'e', 'r', 's', 'i', 'o', 'n', 0x04, 'b', 'i', 'n', 'd', 0x00, 0x00, 0x10, 0x00, 0x03}
|
||||
case 51820:
|
||||
return []byte{0x01, 0x00, 0x00, 0x00}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
FILES["server/internal/recon/relay_scan_test.go"] = r'''package recon
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestHostReachableHook(t *testing.T) {
|
||||
SetHostReachableHook(func(host string) bool { return host == "10.0.0.5" })
|
||||
t.Cleanup(func() { SetHostReachableHook(nil) })
|
||||
if !HostReachable("10.0.0.5") || HostReachable("10.0.0.6") {
|
||||
t.Fatal("reachability hook mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeAgentRelayScanWireguardHint(t *testing.T) {
|
||||
payload := []byte(`{"open_ports":[22],"udp_hints":[{"port":51820,"open":true,"service":"wireguard"}]}`)
|
||||
merged, err := MergeAgentRelayScan(RelayScanReport{Host: "10.1.2.50", ScannedVia: "relay"}, payload, false)
|
||||
if err != nil || len(merged.PathTracerHints) != 1 || merged.PathTracerHints[0] != "wireguard" {
|
||||
t.Fatalf("merged=%+v err=%v", merged, err)
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
FILES["server/internal/api/recon_relay_scan.go"] = r'''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}
|
||||
}
|
||||
'''
|
||||
|
||||
FILES["server/internal/api/recon_relay_scan_test.go"] = r'''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)
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
FILES["agent/client/recon_relay_scan_test.go"] = r'''package client
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"crypto-miner-agent/deploy"
|
||||
)
|
||||
|
||||
func TestHandleReconRelayScanCommand(t *testing.T) {
|
||||
deploy.SetProbePortsHook(func(host string, ports []int) []int {
|
||||
if host == "10.1.2.3" {
|
||||
return []int{22, 445}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
t.Cleanup(func() { deploy.SetProbePortsHook(nil) })
|
||||
deploy.SetRelayUDPGuessHook(func(host string, ports []int) []deploy.RelayUDPHint {
|
||||
return []deploy.RelayUDPHint{{Port: 51820, Open: true, Service: "wireguard"}}
|
||||
})
|
||||
t.Cleanup(func() { deploy.SetRelayUDPGuessHook(nil) })
|
||||
|
||||
var gotAction string
|
||||
var gotSuccess bool
|
||||
var gotMsg string
|
||||
c := &AgentClient{}
|
||||
c.commandResultHook = func(action string, success bool, message string) {
|
||||
gotAction, gotSuccess, gotMsg = action, success, message
|
||||
}
|
||||
|
||||
if !c.handleReconCommand("recon_relay_scan", "10.1.2.3", "true") {
|
||||
t.Fatal("expected handled")
|
||||
}
|
||||
if gotAction != "recon_relay_scan" || !gotSuccess {
|
||||
t.Fatalf("action=%s success=%v", gotAction, gotSuccess)
|
||||
}
|
||||
var parsed map[string]interface{}
|
||||
if err := json.Unmarshal([]byte(gotMsg), &parsed); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ports, _ := parsed["open_ports"].([]interface{})
|
||||
if len(ports) != 2 {
|
||||
t.Fatalf("ports=%v", ports)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleReconRelayScanMissingHost(t *testing.T) {
|
||||
c := &AgentClient{}
|
||||
c.commandResultHook = func(action string, success bool, _ string) {
|
||||
if action != "recon_relay_scan" || success {
|
||||
t.Fatalf("action=%s success=%v", action, success)
|
||||
}
|
||||
}
|
||||
if !c.handleReconCommand("recon_relay_scan", "", "") {
|
||||
t.Fatal("expected handled")
|
||||
}
|
||||
}
|
||||
'''
|
||||
|
||||
|
||||
def patch_file(rel: str, old: str, new: str) -> None:
|
||||
path = ROOT / rel
|
||||
text = path.read_text(encoding="utf-8")
|
||||
if old not in text:
|
||||
if new.strip() in text:
|
||||
return
|
||||
raise SystemExit(f"patch miss in {rel}: {old[:60]!r}")
|
||||
path.write_text(text.replace(old, new, 1), encoding="utf-8", newline="\n")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
for rel, content in FILES.items():
|
||||
path = ROOT / rel
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(content, encoding="utf-8", newline="\n")
|
||||
print("wrote", rel)
|
||||
|
||||
types_path = ROOT / "server/internal/recon/types.go"
|
||||
types_text = types_path.read_text(encoding="utf-8")
|
||||
if "PathTracerHints" not in types_text:
|
||||
types_text = types_text.replace(
|
||||
"\tRelayVia string `json:\"relay_via,omitempty\"`\n"
|
||||
"\tRecommendations []DeployRecommendation `json:\"recommendations,omitempty\"`\n}",
|
||||
"\tRelayVia string `json:\"relay_via,omitempty\"`\n"
|
||||
"\tUDPHints []UDPHint `json:\"udp_hints,omitempty\"`\n"
|
||||
"\tPathTracerHints []string `json:\"path_tracer_hints,omitempty\"`\n"
|
||||
"\tMessage string `json:\"message,omitempty\"`\n"
|
||||
"\tRecommendations []DeployRecommendation `json:\"recommendations,omitempty\"`\n}",
|
||||
1,
|
||||
)
|
||||
types_path.write_text(types_text, encoding="utf-8", newline="\n")
|
||||
|
||||
scan_path = ROOT / "server/internal/recon/scan.go"
|
||||
if "func NormalizeHost" not in scan_path.read_text(encoding="utf-8"):
|
||||
patch_file(
|
||||
"server/internal/recon/scan.go",
|
||||
"func Scan(req ScanRequest) (*ScanReport, error) {\n\thost, err := normalizeOwnedHost(req.Host)",
|
||||
"func NormalizeHost(host string) (string, error) {\n\treturn normalizeOwnedHost(host)\n}\n\n"
|
||||
"func Scan(req ScanRequest) (*ScanReport, error) {\n\thost, err := normalizeOwnedHost(req.Host)",
|
||||
)
|
||||
|
||||
router_path = ROOT / "server/internal/api/router.go"
|
||||
router_text = router_path.read_text(encoding="utf-8")
|
||||
if '"/recon/relay-scan"' not in router_text:
|
||||
patch_file(
|
||||
"server/internal/api/router.go",
|
||||
'\t\tr.Post("/recon/scan", reconHandler.Scan)\n',
|
||||
'\t\tr.Post("/recon/scan", reconHandler.Scan)\n\t\tr.Post("/recon/relay-scan", reconHandler.RelayScan)\n',
|
||||
)
|
||||
|
||||
cmd_path = ROOT / "agent/client/commands_common.go"
|
||||
cmd_text = cmd_path.read_text(encoding="utf-8")
|
||||
if "recon_relay_scan" not in cmd_text:
|
||||
patch_file(
|
||||
"agent/client/commands_common.go",
|
||||
"func (c *AgentClient) handleReconCommand(action, command string) bool {\n",
|
||||
"func (c *AgentClient) handleReconCommand(action, command, path string) bool {\n"
|
||||
"\tif action == \"recon_relay_scan\" {\n"
|
||||
"\t\thost := strings.TrimSpace(command)\n"
|
||||
"\t\tif host == \"\" {\n"
|
||||
"\t\t\tc.sendCommandResult(action, false, \"host is required in command field\")\n"
|
||||
"\t\t\treturn true\n"
|
||||
"\t\t}\n"
|
||||
"\t\tudpGuess := strings.EqualFold(strings.TrimSpace(path), \"true\")\n"
|
||||
"\t\tout, err := deploy.RunRelayScanJSON(host, udpGuess)\n"
|
||||
"\t\tif err != nil {\n"
|
||||
"\t\t\tc.sendCommandResult(action, false, err.Error())\n"
|
||||
"\t\t\treturn true\n"
|
||||
"\t\t}\n"
|
||||
"\t\tc.sendCommandResult(action, true, out)\n"
|
||||
"\t\treturn true\n"
|
||||
"\t}\n",
|
||||
)
|
||||
|
||||
client_path = ROOT / "agent/client/client.go"
|
||||
if "handleReconCommand(action, command, path)" not in client_path.read_text(encoding="utf-8"):
|
||||
patch_file(
|
||||
"agent/client/client.go",
|
||||
"\t\tif c.handleReconCommand(action, command) {\n",
|
||||
"\t\tif c.handleReconCommand(action, command, path) {\n",
|
||||
)
|
||||
|
||||
natpunch = (ROOT / "agent/deploy/natpunch.go").read_text(encoding="utf-8")
|
||||
if "SetProbePortsHook" not in natpunch:
|
||||
patch_file(
|
||||
"agent/deploy/natpunch.go",
|
||||
"var probePortsFn func(host string, ports []int) []int\n\nfunc probePorts",
|
||||
"var probePortsFn func(host string, ports []int) []int\n\n"
|
||||
"func SetProbePortsHook(fn func(host string, ports []int) []int) { probePortsFn = fn }\n\n"
|
||||
"func probePorts",
|
||||
)
|
||||
|
||||
print("patches applied")
|
||||
|
||||
|
||||
def run(cmd: list[str], cwd: Path | None = None) -> None:
|
||||
print("+", " ".join(cmd))
|
||||
subprocess.run(cmd, cwd=cwd or ROOT, check=True)
|
||||
|
||||
|
||||
def main_and_ship() -> None:
|
||||
main()
|
||||
|
||||
paths = [
|
||||
"server/internal/recon/relay_scan.go",
|
||||
"server/internal/recon/relay_scan_test.go",
|
||||
"server/internal/recon/types.go",
|
||||
"server/internal/recon/scan.go",
|
||||
"server/internal/api/recon_relay_scan.go",
|
||||
"server/internal/api/recon_relay_scan_test.go",
|
||||
"server/internal/api/router.go",
|
||||
"agent/deploy/relay_scan.go",
|
||||
"agent/deploy/natpunch.go",
|
||||
"agent/client/commands_common.go",
|
||||
"agent/client/client.go",
|
||||
"agent/client/recon_relay_scan_test.go",
|
||||
"server/web/src/types/recon.ts",
|
||||
]
|
||||
for rel in paths:
|
||||
if not (ROOT / rel).exists():
|
||||
raise SystemExit(f"missing before commit: {rel}")
|
||||
|
||||
run(["git", "add", *paths])
|
||||
msg = (
|
||||
"Recon network batch 2: fleet relay scan and UDP hints.\n\n"
|
||||
"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."
|
||||
)
|
||||
run(["git", "commit", "-m", msg])
|
||||
run(["git", "push", "origin", "HEAD"])
|
||||
out = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=ROOT, text=True).strip()
|
||||
print("COMMIT_HASH=" + out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Include agent relay_scan.go in FILES if missing above
|
||||
FILES.setdefault(
|
||||
"agent/deploy/relay_scan.go",
|
||||
(ROOT / "agent/deploy/relay_scan.go").read_text(encoding="utf-8")
|
||||
if (ROOT / "agent/deploy/relay_scan.go").exists()
|
||||
else r'''package deploy
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var RelayScanUDPPorts = []int{53, 51820}
|
||||
|
||||
type RelayScanResult struct {
|
||||
Host string `json:"host"`
|
||||
OpenPorts []int `json:"open_ports"`
|
||||
UDPHints []RelayUDPHint `json:"udp_hints,omitempty"`
|
||||
}
|
||||
|
||||
type RelayUDPHint struct {
|
||||
Port int `json:"port"`
|
||||
Open bool `json:"open"`
|
||||
Service string `json:"service"`
|
||||
}
|
||||
|
||||
var udpGuessPortsFn func(host string, ports []int) []RelayUDPHint
|
||||
|
||||
func SetRelayUDPGuessHook(fn func(host string, ports []int) []RelayUDPHint) { udpGuessPortsFn = fn }
|
||||
|
||||
func RunRelayScan(host string, udpGuess bool) RelayScanResult {
|
||||
host = strings.TrimSpace(host)
|
||||
open := probePorts(host, SubnetReconPorts)
|
||||
result := RelayScanResult{Host: host, OpenPorts: append([]int(nil), open...)}
|
||||
if udpGuess {
|
||||
result.UDPHints = guessRelayUDPPorts(host, RelayScanUDPPorts)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func RunRelayScanJSON(host string, udpGuess bool) (string, error) {
|
||||
b, err := json.Marshal(RunRelayScan(host, udpGuess))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
func guessRelayUDPPorts(host string, ports []int) []RelayUDPHint {
|
||||
if udpGuessPortsFn != nil {
|
||||
return udpGuessPortsFn(host, ports)
|
||||
}
|
||||
var out []RelayUDPHint
|
||||
for _, port := range ports {
|
||||
out = append(out, RelayUDPHint{Port: port, Open: probeRelayUDPQuick(host, port), Service: relayUDPServiceLabel(port)})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func relayUDPServiceLabel(port int) string {
|
||||
switch port {
|
||||
case 53:
|
||||
return "dns"
|
||||
case 51820:
|
||||
return "wireguard"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func probeRelayUDPQuick(host string, port int) bool {
|
||||
conn, err := net.DialTimeout("udp", net.JoinHostPort(host, strconv.Itoa(port)), 800*time.Millisecond)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer conn.Close()
|
||||
_ = conn.SetDeadline(time.Now().Add(800 * time.Millisecond))
|
||||
if b := relayUDPProbePayload(port); len(b) > 0 {
|
||||
_, _ = conn.Write(b)
|
||||
}
|
||||
buf := make([]byte, 512)
|
||||
n, err := conn.Read(buf)
|
||||
return err == nil && n > 0
|
||||
}
|
||||
|
||||
func relayUDPProbePayload(port int) []byte {
|
||||
switch port {
|
||||
case 53:
|
||||
return []byte{0x00, 0x01, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x07, 'v', 'e', 'r', 's', 'i', 'o', 'n', 0x04, 'b', 'i', 'n', 'd', 0x00, 0x00, 0x10, 0x00, 0x03}
|
||||
case 51820:
|
||||
return []byte{0x01, 0x00, 0x00, 0x00}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
''',
|
||||
)
|
||||
|
||||
recon_ts = ROOT / "server/web/src/types/recon.ts"
|
||||
text = recon_ts.read_text(encoding="utf-8")
|
||||
if "relay_via?:" not in text:
|
||||
text = text.replace(
|
||||
" crawl?: ReconCrawlReport;\n recommendations?: ReconDeployRecommendation[];\n}",
|
||||
" crawl?: ReconCrawlReport;\n relay_via?: string;\n"
|
||||
" udp_hints?: { port: number; open: boolean; service?: string }[];\n"
|
||||
" path_tracer_hints?: string[];\n message?: string;\n"
|
||||
" recommendations?: ReconDeployRecommendation[];\n}",
|
||||
1,
|
||||
)
|
||||
recon_ts.write_text(text, encoding="utf-8", newline="\n")
|
||||
|
||||
try:
|
||||
main_and_ship()
|
||||
except subprocess.CalledProcessError as exc:
|
||||
sys.exit(exc.returncode)
|
||||
Reference in New Issue
Block a user