hub v0.59.0: Direction-2a agent-plane immediate-sync poke sender + ep0 felhom-poke surface
- internal/poke: pinned-host-key SSH poke sender (wgsync sibling) + fire-and-forget Notifier (PokeHost/PokeAllHosts). Poke refuses non-WG targets pre-dial; contentless via ep0 forced command to the box WG /32:51822. - wiring: Server.SetPoke; applyPBSDR pokes the host after each descriptor gen-bump; handleSetArtifacts (MinAgent floor) pokes all hosts. main.go env POKE_SSH_KEY_FILE (reuses peersync endpoint/hostkey). - scripts/felhom-poke.sh (non-root forced command) + offsite-endpoint.md §11; manifests/hub.yaml Secret/agent-poke + POKE_SSH_KEY_FILE (image tag bump follows the build).
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
package poke
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func quietLogger() *log.Logger { return log.New(io.Discard, "", 0) }
|
||||
|
||||
type fakePeerStore struct {
|
||||
byHost map[string]*store.WGPeer
|
||||
all []store.WGPeer
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakePeerStore) GetWGPeerForHost(hostID string) (*store.WGPeer, error) {
|
||||
if f.err != nil {
|
||||
return nil, f.err
|
||||
}
|
||||
return f.byHost[hostID], nil
|
||||
}
|
||||
func (f *fakePeerStore) ListWGPeers() ([]store.WGPeer, error) {
|
||||
if f.err != nil {
|
||||
return nil, f.err
|
||||
}
|
||||
return f.all, nil
|
||||
}
|
||||
|
||||
type fakeSender struct {
|
||||
mu sync.Mutex
|
||||
targets []string
|
||||
err error
|
||||
}
|
||||
|
||||
func (f *fakeSender) Poke(_ context.Context, boxWGIP string) error {
|
||||
f.mu.Lock()
|
||||
f.targets = append(f.targets, boxWGIP)
|
||||
f.mu.Unlock()
|
||||
return f.err
|
||||
}
|
||||
func (f *fakeSender) got() []string {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
return append([]string(nil), f.targets...)
|
||||
}
|
||||
|
||||
// pokeHost resolves the host's WG /32 and sends the poke there — the address is looked up, never
|
||||
// caller-supplied.
|
||||
func TestPokeHost_SendsToResolvedWGIP(t *testing.T) {
|
||||
st := &fakePeerStore{byHost: map[string]*store.WGPeer{
|
||||
"host-abc": {HostID: "host-abc", AssignedIP: "10.77.0.5"},
|
||||
}}
|
||||
snd := &fakeSender{}
|
||||
n := NewNotifier(st, snd, quietLogger())
|
||||
|
||||
if err := n.pokeHost(context.Background(), "host-abc"); err != nil {
|
||||
t.Fatalf("pokeHost: %v", err)
|
||||
}
|
||||
got := snd.got()
|
||||
if len(got) != 1 || got[0] != "10.77.0.5" {
|
||||
t.Fatalf("sender targets = %v, want [10.77.0.5]", got)
|
||||
}
|
||||
}
|
||||
|
||||
// A host with no WG peer is a no-op error the caller logs — never a send, never a save failure.
|
||||
func TestPokeHost_NoPeerNoSend(t *testing.T) {
|
||||
snd := &fakeSender{}
|
||||
n := NewNotifier(&fakePeerStore{byHost: map[string]*store.WGPeer{}}, snd, quietLogger())
|
||||
if err := n.pokeHost(context.Background(), "unknown-host"); err == nil {
|
||||
t.Fatal("pokeHost for a peerless host returned nil, want an error")
|
||||
}
|
||||
if len(snd.got()) != 0 {
|
||||
t.Fatalf("sender was called for a peerless host: %v", snd.got())
|
||||
}
|
||||
}
|
||||
|
||||
// A store error propagates as an error (caller logs, never fails the save) and sends nothing.
|
||||
func TestPokeHost_StoreErrorNoSend(t *testing.T) {
|
||||
snd := &fakeSender{}
|
||||
n := NewNotifier(&fakePeerStore{err: errors.New("db down")}, snd, quietLogger())
|
||||
if err := n.pokeHost(context.Background(), "host-abc"); err == nil {
|
||||
t.Fatal("pokeHost on a store error returned nil")
|
||||
}
|
||||
if len(snd.got()) != 0 {
|
||||
t.Fatalf("sender called despite store error: %v", snd.got())
|
||||
}
|
||||
}
|
||||
|
||||
// A nil *Notifier is a safe no-op (poke disabled) — the public entrypoints must not panic.
|
||||
func TestNilNotifierIsNoOp(t *testing.T) {
|
||||
var n *Notifier
|
||||
n.PokeHost("h") // must not panic
|
||||
n.PokeAllHosts()
|
||||
}
|
||||
|
||||
// Client.Poke REFUSES any target outside the WG /24 BEFORE any network I/O — the confinement the
|
||||
// forced command and the WG kernel also enforce, checked here at the source.
|
||||
func TestClientPoke_RefusesNonWGTarget(t *testing.T) {
|
||||
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("keygen: %v", err)
|
||||
}
|
||||
signer, err := ssh.NewSignerFromKey(priv)
|
||||
if err != nil {
|
||||
t.Fatalf("signer: %v", err)
|
||||
}
|
||||
pemBlock, err := ssh.MarshalPrivateKey(priv, "")
|
||||
if err != nil {
|
||||
t.Fatalf("marshal key: %v", err)
|
||||
}
|
||||
hostLine := string(ssh.MarshalAuthorizedKey(signer.PublicKey()))
|
||||
c, err := New(Config{
|
||||
Addr: "198.51.100.1:22", // TEST-NET-2 — must never actually be dialed
|
||||
User: "felhom-peersync",
|
||||
PrivateKey: pem.EncodeToMemory(pemBlock),
|
||||
HostKeyLine: hostLine,
|
||||
}, quietLogger())
|
||||
if err != nil {
|
||||
t.Fatalf("New: %v", err)
|
||||
}
|
||||
err = c.Poke(context.Background(), "8.8.8.8") // public IP — outside 10.77.0.0/24
|
||||
if err == nil || !strings.Contains(err.Error(), "refusing non-WG target") {
|
||||
t.Fatalf("Poke to a public IP: err = %v, want a pre-dial 'refusing non-WG target' refusal", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user