Skip to content

ADR 0037 — pkg/oobi/client/ — Universal Service Mesh for Inter-MÓDULO Communication

  • Status: Proposed (2026-05-13) — materialization in flight (PR #707)
  • Date: 2026-05-13
  • Deciders: TLSStress.Art project
  • Targets: v4.x (alongside HyperBridge.Art HB-1.6 first adopter)
  • Builds on: ADR 0019 (OOBI slot allocation), ADR 0024 (SPAN.Art), ADR 0029 (Sealed audit log), ADR 0036 (HyperBridge.Art), discuss_oobi_immutable_gateway_art_2026_05_10

Context

The TLSStress.Art bench has 38 MÓDULOs (as of v3.9 + HyperBridge.Art). Many of them need to talk to each other over HTTP:

  • SPAN-1 receives PacketBatch JSON from HyperBridge.Art (HB-1.6)
  • SPAN-2 receives PacketBatch from SPAN-1
  • SPAN-3 receives MetadataEvent from SPAN-2
  • FLOW.Art receives ValidationReport from SPAN-3
  • VALIDATOR.Art receives anomaly tickets from N producers
  • RELAY.Art receives operator commands from GATEWAY
  • ... and many more

Pre-v3.10, every MÓDULO rolled its own HTTP client. Each one had to independently:

  1. Implement an HTTP client + retry policy
  2. Configure a hardcoded destination URL via flag (e.g. --sink-url)
  3. (Forget to) source-bind to the OOBI VXLAN overlay interface
  4. (Forget to) configure mTLS via the cert-manager oobi-ca-issuer
  5. (Forget to) handle failover when destination is HA pair
  6. Emit its own non-standardized metrics
  7. (Forget to) audit-log every outbound for ZTP-prem compliance

The result: communication that "works on the lab single-node" but breaks subtly in production multi-node deployments. The most common failure mode: pod's outbound packets escape via the host's default route (the customer's management LAN) instead of being encapsulated into the OOBI VXLAN underlay — silently bypassing the isolation guarantee that the entire ZTP-prem story depends on.

Decision

Introduce pkg/oobi/client/ as the single, universal HTTP client that every MÓDULO uses to talk to every other MÓDULO. The package encapsulates everything that makes inter-MÓDULO communication correct across all deployment topologies:

Concern What oobi/client does
Slot → IP resolution Lookup determinístico via canon.go. Operator never types a URL.
Source-bind on OOBI overlay SO_BINDTODEVICE("oobi0") on every socket. Packets MUST exit via the overlay, never the default route.
mTLS via cert-manager Loads tls.crt / tls.key / ca.crt from the canonical mount path /etc/oobi/tls/. TLS 1.3 minimum.
Primary → Standby failover Threshold-based switch when primary fails N consecutive times. Auto fail-back via the same mechanism.
Retry with exponential backoff 25ms base, capped at 5s, jittered to avoid thundering herd.
Aggressive timeouts 500ms per request default. A hung peer never blocks the caller's data plane.
Standardized metrics requests_total, errors_total{outcome}, latency mean+max. Same shape for every MÓDULO.
Single-node escape hatch LocalOverride field bypasses the fabric entirely. Lab dev still works.
Audit hook Optional callback receives one entry per outbound request → wires into the ZTP-prem sealed audit log.

Deployment-agnostic by design

The same Config produces correct behavior in every topology:

                       │  Single  │  Multi   │  Bare-metal │  Single  │
                       │  K8s     │  K8s     │  systemd    │  systemd │
                       │  OOBI    │  OOBI    │  multi-host │  lab     │
───────────────────────┼──────────┼──────────┼─────────────┼──────────┤
TargetSlot mode         │   ✅     │   ✅     │     ✅      │    n/a   │
   → resolves to .230  │          │          │             │          │
   → source-bind       │ oobi0 (host on overlay)    │   skipped │
   → traffic stays on  │ overlay  │ overlay  │   overlay   │    n/a   │
LocalOverride mode      │   n/a    │   n/a    │     n/a     │    ✅    │
   → http://localhost  │          │          │             │ direct   │

The operator never knows or cares which deployment they're in. The Config is identical across all four — only the LocalOverride field distinguishes single-node lab from production.

Public API (target)

import (
    "github.com/nollagluiz/AI_forSE/pkg/oobi"
    "github.com/nollagluiz/AI_forSE/pkg/oobi/client"
)

c, err := client.New(client.Config{
    SourceModule: "hyperbridge-art",
    TargetSlot:   oobi.SlotSPANPrimary,    // .230
    FailoverSlot: oobi.SlotSPANStandby,    // .231
    ServicePort:  9098,
    TLS:          &client.TLSConfig{},     // cert-manager defaults
    AuditHook:    sealedLog.Append,
})
defer c.Close()

err = c.PostJSON(ctx, "/batch", packetBatch)

Migration plan

The legacy --sink-url=http://... flag stays as override mode:

1. If --sink-url is set:           legacy mode (becomes LocalOverride)
2. If --target-slot is set:        oobi/client mode (preferred)
3. If neither:                     config error

Each MÓDULO migrates independently — no breaking change, no coordinated deploy. Migration order matches data flow:

# MÓDULO Effort
1 HyperBridge.Art (HB-1.6, first adopter, #708) already in plan
2 SPAN-1 (pkg/span-collector/) — talks to SPAN-2 ~2h
3 SPAN-2 (pkg/span-tls-extractor/) — talks to SPAN-3 ~2h
4 SPAN-3 (pkg/span-correlator/) — talks to FLOW.Art ~2h
5 RELAY.Art, GATEWAY.Art, VALIDATOR.Art ~2h each
... (remaining MÓDULOs as touched) ~2h each

Once all MÓDULOs have migrated, a future major bump removes the legacy --sink-url flag and enforces slot-based addressing.

Consequences

Pros

  • Correctness by construction. New MÓDULO author cannot accidentally skip source-bind, mTLS, or failover. The client handles all of it.
  • One spec to certify. ZTP-prem compliance auditor inspects ONE package, not 38 ad-hoc HTTP clients.
  • Operator UX. Operator configures slot numbers (canonical, documented in canon.go); never thinks about URLs.
  • Observability uniformity. Every MÓDULO emits the same metric shape, enabling cross-cutting Grafana dashboards.
  • Test infrastructure. LocalOverride makes integration tests trivial — point at a httptest.Server and the same code path exercises the production-shape HTTP client.

Cons / risks

  • Shared dependency. All 38 MÓDULOs now depend on pkg/oobi. A bug here is a fleet-wide bug. Mitigation: this package is small (~600 LOC + tests), Tier A, frozen API surface, exhaustive test coverage.
  • CGO-free constraint. No prometheus/client_golang, no fancy TLS extensions. The package exports a Snapshot() and lets each MÓDULO render its own metrics format. Tradeoff: every MÓDULO's /metrics handler has 10 extra lines of boilerplate.
  • Linux-only source-bind. SO_BINDTODEVICE doesn't exist on macOS / BSD — those platforms are dev-only. Source-bind is a no-op there; oobi0 interface doesn't exist anyway.
  • mTLS rollout coordination. First migrated MÓDULO must NOT enable TLS until ALL its peers have certs mounted. Stage TLS rollout per-MÓDULO with TLS: nil default + opt-in.

Compatibility

  • Pre-migration MÓDULOs keep using --sink-url — no change required.
  • Post-migration MÓDULOs use TargetSlot. Both modes coexist across a fleet during the migration window.
  • systemd-deployed MÓDULOs with the canon oobi0 interface set up by systemd-networkd work identically to K8s pods.

References

  • Code: pkg/oobi/client/ (PR #707)
  • First adopter: pkg/hyperbridge-art/ (HB-1.6, PR #708)
  • Sibling: pkg/oobi/canon.go (slot map)
  • Overlay infra: k8s/oobi/30-vtep-daemonset.yaml
  • Cross-ref: ADR 0019 (OOBI slot allocation), ADR 0029 (Sealed audit log), ADR 0036 (HyperBridge.Art)