eb227486d0
- 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).
137 lines
3.8 KiB
Go
137 lines
3.8 KiB
Go
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)
|
|
}
|
|
}
|