A Java framework for encrypted, peer-to-peer distribution of executors, assets, and notifications across a cluster of nodes called a Realm.

JSDF (Java Secure Distribution Framework) is a small Java library for building a decentralized, encrypted cluster of cooperating nodes. Nodes form a Realm, exchange trust through a Beacon join protocol, then publish Distributable objects — executors, assets, and notifications — to peers over AES/GCM channels.

It’s meant for cases where you want several JVMs to share work and state without a heavyweight message bus: remote execution hooks, capability discovery, redundant asset storage, and NAT-aware fallback when direct TCP fails.

JDK-only for the core path (sockets, crypto, Swing for interactive Beacon approval). Build with Maven; Java 16+.

What it’s for

At a high level, JSDF gives you:

  1. Realm membership — create a cluster (UUID) or join one via an authorized peer
  2. Authenticated channels — RSA key exchange into a Trusted Key Store (TKS) of per-node AES keys
  3. Typed distribution — publish EXECUTOR, ASSET, or NOTIFICATION payloads and collect responses
  4. Event handling — register listeners for what arrives, including join/leave
  5. Connectivity fallback — when TCP publish fails, coordinate NAT punch and/or relay through an authorized node

Think of it as a lightweight distribution fabric for Java objects among nodes that have already agreed to trust each other — not a general-purpose RPC framework and not a replacement for Kafka/NATS.

Core concepts

Distributor

The Distributor is the TCP front door. It listens on port 14789 by default. Each incoming client writes a 36-byte Realm UUID first; the Distributor routes the connection to the matching local Realm instance (or rejects unknown Realm IDs).

One process can host multiple Realms; the UUID multiplexes them on a single port.

Realm

A Realm is the logical cluster: known peer addresses, the Trusted Key Store (TKS) (AES keys per authorized node), Beacon evaluation, encrypted publish/receive, and the event registry.

On creation a Realm generates:

  • An RSA keypair (asymmetric bootstrap)
  • A local AES node key (symmetric traffic after trust is established)

Distributable

Anything sent between nodes implements Distributable (Serializable) and reports a DistributableType:

Type Role
EXECUTOR Runnable work; may run on the receiver
ASSET Storable payload (data, artifacts)
NOTIFICATION Control / signaling messages (includes Beacons, NAT punch, capabilities)

Payload wraps an arbitrary Serializable for use inside notifications and similar messages.

Beacon

Joining a Realm is not “connect and you’re in.” An unauthorized node sends a Beacon to an authorized peer’s Distributor. That peer runs local **BeaconEvaluator**s (Swing dialog, console prompt, always-accept for tests, certificate checks, etc.). If those pass, it publishes a BeaconEvaluation to every already-known Realm peer and requires a unanimous yes (and a response from each known peer). Only then does RSA/AES key exchange run and the newcomer enter the TKS.

Older design diagrams called this a “quorum.” The implementation in Realm.handleBeaconRequest is stricter: all known peers must respond, and any false rejects the join. If the authorized node has no known peers yet (first joiner to a fresh Realm), the peer round-trip is empty and the join can proceed after local evaluation alone.

flowchart LR
  start([Joining node]) --> connect[Connect Distributor :14789]
  connect --> uuid[Send Realm UUID]
  uuid --> beacon[Send Beacon]

  beacon --> localEval{Local BeaconEvaluators}
  localEval -->|false| fail([Join failed])
  localEval -->|true| pubEval[Publish BeaconEvaluation to known peers]

  pubEval --> waitResp{All peers responded?}
  waitResp -->|no| fail
  waitResp -->|yes| unanimous{Unanimous true?}

  unanimous -->|no| fail
  unanimous -->|yes| accept[BeaconAcceptance + RSA pubkey]

  accept --> encAes[Joiner sends AES encrypted with RSA]
  encAes --> storeTks[Store joiner AES in TKS]
  storeTks --> retAes[Return auth node AES to joiner]
  retAes --> fanout[TKS publish + NodeJoin]
  fanout --> ok([Authorized — AES/GCM ready])

RealmEventRegistry

Incoming distributables are turned into DistributionEvents (EXECUTOR, ASSET, NOTIFICATION, JOIN, LEAVE) and delivered to registered RealmEventListeners, which return EventResults. That’s the extension point for application logic.

BootstrapExecutor

A special Executor subclass executed on reception by the Realm handler instead of only going through the registry. That lets a published executor self-register listeners or services on remote nodes. Bootstrap execution is off by default and must be enabled explicitly — important if you don’t want remote code self-installing handlers without opt-in.

How a node joins

  1. Peer A already has a Realm (created locally or previously joined).
  2. Peer B connects to A’s Distributor with the Realm UUID and sends a Beacon.
  3. A runs local BeaconEvaluators; on failure it returns BeaconRejection immediately.
  4. On local success, A publishes BeaconEvaluation to known peers and waits for a full set of boolean replies (unanimous accept).
  5. On Realm accept: A sends BeaconAcceptance with its RSA public key; B encrypts its AES key to that key; A stores B’s AES in the TKS and returns A’s AES encrypted to B; B stores A’s key.
  6. A fans out TKS material / NodeJoin so the Realm learns the new peer.
  7. Later traffic uses AES-256/GCM (12-byte IV, 128-bit auth tag). The IV is sent at the start of each connection; distributables are Java-serialized over the encrypted stream.

If a peer isn’t in the TKS yet when opening a normal connection, the Realm falls into Beacon negotiation (handleBeaconRequest) instead of opening a full encrypted channel.

How publishing works

