March Docs

NetKernel

NetKernel: the socket-level transport for distributed OTP (P1).

Bridges the pure protocol layer (NetFrame / NodeIdentity / ClusterAuth / Handshake, all over List(Int) bytes) to real TCP sockets (Socket, over String). It frames messages, reads complete frames off a byte stream (partial-read safe), and runs the authenticated handshake that gates a peer into the cluster.

The pure bridge (to_wire / str_to_bytes / bytes_to_str) is unit-tested; the socket handshake is runtime-exercised (sockets/scheduler are not run by the eval-based stdlib test harness, matching ChannelServer's convention).

P5 frame types (tag byte prepended before the msgpack payload): 0x05 REGISTRY_SYNC_REQ [tag=5, root_hash:Str] 0x06 REGISTRY_SYNC_RESP [tag=6, root_hash:Str, leaves:[[name,nid,pid,present,clock,creation],...]] clock: [[actor_id:Str, ts:Int],...]; a 4-element leaf (no clock, the original encoding) still decodes, with an empty clock, and a 5-element one (no creation) with creation 0 0x07 MONITOR_REQ [tag=7, watcher_node:Str, watcher_pid:Int, target_pid:Int] 0x08 MONITOR_FIRE [tag=8, target_pid:Int, reason_tag:Int, reason_msg:Str] reason_tag: 0=Normal, 1=Killed, 2=Crash, 3=NodeDown

Types

typeMacKeysMacKeys = { tx_key : Bytes, rx_key : Bytes }#
typeAuthedAuthed = { identity : NodeIdentity.Identity, role : String, addr : String,#

What a finished handshake knows about the peer: its identity, the role it announced for this connection, its advertised address ("" when it sent none), its VERIFIED certificate (certificate mode) and whether the connection's frames are sealed from here on.

Functions

fnbytes_to_strbytes_to_str(b : List(Int)) : String do Bytes.to_string(Bytes.from_list(b)) end#

The String whose bytes are [b] (the inverse of str_to_bytes): what the socket layer writes.

fndecode_monitor_firedecode_monitor_fire(bytes : List(Int)) : Result((Int, Int, String), String)#

Decode a MONITOR_FIRE frame payload into (target_pid, reason_tag, reason_msg). Err for bytes that are not MessagePack or not a MONITOR_FIRE.

fndecode_monitor_reqdecode_monitor_req(bytes : List(Int)) : Result((String, Int, Int), String)#

Decode a MONITOR_REQ frame payload into (watcher_node, watcher_pid, target_pid). Err for bytes that are not MessagePack or not a MONITOR_REQ.

fndecode_registry_sync_reqdecode_registry_sync_req(bytes : List(Int)) : Result(String, String)#

Decode a REGISTRY_SYNC_REQ frame payload into its root hash. Err for bytes that are not MessagePack or not a REGISTRY_SYNC_REQ.

fndecode_registry_sync_respdecode_registry_sync_resp(bytes : List(Int)) : Result((String, List((String, GlobalRegistry.Entry))), String)#

Decode a REGISTRY_SYNC_RESP frame payload into (root hash, leaves). Older leaf shapes still decode: a 4-element leaf with an empty clock, a

  1. A malformed leaf is skipped and a malformed

clock pair ignored; a decoded entry's registrant is "", and a present entry never carries a tombstone stamp. Err for bytes that are not MessagePack or not a REGISTRY_SYNC_RESP.

fnencode_monitor_fireencode_monitor_fire(target_pid : Int, reason_tag : Int, reason_msg : String) : List(Int)#

A MONITOR_FIRE frame payload (MessagePack, tag 8): the watched process [target_pid] died, for [reason_tag] (0 = Normal, 1 = Killed, 2 = Crash, 3 = NodeDown) with [reason_msg]; the receiver notifies its local watcher.

fnencode_monitor_reqencode_monitor_req(watcher_node : String, watcher_pid : Int, target_pid : Int) : List(Int)#

A MONITOR_REQ frame payload (MessagePack, tag 7): [watcher_pid] on node [watcher_node] asks to watch the receiver's local process [target_pid].

fnencode_registry_sync_reqencode_registry_sync_req(root_hash : String) : List(Int)#

A REGISTRY_SYNC_REQ frame payload (MessagePack, tag 5): our registry's Merkle [root_hash], asking the peer to diff.

fnencode_registry_sync_respencode_registry_sync_resp(root_hash : String,#

A REGISTRY_SYNC_RESP frame payload (MessagePack, tag 6): our [root_hash] and the [leaves] that differ. Each leaf is [name, node_id, pid, present (1/0), clock, creation], clock the entry's VectorClock as [[actor_id, ts], ...], plus a [0, retired_at] pair for a tombstone.

fnforgetforget(fd : Int) : ()#

Drop [fd]'s MAC state: its frames are plain again (see the rule above).

fnframe_tagframe_tag(bytes : List(Int)) : Option(Int)#

The tag of a frame payload: the first element of its outer MessagePack array, when that is an Int. None for bytes that do not decode, or another shape. It decodes the whole payload (it is not a cheap peek).

match NetKernel.frame_tag(frame) do
  Some(7) -> handle_monitor_req(frame)
  _ -> ()
end
fnframes_rejectedframes_rejected() : Int do Vault.get_or(mac_counts(), "bad_total", 0) end#

Frames dropped for a failed MAC, in this process, since it started.

fnfresh_noncefresh_nonce() : String do Crypto.random_hex(16) end#

A fresh random nonce for a handshake challenge: 16 random bytes as 32 hex digits (Crypto.random_hex(16)).

fnhandshakehandshake(fd : Int, my_id : NodeIdentity.Identity, secret : String,#

The shared-secret handshake on [fd], opening a control connection. Both peers run the same sequence: send hello, read the peer's, send a proof bound to the peer's nonce, read the peer's proof, authenticate (it must bind to the nonce WE issued, [my_nonce]). Returns the authenticated peer's identity, or why it was refused. Blocks with no timeout; handshake_auth is the general form.

match NetKernel.handshake(fd, me, secret, NetKernel.fresh_nonce()) do
  Ok(peer) -> println("joined by " ++ peer.name)
  Err(e) -> println("refused: " ++ e)
end
fnhandshake_addrhandshake_addr(fd : Int, my_id : NodeIdentity.Identity, secret : String, my_nonce : String,#

The shared-secret handshake of handshake_role, sending [addr] (this node's advertised listen address) in the hello, with the whole exchange bounded by [timeout_ms] (<= 0: no bound). Returns the peer's identity, the role it announced and the address it advertised ("" from a peer that sends none).

fnhandshake_authhandshake_auth(fd : Int, my_id : NodeIdentity.Identity, auth : ClusterAuth.Auth, my_nonce : String,#

The handshake, in either mode ([auth]): send a hello, read the peer's, check the modes agree, prove ourselves bound to the peer's nonce, check the peer's proof bound to ours. Shared-secret mode: an HMAC of the nonce under the secret (wire-compatible with pre-11a nodes). Certificate mode: the hello carries our certificate and an ephemeral X25519 key; we verify the peer's certificate (operator signature, expiry, revocation, name) and its ed25519 signature over our nonce and the transcript. When both sides support it, the connection's frames are sealed from then on (NetFrame.seal; mac in the result). [role] names the connection ("control"/"data"), [addr] is our advertised address ("" for none), [timeout_ms] bounds the whole exchange (<= 0: no bound).

fnhandshake_rolehandshake_role(fd : Int, my_id : NodeIdentity.Identity, secret : String,#

handshake, opening a connection of [role] ("control" or "data", Handshake.role_*); returns the peer's identity AND the role the peer announced, so an acceptor knows which of a peer's two connections it just took. A pre-split peer announces no role and reads as control. Reads are exact: nothing past the handshake's own two frames is consumed, so the connection is handed over with the peer's first real frame still unread.

fnmac_failure_limitmac_failure_limit() : Int do 3 end#

Frames that may fail their MAC on one connection before it is closed.

fnnote_raw_refusednote_raw_refused(_fd : Int) : ()#

Record a raw frame refused on a connection (raw_allowed false): counted in raw_refused().

fnoff_frame_rejectedoff_frame_rejected(id : Int) : () do Vault.drop(mac_hooks(), int_to_string(id)) end#

Remove the hook on_frame_rejected returned [id] for; an unknown id is ignored.

fnon_frame_rejectedon_frame_rejected(f : Int -> Int -> Unit) : Int#

Call f whenever a frame fails its MAC (the connection's reader calls it, in its own task). Returns an id for off_frame_rejected.

fnraw_allowedraw_allowed(fd : Int) : Bool do Vault.get_or(raw_table(), fk(fd), 1) == 1 end#

May raw frames (anything but session traffic) cross [fd]? False only on a certificate-mode connection where this node's or the peer's certificate lacks raw_send. Checked by NodeSend (Node.send) and NodeCall (RemoteCall); ClusterNode checks its own connections by certificate.

fnraw_refusedraw_refused() : Int do Vault.get_or(mac_counts(), "raw_refused_total", 0) end#

Raw frames refused on connections this process handshaked, in either direction.

fnrecv_frame_untilrecv_frame_until(fd : Int, deadline : Int) : Result(List(Int), String)#

One frame off [fd], exactly (never over-read, as recv_frame_exact, but never MAC-checked: for the handshake's own frames), before [deadline] (absolute unix ms). Err("net-kernel: handshake timed out") when the deadline passes first, or an error when the read fails or the connection closes.

fnsealedsealed(fd : Int) : Bool do Option.is_some(Vault.get(mac_table(), fk(fd))) end#

Are [fd]'s frames sealed?

fnsend_framesend_frame(fd : Int, payload : List(Int)) : Result((), String)#

Frame [payload] and write it to [fd]; sealed (NetFrame.seal, with the connection's next frame number) when the connection's handshake agreed a MAC key. Err with the socket's message when the write fails.

match NetKernel.send_frame(fd, NodeSend.encode_failed(seq, "no such actor")) do
  Ok(_) -> ()
  Err(e) -> println("write failed: " ++ e)
end
fnstr_to_bytesstr_to_bytes(s : String) : List(Int) do Bytes.to_list(Bytes.from_string(s)) end#

The bytes of [s] (its UTF-8 encoding) as a List(Int), each 0..255.

fnto_wireto_wire(payload : List(Int)) : String#

Frame [payload] (NetFrame.encode) and render it as the String the socket layer sends. Never sealed.

NetKernel.str_to_bytes(NetKernel.to_wire([104, 105]))   -- [0, 0, 0, 2, 104, 105]
fnwrite_framedwrite_framed(fd : Int, framed : Bytes) : Result((), String)#

Write an already-framed (length-prefixed, unsealed) frame, sealing it first when [fd] is sealed: NodeQueue frames its messages when they are queued, before it knows when they will be written.