RPC-like communication between yunos over the network. A local gobj subscribes to a remote service as if it were local.
Source: kernel/c/root-linux/src/c_ievent_cli.c, c_ievent_srv.c
C_IEVENT_CLI¶
Inter-event client — connects to a remote yuno and simulates its service as a local gobj. Handles identity-card exchange, authentication, and subscription management.
| Property | Value |
|---|---|
| States | ST_STOPPED, ST_DISCONNECTED, ST_WAIT_CONNECTED, ST_WAIT_IDENTITY_CARD, ST_SESSION, ST_SUBSCRIBED |
| Input events | EV_SEND_MESSAGE, EV_ON_MESSAGE, EV_ON_OPEN, EV_ON_CLOSE, EV_TIMEOUT, EV_DROP, EV_STOPPED |
| Output events | EV_ON_MESSAGE, EV_ON_OPEN, EV_ON_CLOSE |
Key attributes¶
| Attribute | Type | Description |
|---|---|---|
wanted_yuno_role | string | Role of the remote yuno to connect to. |
wanted_yuno_name | string | Name of the remote yuno. |
wanted_yuno_service | string | Service name on the remote yuno. |
url | string | Connection URL. |
jwt | string | JSON Web Token for authentication. |
timeout_idack | integer | Identity-card ack timeout in seconds. |
Stopping with remote subscriptions open¶
gobj_stop() closes the transport, but the FSM stays in ST_SESSION until
the close arrives, so EV_ON_CLOSE is still published. A subscription added
or withdrawn in that window is not sent to the peer. The peer’s
C_IEVENT_SRV drops every subscription of the channel when the channel
closes, and an added subscription is sent at the next open. Up to 7.25.4 the
__unsubscribing__ frame went down to the stopping transport, and
C_WEBSOCKET or C_TCP logged “Event NOT DEFINED in state”. Example:
gobj_stop_tree(priv->gobj_remote); // the transport starts to close
gobj_unsubscribe_event(priv->gobj_remote, EV_X, 0, gobj); // not sent, and not an errorC_IEVENT_SRV¶
Inter-event server — entry gate for authenticated service access. Manages incoming connections, routes inter-event messages to local services, and handles WebSocket upgrade.
| Property | Value |
|---|---|
| States | ST_STOPPED, ST_DISCONNECTED, ST_WAIT_IDENTITY_CARD, ST_SESSION |
| Input events | EV_ON_MESSAGE, EV_ON_OPEN, EV_ON_CLOSE, EV_TIMEOUT, EV_DROP, EV_STOPPED |
| Output events | EV_ON_MESSAGE, EV_ON_OPEN, EV_ON_CLOSE |
Key attributes¶
| Attribute | Type | Description |
|---|---|---|
__username__ | string | Authenticated username (read-only). |
__session_id__ | string | Session identifier (read-only). |
jwt_payload | json | Decoded JWT payload. |
client_yuno_role | string | Role of the connected client yuno. |
client_yuno_name | string | Name of the connected client yuno. |
this_service | string | Local service name this gate serves. |
authenticated | bool | Whether the connection is authenticated. |
max_subscriptions | integer | Subscriptions a peer may hold on the channel (default 5000, 0 no limit). See What a peer may hold. |
max_subscription_size | integer | Bytes, as compact json, of the __filter__, of the __global__ and of the routing back (__md_iev__) of a peer’s subscription, each one (default 16384, 0 no limit). |
max_pre_session_frame | integer | Bytes of a frame received before the session (default 65536, 0 no limit). See below. |
Lifecycle of a channel¶
The tree of a channel (C_CHANNEL → C_IEVENT_SRV → C_WEBSOCKET or
C_PROT_TCP4H → C_TCP) belongs to its gate, not to one connection.
gobj_start_tree() of the gate starts it. When the peer leaves, only its
C_TCP stops: the protocol gobj stays running and serves the next connection
that the channel accepts.
When the gate stops, all of the tree stops: each layer stops the one below
it, and C_IEVENT_SRV stops its protocol gobj since 7.25.5. Up to 7.25.4 a
gate stopped with a plain gobj_stop() (the way the yuno stops an
autostart service) left each C_WEBSOCKET or C_PROT_TCP4H running, and the
yuno exited with “Destroying a RUNNING gobj”. Only an owner that called
gobj_stop_tree() on the gate stopped it all.
A channel that is disabled and enabled again (the C_IOGATE commands
disable-channel and enable-channel) comes back whole: C_CHANNEL
restarts its tree, and C_IEVENT_SRV starts its protocol gobj in its start,
the mirror of its stop. The transport C_TCP is manual start: C_TCP_S
starts it for each connection it accepts.
ycommand -c 'command-yuno id=<id> service=__input_side__ command=disable-channel channel_name=^input-1$'
ycommand -c 'command-yuno id=<id> service=__input_side__ command=enable-channel channel_name=^input-1$'channel_name is a regular expression. It selects the channels whose name
it matches: ^input-1$ on a gate with input-1 and input-2 selects only
input-1. One that matches no channel selects nothing, and the command
answers with the header of the view only. (Up to 7.25.4 each of the six
commands looped for ever on the first channel that did not match, and
blocked the yuno.) A text that is not a valid regular expression is refused with -1,
for example channel_name=input-[ answers “<role^name>: channel_name is not
a valid regular expression: ‘input-[’” (up to 7.25.4: “regcomp() failed”).
Start and stop the gate as a pair. Either declare it "autostart": true in
the config and let the yuno start and stop it, or do both in the owner:
PRIVATE int mt_play(hgobj gobj)
{
PRIVATE_DATA *priv = gobj_priv_data(gobj);
priv->gobj_input_side = gobj_find_service("__input_side__", TRUE);
gobj_subscribe_event(priv->gobj_input_side, 0, 0, gobj);
gobj_start_tree(priv->gobj_input_side);
return 0;
}
PRIVATE int mt_pause(hgobj gobj)
{
PRIVATE_DATA *priv = gobj_priv_data(gobj);
gobj_stop_tree(priv->gobj_input_side);
return 0;
}Subscription authz¶
A peer subscribes to an event of a local service with a __subscribing__
message. C_IEVENT_SRV accepts it only for a public output event of a
service the channel may reach. Since 7.25.5, when the yuno sets
enable_subscription_authz (off by default) and the event is flagged
EVF_AUTHZ_SUBSCRIBE, the channel’s user also needs the publisher’s
permission aliased __subscribe_event__ — read for the EV_TREEDB_NODE_*
feed of a treedb and for the EV_TRANGER_RECORD_ADDED feed of a
C_TRANGER — or the global __subscribe_event__. A refused subscription is
logged (“No permission to subscribe event”) and not made; the channel stays
open. The refusal is logged once per service and event on a channel: a peer
that repeats it does not write a log line per frame. When the peer withdraws a
refused subscription later, that is logged at info level (“its subscription
was refused”). A withdrawal that matches no subscription of the channel, and
was not refused, is a warning (“UNSUBSCRIBING event matches no subscription
of this channel”), with the frame cut to 256 bytes. Both are the peer’s to
repeat, so each is written at most once per 10 s per channel, with the count
of the ones not written (suppressed). Up to 7.25.4 each frame wrote its
line, and the no-match warning the whole frame, as big as the peer made it.
Example: a yuno that enforces the treedb feed permission, in its config:
{
"yuno": {
"enable_subscription_authz": true
}
}Details, and how a gclass declares a guarded event:
YUNO_AUTH.md §4.6.
What a peer may put in a subscription¶
The kw of a __subscribing__ message can carry __config__, __global__
and __filter__, as a local gobj_subscribe_event() does. C_IEVENT_SRV
builds the subscription from those three, and keeps of them only what a peer
may set (since 7.25.5):
| Key | What is kept |
|---|---|
__filter__ | All of it. It decides only the peer’s own deliveries. |
__config__ | The keys in the list below. |
__global__ | The keys of the peer’s own: not one that starts with _ (the framework’s: __md_iev__, __md_yuno__, __service__, ...), and not gbuffer, the binary field of every kw. They come back to the peer in every event of this subscription, and to nobody else. |
__local__ | Nothing. The one __local__ of a remote subscription is the reference to the channel, set by C_IEVENT_SRV. |
| any other key | Nothing. |
What is dropped is logged as a warning, “SUBSCRIBING keys a peer may not
set, ignored”, at most once per 10 s per channel. A __filter__ or a
__global__ bigger than max_subscription_size refuses the subscription
(“SUBSCRIBING refused, bigger than max_subscription_size”, same pace).
To route every event of the subscription back to the peer, the gate adds
its own back-metadata to the stored __global__: __md_yuno__, and a
__md_iev__ built from the TOP record of the frame’s routing stack (the hop
of this peer, with the gate’s stamps), reversed. Nothing else of the frame’s
__md_iev__ is kept: no key of the peer’s own and no deeper hop. That
routing is measured against max_subscription_size too, after it is built:
a bigger one (a record with long strings) refuses the subscription,
“SUBSCRIBING refused, its routing is bigger than max_subscription_size”,
at most once per 10 s. Up to 7.25.4 the frame’s whole __md_iev__ was
copied, keys of the peer’s own included, and nothing of a subscription was
measured: a peer could store as much as a frame holds in each subscription,
with no cap on their number, and got it back with every event.
Up to 7.25.4 only __config__ was filtered, and the publish shared ONE kw
with every subscriber, so a peer’s subscription changed the event of every
subscriber after it: its __global__ forged keys of the event (a
topic_name, a node), its __local__ removed them, the filters of the
later subscribers were evaluated on the forged kw, and the back-metadata of
the gate (__md_iev__, with the peer’s user name and channel) reached a
local subscriber. A gbuffer in the peer’s __global__ was taken for a
pointer when the event was serialized back to it, and crashed the yuno. Now
gobj_publish_event() gives a subscription with a __local__ or a
__global__ a twin of the kw of its own
(gobj_publish_event()), and C_IEVENT_SRV changes a
kw that somebody else holds only on a copy.
Of __config__ the list of keys a peer may set has one key:
| Key | Meaning |
|---|---|
__first_shot__ | Read by the publisher in its mt_subscription_added(): false asks it not to send its current state when the subscription is made. |
Any other key is removed before the subscription is made, and logged as a warning (“SUBSCRIBING config keys a peer may not set, ignored”). Three of those keys change how the framework delivers the event, and a peer must never have them:
__hard_subscription__makes the subscription survive the close of the channel. The channel is static, so it went to the next user of it, who got the feed without asking and past the subscription authz.__own_event__stops the publish loop when the delivery to this subscription fails. With the channel closed every delivery fails, and every subscriber after it lost the event.__rename_event_name__delivers the event toC_IEVENT_SRVunder another name, one of its own inputs among them.
Up to 7.25.4 the peer’s __config__ went through whole. Also since 7.25.5,
the close of a channel removes every subscription it made with force, hard
or not.
Example: a SPA subscribes without the first shot, and that key arrives:
gobj_subscribe_event(gobj_remote, "EV_REALTIME_TRACK", {
__config__: {__first_shot__: false},
__filter__: {id: device_id}
}, gobj);A key that a publisher reads from a peer is added to the list in
c_ievent_srv.c (peer_subscription_config_keys), and documented here.
A peer withdraws a subscription with an __unsubscribing__ message that
repeats what it subscribed. The kw is filtered the same way, and compared
with what the peer SENT: the __global__ that C_IEVENT_SRV stores carries
its own back-metadata too, which the peer never repeats. Up to 7.25.4 it was
compared whole, and a subscription with a __global__ could not be withdrawn
until the channel closed.
Example: a C client tags the events of its subscription, and withdraws it:
json_t *kw = json_pack("{s:{s:s}, s:{s:s}}",
"__filter__", "topic_name", "devices",
"__global__", "tag", "devices_view" // comes back in each event
);
gobj_subscribe_event(gobj_remote, EV_REALTIME_TRACK, json_incref(kw), gobj);
gobj_unsubscribe_event(gobj_remote, EV_REALTIME_TRACK, kw, gobj); // the same kwWhat a peer may hold¶
Every subscription costs a scan of the publisher’s subscriptions when it is
made, and one more on every publish of its event. A peer could make them
without end: 20000 subscriptions of one peer blocked the event
loop for 80 s, and every later publish took 13 ms. So a channel holds at most
max_subscriptions of its peer. Beyond it a subscription is refused, logged
once (“SUBSCRIBING refused, the peer holds max_subscriptions”), and not
again until the peer is under the cap.
A subscription that repeats one the peer holds takes no room:
the SAME subscription (the same
__filter__,__global__,__local__and__config__) is left as it is. It is not made again: nomt_subscription_deleted()/mt_subscription_added(), and no second__first_shot__.one that overrides a subscription it holds (a match of
gobj_subscribe_event(): the new one has no__filter__, say) takes that one out and is made.
Both are logged as a warning, at most once per 10 s per channel
(“SUBSCRIBING repeated, the one held is kept”, “SUBSCRIBING overrides one
held, it is replaced”). A client does not send either: C_IEVENT_CLI
withdraws the subscription it replaces first. Up to 7.25.4 every repeated
frame went to gobj_subscribe_event(), which deleted the subscription, made
it again, and logged a warning with a stack trace and the whole kw.
A gate whose peers subscribe per device (two subscriptions per device, in the
SPAs of hidraulia) raises the cap in the kw of the C_IEVENT_SRV of its
channel tree:
{
"name": "input-(^^__range__^^)",
"gclass": "C_IEVENT_SRV",
"kw": {
"max_subscriptions": 20000
}
}What the gate stamps¶
The gate writes who sent a message into the kw it hands on, and overwrites
whatever the peer put there under the same key. In a command, a stats
request and an event, __username__ is the channel’s authenticated user
(set by C_AUTHZ), whatever the peer sent. In the routing stack of an event
(__md_iev__), __username__, input_channel and input_service are the
gate’s. The command parser’s authz check reads that __username__.
Example: a peer sends list-yunos with a __username__ of its own in the kw
of the command:
{"__username__": "admin"}The service gets "__username__": "bob" when the channel’s user is bob.
Up to 7.25.4 kw_set_dict_value() kept a key that
already existed, so the peer’s admin reached the service.
A frame the gate cannot route¶
Every frame of a session carries its routing: __md_iev__ with the ievent
stack, whose top record says where the frame comes from (src_yuno,
src_role, src_service, strings) and, if it says, where it goes
(dst_yuno, dst_role, dst_service, strings). C_IEVENT_CLI (C and JS)
pushes it on every request, and an answer copies the one of its request.
A well-formed routing (__msg_type__ is optional, a string when present):
"__md_iev__": {
"ievent_gate_stack": [
{
"src_yuno": "", "src_role": "ycommand", "src_service": "ycommand",
"dst_yuno": "", "dst_role": "yuneta_agent", "dst_service": "agent"
}
],
"__msg_type__": "__command__"
}The same frame with "src_role": 7, with an empty ievent_gate_stack, or
with no __md_iev__ at all, has no routing, and its channel is closed.
The gate reads what the peer sent with plain json calls, never with a
kw_get_*() reader that logs a wrong type with a stack. What it refuses,
and how:
| The frame | What the gate does | Log (per channel) |
|---|---|---|
| No routing, or a malformed one | Closes the channel, as for a frame that is not json. It cannot be answered, nor routed back. | “Frame without its routing (__md_iev__ ievent stack), channel closed”, WARNING, MSGSET_PROTOCOL, the kw capped to 256 bytes, at most once per 10 s. |
For another yuno (dst_role, dst_yuno) | Closes the channel. | “It’s not my role, channel closed” / “It’s not my name, channel closed”, WARNING, the routing capped. |
| For a service the channel may not reach | A command or a stats request gets a negative answer and the channel stays; a subscription, a withdrawal or an event closes it. | “event ignored, dst_service not authorized for this channel”, WARNING, MSGSET_AUTH, the kw capped, at most once per 10 s. |
| For a service that does not exist | The same. | “event ignored, service not found”, WARNING, same pace. |
| A subscription or a withdrawal of an event that is not public | Refused; the channel stays. | “SUBSCRIBING event ignored, not PUBLIC...”, WARNING, same pace. |
A command or a stats request that names no command (neither __command__ in the kw nor in its stack), or whose service is not a string | Answered with an error; the channel stays. | “Request without the command or stats it asks, refused”, WARNING, the kw capped, same pace. |
A command or a stats request whose service does not exist, or (stats) is one the channel may not reach | Answered with an error; the channel stays. | “Service not found” / “Not authorized to request stats of a different service”, WARNING, same pace. |
Each of these is the peer’s to repeat, frame after frame, so none writes a line per frame, none carries a stack trace, and none dumps the whole kw. Up to 7.25.4 a frame without routing logged an error with a stack and the whole kw (the lookup was verbose, “TODO check”) and was processed all the same; a command to a service the channel may not reach logged an error, the whole kw, the authorized services and the routing, for each frame; and a subscription to an event that is not public returned an error without closing the channel, which left it connected and deaf.
tests/c/c_ievent_srv_peer_subs.
An identity card the gate refuses¶
Before its session a peer may send two things: its identity card
(EV_IDENTITY_CARD) or EV_GOODBYE. The card carries the same routing as
any frame, and it must name this yuno and one of its services:
{
"jwt": "<token, or empty with the BFF cookie>",
"__md_iev__": {
"ievent_gate_stack": [
{
"src_yuno": "", "src_role": "ycommand", "src_service": "ycommand",
"dst_yuno": "", "dst_role": "yuneta_agent", "dst_service": "agent"
}
],
"__msg_type__": "__identity__"
}
}What is refused, each time, is the peer’s: the channel is closed, and the
gate writes ONE warning (MSGSET_PROTOCOL, peername, the kw capped to 256
bytes, no stack), with no credential in it. Before a session there is no
command table to ask which parameter is SDF_SECRET, so the NAME decides
(is_secret_name(), the one list of the SDK: passw, token, secret,
jwt, api_key, private_key... any case): at any depth of the kw the value
of such a key is shown as ******** (whatever its json type), and in a string
(a command line) the value of such a name=value too -- the kernel’s
json_mask_secrets(), the same rule as the ievents
traces. A command sent before the card:
kw {"jwt": "eyJ...", "password": "s3cr3t", "kw": {"passw": "x"}, "__command__": "help token=abc"}
log {"jwt":"********","password":"********","kw":{"passw":"********"},"__command__":"help token=********", ...}The ievents and ievents2 traces of C_IEVENT_CLI and C_IEVENT_SRV
(trace_inter_event(), trace_inter_event2()) mask the same way, in both
directions. A command (__command__, v6 or v7) is also masked by the command
table of its destination service when that service is in this yuno -- its
SDF_SECRET parameters, positional ones too -- as the commands trace does
(command_mask_secret_line()). Up to 7.25.20
ievents2 printed a command’s password in clear.
kw {"__command__": "set-user-pwd username=bob password=hunter2", "password": "hunter2"}
log {"event": "EV_MT_COMMAND", "kw": {"__command__": "set-user-pwd username=bob password=********", "password": "********"}}| The card | Log |
|---|---|
| No routing, or a malformed one | “Identity card without its routing (__md_iev__ ievent stack), refused” |
dst_role is not this yuno’s role | “Identity card refused, dst_role NOT MATCH”, with dst_role |
dst_yuno given and not this yuno’s name | “Identity card refused, dst_yuno NOT MATCH”, with dst_yuno |
Empty src_role | “Identity card refused, without yuno role” |
Empty src_service | “Identity card refused, without yuno service” |
dst_service is no service of this yuno | “Identity card refused, dst_service NOT FOUND in this yuno”, with dst_service |
jwt present and not a string | “Identity card refused, its jwt is not a string” |
Any event but the card or EV_GOODBYE | “Event before the identity card, channel closed”, with event |
A frame bigger than max_pre_session_frame | “Frame before the identity card too big, channel closed”, with size and max; the frame is not parsed, and its bytes are not dumped (unparsed they cannot be masked, and a card carries a jwt) |
A frame before the session is not even parsed when it is bigger than
max_pre_session_frame. An identity card is a few KB (its jwt the largest
part); parsed, a frame of [{},...] takes about 100 times its size (16 MB:
1.7 GB), so up to 7.25.21 the max block of a yuno (200 MB) let a peer nobody
had authenticated ask for some 20 GB. A gate whose peers send bigger cards
raises it:
{
"name": "input",
"gclass": "C_IEVENT_SRV",
"kw": {
"max_pre_session_frame": 1048576
}
}A card refused by the authentication is answered with a negative
EV_IDENTITY_CARD_ACK, logged by the authenticator, and the channel is
dropped at timeout_idgot. Up to 7.25.20 each refusal above was an error
with the whole kw (its jwt with it), followed by a second error, “event
UNKNOWN in not-session state”, with the whole kw again, credentials and
all; a jwt that was not a string went on to the authentication.
tests/c/c_ievent_srv_identity_card.