An authorized node calls into the Realm to publish a Distributable to known peers (in parallel) and aggregate responses. Receivers deserialize (under the trust rules below), package a DistributionEvent, and fire the registry.

Rough intended flows for the three content kinds:

  • Capabilities / notifications — peers evaluate and may store or ignore
  • Assets — peers decide whether to keep a copy based on local capacity
  • Executors — peers check capabilities/assets; may accept, reject, or compete when multiple nodes can run the work (unless marked parallel)

When direct TCP fails, the Realm can request NAT punch coordination or relay through an authorized node so the same event path still runs.

Security model

Trust-based deserialization

SafeObjectInputStream splits behavior by trust:

  • Untrusted (not in TKS): whitelist of known-safe JSDF classes only — covers Beacon / early exchange with strangers
  • Trusted (in TKS, AES/GCM authenticated): broader deserialization so peers can send application Executor types

Known gadget classes are blocked; object-graph depth is capped.

Optional post-auth filter

After authentication, you can attach a JDK ObjectInputFilter. JSDF ships a factory for a default allowlist:

import java.util.UUID;
import com.javashell.distributor.Realm;

Realm realm = new Realm(
    UUID.randomUUID(),
    null,
    null,
    false,
    Realm.createJsdfPostAuthObjectInputFilter()
);

Auth-session traffic still goes through SafeObjectInputStream; the optional filter applies once the peer is authenticated. Stricter apps can pass a custom filter.

Crypto notes (current)

  • Node-to-node: RSA-2048 for bootstrap, AES-256/GCM for ongoing traffic
  • Beacon key-exchange phase: still uses a simpler AES path in places; upgrading that segment to GCM (with proper IV handling) is on the roadmap
  • Default Beacon UX is interactive (Swing) — fine for demos, not for headless servers (use console, always-accept in tests, or cert evaluators)

Certificate-oriented join is documented under docs/CERT_USAGE.md (X509CertificateValidationEvaluator, fingerprint or CA trust models, replay protections via timestamp/nonce).

How to use it

Build and run the REPL

From the JSDF project root:

mvn -q -DskipTests compile
mvn -q exec:java -Dexec.mainClass=com.javashell.main.ReplMain

Or with javac/java (no external deps beyond the JDK):

javac -d bin -sourcepath src src/com/javashell/main/ReplMain.java
java -cp bin com.javashell.main.ReplMain

The Distributor listens on 14789. The REPL prompt accepts built-in commands implemented as Executors:

Command Purpose
CreateRealm Create a local Realm (random UUID) and set it active
JoinRealm <UUID> <IP> Join via an authorized node’s address
ListNodes List known peers in the current Realm
NumNodes Count of known peers
JSDFTest Small demo/test command

Example session sketch:

> CreateRealm
> ListNodes
> JoinRealm 550e8400-e29b-41d4-a716-446655440000 192.168.1.10

(Exact output depends on evaluators and whether the peer accepts the Beacon.)

Embedding in your own app

Typical integration shape:

  1. Construct a Distributor and one or more Realms (with the filter/bootstrap flags you want).
  2. Register RealmEventListeners for the event types you care about.
  3. Publish Notification / Asset / Executor subclasses as your protocol.
  4. Prefer non-interactive BeaconEvaluators (or cert-based trust) for servers.
  5. Leave BootstrapExecutor disabled unless you explicitly want remote self-registration.

Docker / NAT fallback testing

Compose files in the repo exercise Beacon join with TCP blocked so NAT punch / relay paths run. Example:

docker compose -f docker-compose.realm.yml up --build --abort-on-container-exit realm_peer_a realm_peer_b

Look for log markers such as PUBLISH_RESULT success=true and LISTENER_RESULT received=true. Compose may still exit non-zero because of stop ordering — treat those markers as the functional pass.

There’s also a localhost rendezvous + holepunch demo (com.javashell.main.RendezvousHolepunchDemo) for UDP punch experiments; real NAT traversal needs a reachable rendezvous host and synchronized punches on both peers.

Architecture at a glance

Join (simplified): unauthorized node → Beacon → authorized node evaluates → on accept, RSA/AES key exchange → TKS updated → AES/GCM channels.

Connection: client connects to Distributor → sends Realm UUID → Realm checks TKS → encrypted distributable → RealmEventRegistry fires listeners.

Publish fallback: TCP fail → NAT punch instruction / status → direct UDP framing and/or RelayEnvelope via authorized node → same event handling on receipt.

Current limitations

Worth knowing before you lean on it in production:

  • Wire format is Java serialization — needs trust first; message versioning is still a future concern
  • TKS / known nodes are in-memory (no durable membership store yet)
  • Distributor framing is “36-byte UUID up front” — workable, but brittle vs a length-prefixed handshake
  • Some Beacon evaluator paths (full quorum, MFA, ZKP, richer scope rules) are roadmap items
  • Unit-test coverage is thinner than the Docker integration scenarios

Shutdown of a Realm logs targeted diagnostics from Distributor.removeRealm(...) ([JSDF][Shutdown] ...) to help trace teardown and invalidation publish.

Where to look in the code

src/com/javashell/
├── main/ReplMain.java          # entry
├── repl/                       # CLI commands
└── distributor/
    ├── Distributor.java        # TCP multiplex on 14789
    ├── Realm.java              # TKS, crypto, publish, join
    ├── Distributable*.java
    ├── beacon/                 # Beacon protocol + evaluators
    ├── types/                  # Executor, Asset, Notification, events
    ├── udp/                    # fragment / reassembly / stream adapt
    └── utils/SafeObjectInputStream.java

Docs in-repo: README.md, docs/CERT_USAGE.md, docs/NAT_DOCKER_TESTING.md.