/* libink — brokerless D-Bus server library, born inside Finit * * Copyright (c) 2026 Joachim Wiberg * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ #ifndef LIBINK_LINK_H_ #define LIBINK_LINK_H_ #include #include #include #ifdef __cplusplus extern "C" { #endif typedef struct link_server link_server_t; typedef struct link_connection link_connection_t; typedef struct link_call link_call_t; typedef struct link_client link_client_t; /* D-Bus message type codes -- see link_reply_t.type. */ #define LINK_MSG_INVALID 0 #define LINK_MSG_METHOD_CALL 1 #define LINK_MSG_METHOD_RETURN 2 #define LINK_MSG_ERROR 3 #define LINK_MSG_SIGNAL 4 /* Writer is exposed so callers can stack-allocate one for marshalling * signal/reply bodies. Treat the fields as opaque; use link_writer_init * + the link_w_* helpers + link_writer_finish. Sized for typical D-Bus * messages -- the array stack supports up to 8 levels of nesting. */ #define LINK_WRITER_MAX_NESTING 8 typedef struct link_writer { uint8_t *buf; size_t cap; size_t off; int err; struct { size_t lenpos; size_t elemstart; } arrays[LINK_WRITER_MAX_NESTING]; size_t array_depth; } link_writer_t; /* Reader is exposed so callers can stack-allocate one for decoding * reply or signal bodies received from a peer. Treat fields as * opaque; use link_reader_init + the link_r_* helpers. */ typedef struct link_reader { const uint8_t *base; size_t off; size_t cap; int err; /* sticky */ } link_reader_t; /* View of an inbound message (method-return, error, or signal), * populated by link_client_call(_v) and link_client_wait(), and * returned by link_client_reply(). All pointers reference internal * client storage and are invalidated by the next call or wait on the * same client, or by link_client_close(). `body` is NULL iff * body_len==0; `error_name` is non-NULL only when type == LINK_MSG_ERROR; * `path`/`interface`/`member` are non-NULL on signals. */ typedef struct { uint8_t type; /* LINK_MSG_METHOD_RETURN, _ERROR, or _SIGNAL */ const char *signature; const char *error_name; const char *path; const char *interface; const char *member; const uint8_t *body; size_t body_len; } link_reply_t; /* ---------- debug tracing ---------- */ /* Receives one line per connection, method call, and authorization * decision. `func` is the libink function that emitted it, so a host * can format it the way it formats its own traces. * * With no logger installed, the default, a trace point costs one NULL * test. With one installed the message is formatted before the host * sees it, so a host that only wants tracing sometimes should install * and remove the callback rather than discard by level. */ typedef void (*link_log_cb_t)(void *userdata, const char *func, const char *msg); void link_set_logger(link_log_cb_t cb, void *userdata); /* ---------- caller identity on a broker connection ---------- */ /* Handle for a call parked while its caller is identified. Opaque, * and safe to hold: it encodes a slot and a generation, so resolving * a stale handle is a no-op rather than a use-after-free. */ typedef uint64_t link_authz_t; /* "Nobody asked yet", distinct from any real uid. link_call_uid() * returns this on a broker connection until something forces the * question, which today only a LINK_METHOD_PRIVILEGED method does. */ #define LINK_UID_UNKNOWN ((uid_t)-1) /* A broker sets SENDER to a unique name, ":1.", so this is very * generous. Callers that key anything on a sender must reject longer * names rather than truncate: two senders sharing a truncated key * would share an identity. */ #define LINK_SENDER_MAX 64 /* Answer "which uid is `sender`?" for a privileged call arriving on a * broker connection, where SO_PEERCRED describes the broker and not * the caller. * * Return 0 with *uid set when the answer is already known, 1 to answer * later by calling link_uid_resolved() with `tok`, or -1 when it * cannot be determined, which fails the call closed. Returning 1 * without ever calling link_uid_resolved() leaks the slot and leaves * the caller without a reply, so always answer. */ typedef int (*link_uid_resolver_t)(link_connection_t *conn, const char *sender, link_authz_t tok, uid_t *uid, void *userdata); void link_server_set_uid_resolver(link_server_t *server, link_uid_resolver_t cb, void *userdata); /* May this caller invoke a LINK_METHOD_PRIVILEGED method? Return * non-zero to allow. `groups` is the caller's group set (primary gid * first), captured from the kernel with SO_PEERGROUPS so the embedder * can decide membership without an NSS lookup that would block PID 1; * `ngroups` is 0 when the set is unknown, e.g. a call arriving over a * broker, where only `uid` is available. Who counts as privileged is * the embedder's policy; with no authorizer installed only uid 0 may. */ typedef int (*link_authorizer_t)(uid_t uid, const gid_t *groups, int ngroups, void *userdata); void link_server_set_authorizer(link_server_t *server, link_authorizer_t cb, void *userdata); /* Complete a deferred resolve and resume the parked call. Pass * (uid_t)-1 to say the caller could not be identified, which denies * it. Resolving a handle twice, or one whose connection has since * closed, does nothing. The connection is the one the resolver was * asked about; a reply callback is handed it as its first argument. */ void link_uid_resolved(link_connection_t *conn, link_authz_t tok, uid_t uid); /* ---------- handler-deferred replies ---------- */ /* A handler that cannot answer yet parks the call and returns 0 * without replying; the framework sends nothing. Resume re-runs the * handler with link_call_resumed() reading true, and this time it * must reply -- a resumed call cannot park again. Parked calls that * are never resumed are expired by link_connection_expire() with a * TimedOut error. Room is limited (four per connection); a failed * park means answer now. */ int link_call_park (link_call_t *call, link_authz_t *tok); void link_call_resume (link_connection_t *conn, link_authz_t tok); int link_call_resumed(const link_call_t *call); /* Called with the reply to an outbound link_connection_call(). `reply` * is NULL if the connection dropped before one arrived. */ typedef void (*link_reply_cb_t)(link_connection_t *conn, const link_reply_t *reply, void *userdata); /* Issue a method call on an established connection and invoke `cb` * when the reply lands. Unlike link_client_call() this never blocks: * the reply is picked up by the normal read loop. Argument marshalling * matches link_client_call_v(). */ int link_connection_call(link_connection_t *conn, const char *destination, const char *path, const char *interface, const char *member, link_reply_cb_t cb, void *userdata, const char *signature, ...); /* Nothing in libink runs a clock, it has no event loop, so calls that * go unanswered in either direction are the embedder's to time out. * This drops anything held longer than `age_ms` on one connection: a * park whose resolver never answered, which leaves its caller * org.freedesktop.DBus.Error.TimedOut, and a call whose reply never * came, whose callback runs once with a NULL reply exactly as a * dropped connection would. * * Returns how many are still outstanding, so a sweep can stop * rearming once nothing is left. */ int link_connection_expire(link_connection_t *conn, unsigned int age_ms); /* ---------- server / connection lifecycle ---------- */ /* Bind a listening socket at `path` with file mode `mode`, e.g. 0660 * to keep it to root and one group. The mode is applied at bind(), * so the socket is never briefly more permissive than asked; setting * the owning group afterwards is the caller's job. */ int link_server_new (link_server_t **server, const char *path, mode_t mode); void link_server_free (link_server_t *server); int link_server_get_fd(const link_server_t *server); int link_server_accept(link_server_t *server, link_connection_t **conn); /* Insert an externally-authenticated fd into the server's connection * set. Used to integrate an outbound peer (e.g. a client-side * handshake against an external dbus-daemon) so the same dispatch + * signal-fan-out machinery covers it. `peer_uid` becomes what * privileged-method checks see; pass (uid_t)-1 to make all * LINK_METHOD_PRIVILEGED methods reject by default. * * On success the connection takes ownership of `fd`. On any failure * `fd` is closed before the function returns NULL, so callers never * have to track partial state. * * LINK_ATTACH_BROKER says the peer is a message bus rather than an * ordinary client. A broker subscribes on behalf of its own clients * and never sends us AddMatch, so signals go to it unconditionally * instead of being filtered by this connection's match rules. */ #define LINK_ATTACH_BROKER 0x01 link_connection_t *link_server_attach(link_server_t *server, int fd, uid_t peer_uid, unsigned int attach_flags); int link_connection_get_fd (const link_connection_t *conn); uid_t link_connection_get_uid (const link_connection_t *conn); int link_connection_process (link_connection_t *conn); void link_connection_close (link_connection_t *conn); /* ---------- object registration ---------- */ typedef int (*link_method_fn)(link_call_t *call, void *userdata); /* Method flags for link_method_t.flags */ #define LINK_METHOD_PRIVILEGED (1u << 0) /* peer must be uid 0 (root) */ typedef struct { const char *name; /* member name */ const char *in_sig; /* input signature (D-Bus, e.g. "" or "s") */ const char *out_sig; /* output signature */ unsigned flags; /* OR of LINK_METHOD_* */ link_method_fn handler; } link_method_t; /* A read-only property descriptor. Set via the Properties.Set side * is not yet implemented; only Get and GetAll are. The framework * emits the variant signature from `sig`; the getter writes only the * bare value into the provided writer (link_w_string for "s", * link_w_u32 for "u", ...), so the declared type is the single * source of truth. */ typedef int (*link_property_getter_fn)(link_writer_t *w, void *userdata); typedef struct { const char *name; /* property name */ const char *sig; /* D-Bus signature, e.g. "s" */ link_property_getter_fn getter; } link_property_t; /* * Declares a signal for introspection only; emission is unchecked, * see link_connection_emit_signal(). */ typedef struct { const char *name; /* member name */ const char *sig; /* D-Bus signature, e.g. "sss" */ } link_signal_t; typedef struct { const char *interface; /* e.g. "org.finit.Manager1" */ const link_method_t *methods; /* terminated by {NULL, ...}, or NULL */ const link_property_t *properties; /* terminated by {NULL, ...}, or NULL */ const link_signal_t *signals; /* terminated by {NULL, ...}, or NULL */ } link_vtable_t; /* Register one (interface, methods) at `path`. Calling repeatedly * with the same path and different vtables adds more interfaces at * that object. The vtable pointer must outlive the server (typically * a static table). */ int link_server_add_object(link_server_t *server, const char *path, const link_vtable_t *vt, void *userdata); /* Remove every vtable registered at `path` and free the object. * Returns 0 if the object existed, -1 (errno=ENOENT) otherwise. */ int link_server_remove_object(link_server_t *server, const char *path); /* ---------- call accessors ---------- */ const char *link_call_path (const link_call_t *call); const char *link_call_interface(const link_call_t *call); const char *link_call_member (const link_call_t *call); uid_t link_call_uid (const link_call_t *call); link_connection_t *link_call_connection(const link_call_t *call); /* ---------- reading method-call arguments ---------- * * Cursor starts at the beginning of the request body. Each * function returns 0 on success and advances the cursor; on * failure it returns -1 and leaves the cursor in an error state * (subsequent reads also fail). Strings reference the * connection's rx buffer and are valid for the duration of the * method handler. */ int link_call_read_byte (link_call_t *call, uint8_t *out); int link_call_read_bool (link_call_t *call, int *out); int link_call_read_u32 (link_call_t *call, uint32_t *out); int link_call_read_u64 (link_call_t *call, uint64_t *out); int link_call_read_string(link_call_t *call, const char **out); /* "s" */ int link_call_read_path (link_call_t *call, const char **out); /* "o" */ /* ---------- reply construction ---------- */ /* Get the writer for the reply body, write args into it, return 0 * from the handler. Dispatch finalizes and sends the reply with * the out_sig declared on the vtable. May be called once per * call. */ link_writer_t *link_call_reply(link_call_t *call); /* Send a D-Bus error reply. `name` must be a valid D-Bus error * name (e.g. "org.freedesktop.DBus.Error.UnknownMethod"); `message` * may be NULL. */ int link_call_reply_error(link_call_t *call, const char *name, const char *message); /* ---------- signal emission ---------- * * Send a signal to a single peer if its AddMatch rules accept it. * Callers marshal the body separately and pass the resulting bytes. * Returns 0 on success (or "filtered out, nothing sent"), -1 with * errno set on failure: EMSGSIZE and EINVAL mean nothing hit the * wire and the connection is still usable; anything else is a * transport failure that may have left a partial frame -- the * caller must drop the peer. */ int link_connection_emit_signal(link_connection_t *conn, const char *path, const char *interface, const char *member, const char *signature, const uint8_t *body, size_t body_len); /* ---------- client (outgoing method calls) ---------- * * Connect, authenticate as the current effective uid, send BEGIN. * Returns NULL on any failure (caller can fall back to another * transport if it has one). */ link_client_t *link_client_open(const char *path); /* As link_client_open but applies SO_SNDTIMEO + SO_RCVTIMEO before * the connect/AUTH handshake. After link_server_attach flips the fd * to non-blocking the timeout is silently inert; it only protects * the synchronous open path against a hung peer. timeout_ms == 0 * disables the budget (same behaviour as link_client_open). */ link_client_t *link_client_open_timeout(const char *path, int timeout_ms); void link_client_close(link_client_t *c); /* Address subsequent calls on `c` to a well-known name. Needed when * a broker routes the message, e.g. "org.freedesktop.DBus" to reach * the bus driver itself; a brokerless link has a single peer and * needs no destination, which is the default. `destination` is not * copied, so it must outlive the client. */ void link_client_set_destination(link_client_t *c, const char *destination); /* Detach the authenticated socket from the client and return the raw * fd; subsequent link_client_close on `c` is invalid because the * structure has already been freed. Used by callers (e.g. system-bus * integration) that want to promote an outbound client connection * into a server-attached peer via link_server_attach(). */ int link_client_steal_fd(link_client_t *c); /* Status codes returned by link_client_call(_v). */ #define LINK_CALL_OK 0 /* method-return received */ #define LINK_CALL_ERROR 1 /* server replied with an error */ #define LINK_CALL_FAIL (-1) /* transport, parse, or invalid-arg failure */ /* Send a METHOD_CALL and read the reply synchronously. * * `signature` and `body`/`body_len` describe the outgoing body -- * marshal it yourself with link_writer_init + the link_w_* helpers * + link_writer_finish. Pass signature=NULL and body=NULL for * methods that take no arguments. * * After the call, inspect the reply via link_client_reply() -- it * exposes the body bytes (for callers that want to decode them with * link_reader_init + link_r_*) and the error name on LINK_CALL_ERROR. * The reply view is invalidated by the next call on the same client * or by link_client_close(). */ int link_client_call(link_client_t *c, const char *obj_path, const char *interface, const char *member, const char *signature, const uint8_t *body, size_t body_len); /* Convenience wrapper that marshals the outgoing body from varargs * matching `signature`. Supported type codes (one per arg): * 'y' -> int (promoted uint8_t) * 'b' -> int (0/non-zero) * 'u' -> uint32_t * 's' -> const char * * 'o' -> const char * (object path) * * Pass signature=NULL or "" for void calls. Return value matches * link_client_call; an unsupported type code returns LINK_CALL_FAIL * with no message sent. */ int link_client_call_v(link_client_t *c, const char *obj_path, const char *interface, const char *member, const char *signature, ...); const link_reply_t *link_client_reply(link_client_t *c); /* Convenience accessors for the common case where a reply carries * exactly one string ("s" or "o") or one u32 ("u"). They wrap the * link_reader_init + link_r_* pattern; on success return 0 and * populate *out, on parse failure or missing body return -1. Use * link_client_reply + link_reader_init directly for richer payloads. */ int link_reply_get_string(const link_reply_t *r, const char **out); int link_reply_get_u32 (const link_reply_t *r, uint32_t *out); /* Wait up to `timeout_ms` milliseconds for the next inbound message * (typically a SIGNAL delivered after an AddMatch subscription), and * populate the same view returned by link_client_reply(). * timeout_ms < 0 : block forever * timeout_ms == 0 : non-blocking (returns 1 immediately if no data) * timeout_ms > 0 : wait that long * Returns 0 on success, 1 on timeout, -1 on transport/parse error. * * Note: the timeout gates only the wait for the first byte of the * next frame. Once data starts arriving the rest of the message is * read blockingly; callers that need a hard upper bound should pass * a positive timeout AND have a watchdog at a higher level. */ int link_client_wait(link_client_t *c, int timeout_ms); /* ---------- standalone writer ---------- * * For marshalling bodies outside a method-call handler (signals, * pre-computed replies). Initialise on a caller-owned buffer, * write args via link_w_*, then call link_writer_finish which * returns the body length or -1 on overflow. */ void link_writer_init (link_writer_t *w, uint8_t *buf, size_t cap); ssize_t link_writer_finish(link_writer_t *w); /* ---------- writer (mirrors the internal marshaller) ---------- */ void link_w_byte (link_writer_t *w, uint8_t v); void link_w_bool (link_writer_t *w, int v); void link_w_u32 (link_writer_t *w, uint32_t v); void link_w_u64 (link_writer_t *w, uint64_t v); /* "t" */ void link_w_string (link_writer_t *w, const char *s); /* "s" */ void link_w_path (link_writer_t *w, const char *s); /* "o" */ void link_w_variant_string(link_writer_t *w, const char *s); /* "v" containing "s" */ void link_w_array_begin (link_writer_t *w, char element_sig); void link_w_array_end (link_writer_t *w); void link_w_struct_begin(link_writer_t *w); void link_w_struct_end (link_writer_t *w); /* ---------- standalone reader ---------- * * For decoding bodies received off the wire (reply or signal). * Initialise on the body pointer + length, read via link_r_*, * check link_r_done() to confirm everything was consumed. */ void link_reader_init(link_reader_t *r, const uint8_t *body, size_t len); int link_r_byte (link_reader_t *r, uint8_t *out); int link_r_bool (link_reader_t *r, int *out); int link_r_u32 (link_reader_t *r, uint32_t *out); int link_r_u64 (link_reader_t *r, uint64_t *out); int link_r_string (link_reader_t *r, const char **out); /* "s" */ int link_r_path (link_reader_t *r, const char **out); /* "o" */ int link_r_variant_begin (link_reader_t *r, char *type); /* sig header, cursor at value */ int link_r_skip_basic (link_reader_t *r, char type); /* skip one basic value */ int link_r_variant_string(link_reader_t *r, const char **out); /* "v" containing "s" */ int link_r_align (link_reader_t *r, size_t n); /* skip to next n-byte boundary */ int link_r_done (const link_reader_t *r); /* Begin reading an "a" array. On success returns 0 and sets * *out_end to the absolute reader offset at which the array ends; * caller loops while link_r_pos < *out_end. For dict-entry arrays * ("a{T}") call link_r_align(r, 8) at the top of each iteration -- * the element-alignment skip from the array prefix only covers the * first entry. */ int link_r_array_begin(link_reader_t *r, size_t *out_end); /* Byte offset of the next read inside the original body buffer. * Use together with the *out_end returned by link_r_array_begin to * walk the elements of an "a" payload. */ size_t link_r_pos (const link_reader_t *r); #ifdef __cplusplus } #endif #endif /* LIBINK_LINK_H_ */