enroll: add PEX sub-protocol to support enrolling new nodes into a network
authorFelix Fietkau <nbd@nbd.name>
Sat, 21 Dec 2024 19:54:28 +0000 (20:54 +0100)
committerFelix Fietkau <nbd@nbd.name>
Sun, 26 Jan 2025 10:14:35 +0000 (11:14 +0100)
This protocol does a full DH exchange and allows both sides to confirm
the result based on a session id hash derived from the DH session key.
It exchanges the public auth_key for a network and a newly generated keypair
for the added node

Signed-off-by: Felix Fietkau <nbd@nbd.name>
CMakeLists.txt
enroll.c [new file with mode: 0644]
enroll.h [new file with mode: 0644]
network.c
pex-msg.c
pex-msg.h
pex.c
ubus.c
ubus.h

index ec31711dc7ee108c85eb69f677310e969ddf353d..1da8366d78e05df1637df129fdc6cc8ca3e0d63d 100644 (file)
@@ -4,7 +4,8 @@ PROJECT(unetd C)
 
 
 SET(SOURCES
-       main.c network.c host.c service.c pex.c pex-stun.c
+       main.c network.c host.c service.c
+       pex.c pex-stun.c
        wg.c wg-user.c
 )
 
@@ -35,7 +36,7 @@ ELSE()
 ENDIF()
 
 IF(UBUS_SUPPORT)
-  SET(SOURCES ${SOURCES} ubus.c)
+  SET(SOURCES ${SOURCES} ubus.c enroll.c)
   SET(DHT_SOURCES ${DHT_SOURCES} udht-ubus.c)
   ADD_DEFINITIONS(-DUBUS_SUPPORT=1)
   FIND_LIBRARY(ubus ubus)
diff --git a/enroll.c b/enroll.c
new file mode 100644 (file)
index 0000000..a8ec706
--- /dev/null
+++ b/enroll.c
@@ -0,0 +1,851 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2024 Felix Fietkau <nbd@nbd.name>
+ */
+#define _GNU_SOURCE
+#include <arpa/inet.h>
+#include "enroll.h"
+#include "curve25519.h"
+#include "sha512.h"
+#include "chacha20.h"
+#include "unetd.h"
+#include <libubus.h>
+#include <libubox/blobmsg_json.h>
+
+#define CHAINING_KEY_STR "unetd enroll"
+
+static struct enroll_state *state;
+static uint8_t chaining_hash[SHA512_HASH_SIZE];
+static struct blob_buf b;
+
+enum msg_id {
+       MSG_ID_C_DISCOVERY,
+       MSG_ID_S_DISCOVERY,
+       MSG_ID_C_ANNOUNCE,
+       MSG_ID_S_CONFIRM,
+       MSG_ID_C_ACCEPT,
+       __MSG_ID_MAX
+};
+
+static const char * const msg_op_names[] = {
+       [MSG_ID_C_DISCOVERY] = "discovery",
+       [MSG_ID_C_ANNOUNCE] = "announce",
+       [MSG_ID_C_ACCEPT] = "accept",
+       [MSG_ID_S_DISCOVERY] = "discovery",
+       [MSG_ID_S_CONFIRM] = "confirm",
+};
+
+struct enroll_msg_hdr {
+       uint8_t op;
+       uint8_t pubkey[CURVE25519_KEY_SIZE];
+       uint8_t hmac[ENROLL_HASH_SIZE];
+       uint8_t nonce[8];
+};
+
+struct enroll_msg_key_data {
+       struct {
+               uint8_t session_key[CURVE25519_KEY_SIZE];
+               uint8_t op;
+       } state;
+       uint8_t data_key[SHA512_HASH_SIZE];
+       uint8_t session_id[ENROLL_SESSION_ID_LEN];
+};
+
+enum {
+       ENROLL_ATTR_KEY,
+       ENROLL_ATTR_HASH,
+       ENROLL_ATTR_SECRET,
+       ENROLL_ATTR_INFO,
+       __ENROLL_ATTR_MAX
+};
+
+static const struct blobmsg_policy enroll_policy[__ENROLL_ATTR_MAX] = {
+       [ENROLL_ATTR_KEY] = { "key", BLOBMSG_TYPE_STRING },
+       [ENROLL_ATTR_HASH] = { "hash", BLOBMSG_TYPE_STRING },
+       [ENROLL_ATTR_SECRET] = { "secret", BLOBMSG_TYPE_STRING },
+       [ENROLL_ATTR_INFO] = { "info", BLOBMSG_TYPE_TABLE },
+};
+
+struct blob_attr *tb[__ENROLL_ATTR_MAX];
+
+static void
+blobmsg_add_key(struct blob_buf *buf, const char *name, const uint8_t *key)
+{
+       size_t keystr_len = B64_ENCODE_LEN(CURVE25519_KEY_SIZE);
+       char *str;
+
+       str = blobmsg_alloc_string_buffer(buf, name, keystr_len);
+       b64_encode(key, CURVE25519_KEY_SIZE, str, keystr_len);
+       blobmsg_add_string_buffer(buf);
+}
+
+static int enroll_peer_cmp(const void *k1, const void *k2, void *ptr)
+{
+       return memcmp(k1, k2, CURVE25519_KEY_SIZE);
+}
+
+static void enroll_global_init(void)
+{
+       static bool init_done = false;
+       struct sha512_state s;
+
+       if (init_done)
+               return;
+
+       sha512_init(&s);
+       sha512_add(&s, CHAINING_KEY_STR, sizeof(CHAINING_KEY_STR));
+       sha512_final(&s, chaining_hash);
+       init_done = true;
+}
+
+static inline bool is_server_msg(uint8_t op)
+{
+       switch (op) {
+       case MSG_ID_S_DISCOVERY:
+       case MSG_ID_S_CONFIRM:
+               return true;
+       default:
+               return false;
+       }
+}
+
+static bool
+enroll_parse_b64(uint8_t *dest, struct blob_attr *attr, size_t len)
+{
+       if (!attr)
+               return false;
+
+       return b64_decode(blobmsg_get_string(attr), dest, len) == len;
+}
+
+static bool
+enroll_parse_key(uint8_t *key)
+{
+       return enroll_parse_b64(key, tb[ENROLL_ATTR_KEY], CURVE25519_KEY_SIZE);
+}
+
+static bool
+enroll_parse_hash(uint8_t *hash)
+{
+       return enroll_parse_b64(hash, tb[ENROLL_ATTR_HASH], ENROLL_HASH_SIZE);
+}
+
+static bool
+enroll_parse_secret(uint8_t *hash)
+{
+       return enroll_parse_b64(hash, tb[ENROLL_ATTR_SECRET], ENROLL_HASH_SIZE);
+}
+
+static void
+enroll_add_b64(const char *name, const void *data, size_t len)
+{
+       size_t str_len = B64_ENCODE_LEN(len);
+       char *str;
+
+       str = blobmsg_alloc_string_buffer(&b, name, str_len);
+       b64_encode(data, len, str, str_len);
+       blobmsg_add_string_buffer(&b);
+}
+
+static void
+blobmsg_add_ipaddr(struct blob_buf *buf, const char *name, const void *addr)
+{
+       const struct sockaddr *sa = addr;
+       int af = sa->sa_family;
+       int addr_len;
+       char *str;
+
+       addr = network_endpoint_addr((void *)addr, &addr_len);
+       str = blobmsg_alloc_string_buffer(buf, name, INET6_ADDRSTRLEN);
+       inet_ntop(af, addr, str, INET6_ADDRSTRLEN);
+       blobmsg_add_string_buffer(buf);
+}
+
+static void
+enroll_add_hash(const uint8_t *hash)
+{
+       enroll_add_b64("hash", hash, ENROLL_HASH_SIZE);
+}
+
+static void
+enroll_add_secret(const uint8_t *hash)
+{
+       enroll_add_b64("secret", hash, ENROLL_HASH_SIZE);
+}
+
+static void
+enroll_add_key(const uint8_t *key)
+{
+       enroll_add_b64("key", key, CURVE25519_KEY_SIZE);
+}
+
+static void
+enroll_add_info(struct blob_attr *attr)
+{
+       const void *data;
+       size_t len;
+
+       if (attr) {
+               data = blobmsg_data(attr);
+               len = blobmsg_data_len(attr);
+       } else {
+               data = "";
+               len = 0;
+       }
+
+       blobmsg_add_field(&b, BLOBMSG_TYPE_TABLE, "info", data, len);
+}
+
+static void
+enroll_calc_session_keys(struct enroll_msg_key_data *key_data,
+                        uint8_t op, const uint8_t *pubkey)
+{
+       uint8_t pubkeys[CURVE25519_KEY_SIZE];
+       uint8_t hmac[SHA512_HASH_SIZE];
+
+       key_data->state.op = op;
+       curve25519(key_data->state.session_key, state->privkey, pubkey);
+
+       memcpy(pubkeys, pubkey, CURVE25519_KEY_SIZE);
+       for (size_t i = 0; i < CURVE25519_KEY_SIZE; i++)
+               pubkeys[i] ^= state->pubkey[i];
+       hmac_sha512(hmac, chaining_hash, sizeof(chaining_hash),
+                   pubkeys, sizeof(pubkeys));
+       hmac_sha512(hmac, hmac, sizeof(hmac),
+                   &key_data->state, sizeof(key_data->state));
+       memcpy(key_data->data_key, hmac, sizeof(key_data->data_key));
+
+       hmac_sha512(hmac, chaining_hash, sizeof(chaining_hash),
+                   &key_data->state.session_key, sizeof(key_data->state.session_key));
+       memcpy(key_data->session_id, hmac, sizeof(key_data->session_id));
+}
+
+static void
+enroll_peer_free(struct enroll_peer *peer)
+{
+       avl_delete(&state->peers, &peer->node);
+       free(peer->enroll_meta);
+       free(peer);
+}
+
+static void
+enroll_msg_send(uint8_t op, struct blob_attr *msg,
+               const uint8_t *pubkey, struct sockaddr_in6 *addr)
+{
+       struct enroll_msg_key_data key_data = {};
+       uint8_t hmac[SHA512_HASH_SIZE];
+       struct enroll_msg_hdr *hdr;
+       static struct blob_buf b;
+       uint64_t nonce;
+       size_t len = 0;
+       void *data;
+       char *str;
+
+       __pex_msg_init(state->pubkey, PEX_MSG_ENROLL);
+       pex_msg_append(sizeof(struct pex_ext_hdr));
+
+       hdr = pex_msg_append(sizeof(*hdr));
+       hdr->op = op;
+       memcpy(hdr->pubkey, state->pubkey, sizeof(hdr->pubkey));
+
+       if (!msg)
+               goto out;
+
+       len = blobmsg_data_len(msg);
+       data = pex_msg_append(len);
+       if (!data) {
+               D("message too large");
+               return;
+       }
+
+       memcpy(data, blobmsg_data(msg), len);
+
+       enroll_calc_session_keys(&key_data, op, pubkey);
+
+       nonce = cpu_to_be64(state->nonce);
+       memcpy(hdr->nonce, &nonce, sizeof(hdr->nonce));
+       state->nonce++;
+
+       chacha20_encrypt_msg(data, len, hdr->nonce, key_data.data_key);
+
+       hmac_sha512(hmac, key_data.data_key, sizeof(key_data.data_key), data, len);
+       memcpy(hdr->hmac, hmac, sizeof(hdr->hmac));
+
+out:
+       blob_buf_init(&b, 0);
+       blobmsg_add_ipaddr(&b, "address", addr);
+       blobmsg_add_key(&b, "id", hdr->pubkey);
+       if (msg)
+               blobmsg_add_field(&b, BLOBMSG_TYPE_TABLE, "data",
+                                 blobmsg_data(msg), blobmsg_data_len(msg));
+       str = blobmsg_format_json(b.head, true);
+       D("tx enroll %s %s: %s", is_server_msg(op) ? "server" : "client", msg_op_names[op], str);
+       free(str);
+
+       if (__pex_msg_send(-1, addr, NULL, 0) < 0)
+               D("enroll: pex_msg_send failed: %s", strerror(errno));
+}
+
+static struct enroll_peer *
+enroll_get_peer(const struct enroll_msg_hdr *hdr,
+               struct sockaddr_in6 *addr,
+               struct enroll_msg_key_data *key_data,
+               struct blob_attr *meta)
+{
+       struct enroll_peer *peer;
+       uint64_t nonce;
+
+       memcpy(&nonce, hdr->nonce, sizeof(nonce));
+       nonce = be64_to_cpu(nonce);
+
+       peer = avl_find_element(&state->peers, hdr->pubkey, peer, node);
+       if (peer) {
+               if (key_data && nonce <= peer->nonce) {
+                       D("replay detected");
+                       return NULL;
+               }
+
+               goto out;
+       }
+
+       if (!meta || !key_data || state->peers.count >= ENROLL_MAX_PEERS)
+               return NULL;
+
+       peer = calloc(1, sizeof(*peer) + blob_pad_len(meta));
+       peer->node.key = peer->pubkey;
+       memcpy(peer->pubkey, hdr->pubkey, sizeof(peer->pubkey));
+       memcpy(peer->session_id, key_data->session_id, sizeof(peer->session_id));
+       memcpy(peer->session_key, key_data->state.session_key, sizeof(peer->session_key));
+       memcpy(peer->meta, meta, blob_pad_len(meta));
+       avl_insert(&state->peers, &peer->node);
+
+out:
+       peer->addr = *addr;
+       peer->nonce = nonce;
+
+       return peer;
+}
+
+static void
+enroll_peer_notify(struct enroll_peer *peer)
+{
+       blob_buf_init(&b, 0);
+       enroll_peer_info(&b, peer);
+       unetd_ubus_notify("enroll_peer_update", b.head);
+}
+
+static void
+enroll_peer_derive_local_key(struct enroll_peer *peer, uint8_t *key)
+{
+       struct sha512_state s;
+
+       sha512_init(&s);
+       sha512_add(&s, CHAINING_KEY_STR, sizeof(CHAINING_KEY_STR));
+       sha512_add(&s, state->privkey, sizeof(state->privkey));
+       sha512_add(&s, peer->pubkey, sizeof(peer->pubkey));
+       memcpy(key, sha512_final_get(&s), CURVE25519_KEY_SIZE);
+       curve25519_clamp_secret(key);
+}
+
+static void
+enroll_send_client_discovery(struct sockaddr_in6 *addr)
+{
+       enroll_msg_send(MSG_ID_C_DISCOVERY, NULL, NULL, addr);
+}
+
+static void
+enroll_send_server_discovery(const uint8_t *pubkey, struct sockaddr_in6 *addr,
+                            struct blob_attr *meta)
+{
+       blob_buf_init(&b, 0);
+       enroll_add_info(meta);
+       enroll_msg_send(MSG_ID_S_DISCOVERY, b.head, pubkey, addr);
+}
+
+static void
+enroll_recv_client_announce(const struct enroll_msg_hdr *hdr,
+                        struct enroll_msg_key_data *key_data,
+                        struct sockaddr_in6 *addr)
+{
+       uint8_t pubkey[CURVE25519_KEY_SIZE];
+       uint8_t key_dh[CURVE25519_KEY_SIZE];
+       uint8_t hmac[SHA512_HASH_SIZE];
+       uint8_t msg_hash[ENROLL_HASH_SIZE];
+       uint8_t secret_hash[ENROLL_HASH_SIZE];
+       struct enroll_peer *peer;
+       bool valid_secret = false;
+
+       if (!tb[ENROLL_ATTR_INFO] ||
+           !enroll_parse_key(pubkey) || !enroll_parse_hash(msg_hash)) {
+               D("Invalid client announce message");
+               return;
+       }
+
+       curve25519(key_dh, state->privkey, pubkey);
+       hmac_sha512(hmac, chaining_hash, sizeof(chaining_hash),
+                   key_dh, sizeof(key_dh));
+       if (memcmp(hmac, msg_hash, sizeof(msg_hash)) != 0) {
+               D("Public key DH HMAC does not match");
+               return;
+       }
+
+       if (state->has_secret && enroll_parse_secret(secret_hash)) {
+               hmac_sha512(hmac, state->secret_hash, sizeof(state->secret_hash),
+                           hdr->pubkey, sizeof(hdr->pubkey));
+               hmac_sha512(hmac, hmac, sizeof(hmac), key_dh, sizeof(key_dh));
+               curve25519_clamp_secret(hmac + CURVE25519_KEY_SIZE);
+               curve25519_generate_public(hmac, hmac + CURVE25519_KEY_SIZE);
+               valid_secret = !memcmp(hmac, secret_hash, sizeof(secret_hash));
+       }
+
+       peer = enroll_get_peer(hdr, addr, key_data, tb[ENROLL_ATTR_INFO]);
+       if (!peer)
+               return;
+
+       memcpy(peer->enroll_key, pubkey, sizeof(peer->enroll_key));
+       peer->has_key = true;
+       peer->has_secret = valid_secret;
+       enroll_peer_notify(peer);
+       if (valid_secret && state->auto_accept)
+               enroll_peer_accept(peer, NULL);
+}
+
+static void
+enroll_send_client_announce(struct enroll_peer *peer, struct blob_attr *meta)
+{
+       uint8_t local_key[CURVE25519_KEY_SIZE];
+       uint8_t pubkey[CURVE25519_KEY_SIZE];
+       uint8_t hmac[SHA512_HASH_SIZE];
+
+       blob_buf_init(&b, 0);
+
+       enroll_peer_derive_local_key(peer, local_key);
+       curve25519_generate_public(pubkey, local_key);
+       enroll_add_key(pubkey);
+
+       /*
+        * TMP_DH = DH(local_key, peer->pubkey));
+        * HASH = HMAC(chaining_hash, TMP_DH)
+        */
+       curve25519(pubkey, local_key, peer->pubkey);
+       hmac_sha512(hmac, chaining_hash, sizeof(chaining_hash),
+                   pubkey, sizeof(pubkey));
+       enroll_add_hash(hmac);
+
+       /*
+        * SECRET_HASH = HMAC(chaining_hash, secret)
+        * SECRET_TMP = HMAC(SECRET_HASH, pubkey)
+        * SECRET = HMAC(SECRET_TMP, TMP_DH)
+        * SECRET_PUB = DH_PUB(SECRET[32-64])
+        */
+       if (state->has_secret) {
+               hmac_sha512(hmac, state->secret_hash, sizeof(state->secret_hash),
+                           state->pubkey, sizeof(state->pubkey));
+               hmac_sha512(hmac, hmac, sizeof(hmac), pubkey, sizeof(pubkey));
+               curve25519_clamp_secret(hmac + CURVE25519_KEY_SIZE);
+               curve25519_generate_public(hmac, hmac + CURVE25519_KEY_SIZE);
+               enroll_add_secret(hmac);
+       }
+
+       enroll_add_info(meta);
+
+       enroll_msg_send(MSG_ID_C_ANNOUNCE, b.head, peer->pubkey, &peer->addr);
+}
+
+static void
+enroll_recv_client_accept(const struct enroll_msg_hdr *hdr,
+                      struct enroll_msg_key_data *key_data,
+                      struct sockaddr_in6 *addr)
+{
+       struct enroll_peer *peer;
+
+       peer = enroll_get_peer(hdr, addr, NULL, NULL);
+       if (!peer)
+               return;
+
+       if (peer->confirmed)
+               return;
+
+       peer->confirmed = true;
+       enroll_peer_notify(peer);
+}
+
+static void
+enroll_send_client_accept(struct enroll_peer *peer)
+{
+       struct sha512_state s;
+
+       sha512_init(&s);
+       sha512_add(&s, peer->enroll_key, sizeof(peer->enroll_key));
+       sha512_add(&s, peer->pubkey, sizeof(peer->pubkey));
+
+       blob_buf_init(&b, 0);
+       enroll_add_hash(sha512_final_get(&s));
+       enroll_msg_send(MSG_ID_C_ACCEPT, b.head, peer->pubkey, &peer->addr);
+}
+
+static void
+enroll_recv_server_discovery(const struct enroll_msg_hdr *hdr,
+                         struct enroll_msg_key_data *key_data,
+                         struct sockaddr_in6 *addr)
+{
+       struct enroll_peer *peer;
+
+       if (!tb[ENROLL_ATTR_INFO]) {
+               D("Invalid server discovery message");
+               return;
+       }
+
+       peer = enroll_get_peer(hdr, addr, key_data, tb[ENROLL_ATTR_INFO]);
+       if (!peer)
+               return;
+
+       enroll_send_client_announce(peer, state->meta);
+       enroll_peer_notify(peer);
+}
+
+static void
+enroll_recv_server_confirm(const struct enroll_msg_hdr *hdr,
+                       struct enroll_msg_key_data *key_data,
+                       struct sockaddr_in6 *addr)
+{
+       uint8_t auth_key[CURVE25519_KEY_SIZE];
+       uint8_t secret_hash[ENROLL_HASH_SIZE];
+       uint8_t hmac[SHA512_HASH_SIZE];
+       struct enroll_peer *peer;
+
+       if (!tb[ENROLL_ATTR_INFO] || !enroll_parse_key(auth_key)) {
+               D("Invalid server confirm message");
+               return;
+       }
+
+       peer = enroll_get_peer(hdr, addr, NULL, NULL);
+       if (!peer)
+               return;
+
+       memcpy(peer->enroll_key, auth_key, sizeof(peer->enroll_key));
+       free(peer->enroll_meta);
+       peer->enroll_meta = blob_memdup(tb[ENROLL_ATTR_INFO]);
+       peer->has_key = true;
+       peer->confirmed = true;
+       enroll_peer_notify(peer);
+
+       if (state->has_secret && enroll_parse_secret(secret_hash)) {
+               hmac_sha512(hmac, state->pubkey, sizeof(state->pubkey),
+                           state->secret_hash, sizeof(state->secret_hash));
+               if (!memcmp(hmac, secret_hash, sizeof(secret_hash))) {
+                       peer->has_secret = true;
+                       if (state->auto_accept)
+                               peer->accepted = true;
+               }
+       }
+
+       if (peer->accepted)
+               enroll_peer_accept(peer, NULL);
+}
+
+static void
+enroll_send_server_confirm(struct enroll_peer *peer, struct blob_attr *meta)
+{
+       uint8_t hmac[SHA512_HASH_SIZE];
+
+       if (!meta)
+               meta = state->enroll_meta;
+
+       blob_buf_init(&b, 0);
+       enroll_add_info(meta);
+       enroll_add_key(state->net->config.auth_key);
+       if (peer->has_secret) {
+               /*
+                * SECRET_TMP = HMAC(chaining_hash, secret)
+                * SECRET = HMAC(peer->pubkey, SECRET_TMP)
+                */
+               hmac_sha512(hmac, peer->pubkey, sizeof(peer->pubkey),
+                           state->secret_hash, sizeof(state->secret_hash));
+               enroll_add_secret(hmac);
+       }
+       enroll_msg_send(MSG_ID_S_CONFIRM, b.head, peer->pubkey, &peer->addr);
+}
+
+
+void pex_enroll_recv(void *data, size_t len, struct sockaddr_in6 *addr)
+{
+       const struct enroll_msg_hdr *hdr = data;
+       struct enroll_msg_key_data key_data = {};
+       uint8_t hmac[SHA512_HASH_SIZE];
+       bool server_msg;
+       char *msg_str;
+
+       if (!state || len < sizeof(struct enroll_msg_hdr))
+               return;
+
+       data += sizeof(*hdr);
+       len -= sizeof(*hdr);
+
+       if (!memcmp(hdr->pubkey, state->pubkey, sizeof(hdr->pubkey)))
+               return;
+
+       if (hdr->op != MSG_ID_C_DISCOVERY) {
+               if (!len)
+                       return;
+
+               enroll_calc_session_keys(&key_data, hdr->op, hdr->pubkey);
+               hmac_sha512(hmac, key_data.data_key, sizeof(key_data.data_key),
+                           data, len);
+
+               if (memcmp(hmac, hdr->hmac, sizeof(hdr->hmac)) != 0) {
+                       D("Invalid HMAC in enroll msg, op=%d", hdr->op);
+                       return;
+               }
+
+               chacha20_encrypt_msg(data, len, hdr->nonce, key_data.data_key);
+
+               if (blobmsg_parse(enroll_policy, __ENROLL_ATTR_MAX, tb,
+                                 data, len)) {
+                       D("Invalid data in enroll msg, op=%d", hdr->op);
+                       return;
+               }
+       }
+
+       if (hdr->op >= ARRAY_SIZE(msg_op_names) || !msg_op_names[hdr->op]) {
+               D("Unknown enroll message id, op=%d\n", hdr->op);
+               return;
+       }
+
+       server_msg = is_server_msg(hdr->op);
+
+       blob_buf_init(&b, 0);
+       blobmsg_add_ipaddr(&b, "address", addr);
+       blobmsg_add_key(&b, "id", hdr->pubkey);
+       if (hdr->op != MSG_ID_C_DISCOVERY)
+               blobmsg_add_field(&b, BLOBMSG_TYPE_TABLE, "data", data, len);
+       msg_str = blobmsg_format_json(b.head, true);
+       D("rx enroll %s %s: %s", server_msg ? "server" : "client", msg_op_names[hdr->op], msg_str);
+       free(msg_str);
+
+       if (server_msg != !state->net)
+               return;
+
+       switch (hdr->op) {
+       case MSG_ID_C_DISCOVERY:
+               if (enroll_get_peer(hdr, addr, NULL, NULL))
+                   return;
+               enroll_send_server_discovery(hdr->pubkey, addr, state->meta);
+               return;
+       case MSG_ID_C_ANNOUNCE:
+               return enroll_recv_client_announce(hdr, &key_data, addr);
+       case MSG_ID_C_ACCEPT:
+               return enroll_recv_client_accept(hdr, &key_data, addr);
+       case MSG_ID_S_DISCOVERY:
+               return enroll_recv_server_discovery(hdr, &key_data, addr);
+       case MSG_ID_S_CONFIRM:
+               return enroll_recv_server_confirm(hdr, &key_data, addr);
+       default:
+               D("Invalid enroll msg, op=%d\n", hdr->op);
+               break;
+       }
+}
+
+void enroll_net_cleanup(struct network *net)
+{
+       if (state && state->net == net)
+               enroll_stop();
+}
+
+void enroll_peer_info(struct blob_buf *buf, struct enroll_peer *peer)
+{
+       uint8_t local_key[CURVE25519_KEY_SIZE];
+       uint8_t local_pubkey[CURVE25519_KEY_SIZE];
+
+       blobmsg_add_key(buf, "id", peer->pubkey);
+       blobmsg_printf(buf, "session", "%08x", be32_to_cpu(*(uint32_t *)peer->session_id));
+       blobmsg_add_ipaddr(buf, "address", &peer->addr);
+
+       if (!state->net) {
+               enroll_peer_derive_local_key(peer, local_key);
+               blobmsg_add_key(buf, "local_key", local_key);
+               curve25519_generate_public(local_pubkey, local_key);
+               blobmsg_add_key(buf, "local_pubkey", local_pubkey);
+       }
+       if (peer->has_key)
+               blobmsg_add_key(buf, "enroll_key", peer->enroll_key);
+       if (peer->enroll_meta)
+               blobmsg_add_field(buf, BLOBMSG_TYPE_TABLE, "enroll_meta",
+                                 blobmsg_data(peer->enroll_meta),
+                                 blobmsg_data_len(peer->enroll_meta));
+
+       blobmsg_add_u8(buf, "confirmed", peer->confirmed);
+       blobmsg_add_u8(buf, "accepted", peer->accepted);
+       blobmsg_add_u8(buf, "has_secret", peer->has_secret);
+}
+
+void enroll_peer_accept(struct enroll_peer *peer, struct blob_attr *meta)
+{
+       peer->accepted = true;
+       enroll_peer_notify(peer);
+       if (state->net) {
+               enroll_send_server_confirm(peer, meta);
+               return;
+       }
+
+       if (!peer->has_key)
+               return;
+
+       enroll_send_client_accept(peer);
+       uloop_timeout_cancel(&state->connect_timer);
+}
+
+static void enroll_timeout_cb(struct uloop_timeout *t)
+{
+       blob_buf_init(&b, 0);
+       unetd_ubus_notify("enroll_timeout", b.head);
+       enroll_stop();
+}
+
+struct enroll_state *enroll_state(void)
+{
+       return state;
+}
+
+static void connect_timer_cb(struct uloop_timeout *t)
+{
+       uloop_timeout_set(t, state->connect_interval);
+
+       for (size_t i = 0; i < state->n_connect; i++)
+               enroll_send_client_discovery(&state->connect[i].in6);
+}
+
+const struct blobmsg_policy enroll_start_policy[__ENROLL_START_ATTR_MAX] = {
+       [ENROLL_START_ATTR_NETWORK] = { "network", BLOBMSG_TYPE_STRING },
+       [ENROLL_START_ATTR_TIMEOUT] = { "timeout", BLOBMSG_TYPE_INT32 },
+       [ENROLL_START_ATTR_CONNECT] = { "connect", BLOBMSG_TYPE_ARRAY },
+       [ENROLL_START_ATTR_INTERVAL] = { "interval", BLOBMSG_TYPE_INT32 },
+       [ENROLL_START_ATTR_ENROLL_AUTO] = { "enroll_auto", BLOBMSG_TYPE_BOOL },
+       [ENROLL_START_ATTR_ENROLL_SECRET] = { "enroll_secret", BLOBMSG_TYPE_STRING },
+       [ENROLL_START_ATTR_ENROLL_INFO] = { "enroll_info", BLOBMSG_TYPE_TABLE },
+       [ENROLL_START_ATTR_INFO] = { "info", BLOBMSG_TYPE_TABLE },
+};
+
+int enroll_start(struct blob_attr *data)
+{
+       struct blob_attr *tb[__ENROLL_START_ATTR_MAX], *cur;
+       struct blob_attr *meta, *enroll_meta, *remote;
+       struct blob_attr *meta_buf, *enroll_meta_buf;
+       unsigned int timeout, interval;
+       struct network *net = NULL;
+       int n_connect = 0, err = 0;
+       size_t rem;
+       FILE *f;
+
+       enroll_stop();
+       blobmsg_parse_attr(enroll_start_policy, __ENROLL_START_ATTR_MAX, tb, data);
+
+       if ((cur = tb[ENROLL_START_ATTR_NETWORK]) != NULL) {
+               const char *name = blobmsg_get_string(cur);
+
+               net = avl_find_element(&networks, name, net, node);
+               if (!net)
+                       return UBUS_STATUS_NOT_FOUND;
+       }
+
+       if ((cur = tb[ENROLL_START_ATTR_TIMEOUT]) != NULL)
+               timeout = blobmsg_get_u32(cur);
+       else
+               timeout = 120;
+
+       if (net)
+               interval = 0;
+       else if ((cur = tb[ENROLL_START_ATTR_INTERVAL]) != NULL)
+               interval = blobmsg_get_u32(cur);
+       else
+               interval = 10;
+
+       blob_buf_init(&b, 0);
+       meta = tb[ENROLL_START_ATTR_INFO];
+       if (!meta)
+               meta = b.head;
+
+       enroll_meta = tb[ENROLL_START_ATTR_ENROLL_INFO];
+       if (!enroll_meta)
+               enroll_meta = b.head;
+
+       remote = tb[ENROLL_START_ATTR_CONNECT];
+       if (remote) {
+               n_connect = blobmsg_check_array(remote, BLOBMSG_TYPE_STRING);
+               if (n_connect < 0)
+                       return UBUS_STATUS_INVALID_ARGUMENT;
+       }
+
+       enroll_global_init();
+       state = calloc_a(sizeof(*state) + n_connect * sizeof(state->connect[0]),
+                        &meta_buf, blob_pad_len(meta),
+                        &enroll_meta_buf, blob_pad_len(enroll_meta));
+       state->net = net;
+       state->connect_interval = interval * 1000;
+       avl_init(&state->peers, enroll_peer_cmp, false, NULL);
+       state->meta = memcpy(meta_buf, meta, blob_pad_len(meta));
+       state->enroll_meta = memcpy(enroll_meta_buf, enroll_meta, blob_pad_len(enroll_meta));
+
+       blobmsg_for_each_attr(cur, remote, rem) {
+               if (network_get_endpoint(&state->connect[state->n_connect],
+                                        AF_UNSPEC, blobmsg_get_string(cur),
+                                        UNETD_GLOBAL_PEX_PORT, 0))
+                       continue;
+
+               state->n_connect++;
+       }
+
+       f = fopen("/dev/urandom", "r");
+       if (!f)
+               return UBUS_STATUS_UNKNOWN_ERROR;
+
+       if (fread(state->privkey, sizeof(state->privkey), 1, f) != 1)
+           err = UBUS_STATUS_UNKNOWN_ERROR;
+
+       fclose(f);
+       if (err)
+           goto error;
+
+       curve25519_clamp_secret(state->privkey);
+       curve25519_generate_public(state->pubkey, state->privkey);
+
+       if ((cur = tb[ENROLL_START_ATTR_ENROLL_SECRET]) != NULL) {
+               const char *str = blobmsg_get_string(cur);
+
+               hmac_sha512(state->secret_hash, chaining_hash, sizeof(chaining_hash),
+                           str, strlen(str));
+               state->has_secret = true;
+               if ((cur = tb[ENROLL_START_ATTR_ENROLL_AUTO]) != NULL)
+                   state->auto_accept = blobmsg_get_bool(cur);
+       }
+
+       state->timeout.cb = enroll_timeout_cb;
+       if (timeout)
+               uloop_timeout_set(&state->timeout, timeout * 1000);
+       state->connect_timer.cb = connect_timer_cb;
+       if (interval && state->n_connect)
+               uloop_timeout_set(&state->connect_timer, 10);
+
+       return 0;
+
+error:
+       free(state);
+       state = NULL;
+       return err;
+}
+
+void enroll_stop(void)
+{
+       struct enroll_peer *p, *tmp;
+
+       if (!state)
+               return;
+
+       avl_for_each_element_safe(&state->peers, p, node, tmp)
+               enroll_peer_free(p);
+
+       uloop_timeout_cancel(&state->timeout);
+       uloop_timeout_cancel(&state->connect_timer);
+       free(state);
+       state = NULL;
+}
diff --git a/enroll.h b/enroll.h
new file mode 100644 (file)
index 0000000..99989b6
--- /dev/null
+++ b/enroll.h
@@ -0,0 +1,103 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2024 Felix Fietkau <nbd@nbd.name>
+ */
+#ifndef __ENROLL_H
+#define __ENROLL_H
+
+#include <libubox/blobmsg.h>
+#include <libubox/uloop.h>
+#include <libubox/avl.h>
+#include "utils.h"
+#include "curve25519.h"
+#include "sha512.h"
+
+#define ENROLL_SESSION_ID_LEN 4
+#define ENROLL_HASH_SIZE 32
+
+#define ENROLL_MAX_PEERS 64
+
+struct network;
+
+struct enroll_peer {
+       struct avl_node node;
+
+       struct sockaddr_in6 addr;
+       uint64_t nonce;
+
+       uint8_t session_id[ENROLL_SESSION_ID_LEN];
+       uint8_t session_key[CURVE25519_KEY_SIZE];
+       uint8_t pubkey[CURVE25519_KEY_SIZE];
+
+       struct blob_attr *enroll_meta;
+       uint8_t enroll_key[CURVE25519_KEY_SIZE];
+
+       bool has_secret;
+       bool has_key;
+       bool confirmed;
+       bool accepted;
+
+       struct blob_attr meta[];
+};
+
+struct enroll_state {
+       struct network *net;
+
+       struct avl_tree peers;
+
+       struct uloop_timeout timeout;
+       struct uloop_timeout connect_timer;
+       uint8_t privkey[2 * CURVE25519_KEY_SIZE];
+       uint8_t pubkey[CURVE25519_KEY_SIZE];
+       uint64_t nonce;
+
+       struct blob_attr *meta;
+       struct blob_attr *enroll_meta;
+
+       uint8_t secret_hash[SHA512_HASH_SIZE];
+       bool has_secret;
+       bool auto_accept;
+
+       unsigned int connect_interval;
+       unsigned int n_connect;
+       union network_endpoint connect[];
+};
+
+enum {
+       ENROLL_START_ATTR_NETWORK,
+       ENROLL_START_ATTR_TIMEOUT,
+       ENROLL_START_ATTR_CONNECT,
+       ENROLL_START_ATTR_INTERVAL,
+       ENROLL_START_ATTR_ENROLL_AUTO,
+       ENROLL_START_ATTR_ENROLL_SECRET,
+       ENROLL_START_ATTR_ENROLL_INFO,
+       ENROLL_START_ATTR_INFO,
+       __ENROLL_START_ATTR_MAX,
+};
+
+#ifdef UBUS_SUPPORT
+
+extern const struct blobmsg_policy enroll_start_policy[__ENROLL_START_ATTR_MAX];
+
+void pex_enroll_recv(void *data, size_t len, struct sockaddr_in6 *addr);
+
+struct enroll_state *enroll_state(void);
+void enroll_net_cleanup(struct network *net);
+void enroll_peer_info(struct blob_buf *buf, struct enroll_peer *peer);
+void enroll_peer_accept(struct enroll_peer *peer, struct blob_attr *meta);
+int enroll_start(struct blob_attr *data);
+void enroll_stop(void);
+
+#else
+
+static inline void pex_enroll_recv(void *data, size_t len, struct sockaddr_in6 *addr)
+{
+}
+
+static inline void enroll_net_cleanup(struct network *net)
+{
+}
+
+#endif
+
+#endif
index ecce10233246dfb355b077d3fd8daa1f816639e0..fe1b2c901b19c534388ad418972e519fa3ebcbd0 100644 (file)
--- a/network.c
+++ b/network.c
@@ -12,6 +12,7 @@
 #include <libubox/utils.h>
 #include <libubox/blobmsg_json.h>
 #include "unetd.h"
+#include "enroll.h"
 
 enum {
        NETDATA_ATTR_CONFIG,
@@ -561,6 +562,7 @@ static int network_setup(struct network *net)
 
 static void network_teardown(struct network *net)
 {
+       enroll_net_cleanup(net);
        uloop_timeout_cancel(&net->connect_timer);
        uloop_timeout_cancel(&net->reload_timer);
        network_do_update(net, false);
index 1d85b268ac9a5bd179ea5942f3841f4dd44cfd04..b45eda098c6b7dafb7cafd5ce56439334038a085 100644 (file)
--- a/pex-msg.c
+++ b/pex-msg.c
@@ -123,6 +123,13 @@ struct pex_hdr *__pex_msg_init_ext(const uint8_t *pubkey, const uint8_t *auth_ke
        return hdr;
 }
 
+void *pex_msg_tail(void)
+{
+       struct pex_hdr *hdr = (struct pex_hdr *)pex_tx_buf;
+
+       return &pex_tx_buf[hdr->len + sizeof(struct pex_hdr)];
+}
+
 void *pex_msg_append(size_t len)
 {
        struct pex_hdr *hdr = (struct pex_hdr *)pex_tx_buf;
index 3ca1984063b37e190103295b5e9b35ba1c4cc179..36aade2b02f6b0c8208668d2f6d74a879585453a 100644 (file)
--- a/pex-msg.h
+++ b/pex-msg.h
@@ -25,6 +25,7 @@ enum pex_opcode {
        PEX_MSG_UPDATE_RESPONSE_NO_DATA,
        PEX_MSG_ENDPOINT_NOTIFY,
        PEX_MSG_ENDPOINT_PORT_NOTIFY,
+       PEX_MSG_ENROLL,
 };
 
 #define PEX_ID_LEN             8
@@ -116,6 +117,7 @@ struct pex_hdr *__pex_msg_init_ext(const uint8_t *pubkey, const uint8_t *auth_ke
                                   uint8_t opcode, bool ext);
 int __pex_msg_send(int fd, const void *addr, void *ip_hdr, size_t ip_hdrlen);
 void *pex_msg_append(size_t len);
+void *pex_msg_tail(void);
 
 struct pex_update_request *
 pex_msg_update_request_init(const uint8_t *pubkey, const uint8_t *priv_key,
diff --git a/pex.c b/pex.c
index 854837c8285d91f92c57b4a45734764810a54fc1..8a13c80c2ccac6a1f3c29445f23a066252abb3d4 100644 (file)
--- a/pex.c
+++ b/pex.c
@@ -14,6 +14,7 @@
 #include <inttypes.h>
 #include "unetd.h"
 #include "pex-msg.h"
+#include "enroll.h"
 
 static const char *pex_peer_id_str(const uint8_t *key)
 {
@@ -928,7 +929,7 @@ global_pex_recv(void *msg, size_t msg_len, struct sockaddr_in6 *addr)
        struct pex_hdr *hdr;
        struct pex_ext_hdr *ehdr;
        struct network_peer *peer;
-       struct network *net;
+       struct network *net = NULL;
        char buf[INET6_ADDRSTRLEN];
        void *data;
        int addr_len;
@@ -949,13 +950,15 @@ global_pex_recv(void *msg, size_t msg_len, struct sockaddr_in6 *addr)
        if (hdr->version != 0)
                return;
 
-       net = global_pex_find_network(ehdr->auth_id);
-       if (!net || net->config.type != NETWORK_TYPE_DYNAMIC)
-               return;
+       if (hdr->opcode != PEX_MSG_ENROLL) {
+               net = global_pex_find_network(ehdr->auth_id);
+               if (!net || net->config.type != NETWORK_TYPE_DYNAMIC)
+                       return;
 
-       *(uint64_t *)hdr->id ^= pex_network_hash(net->config.auth_key, ehdr->nonce);
+               *(uint64_t *)hdr->id ^= pex_network_hash(net->config.auth_key, ehdr->nonce);
 
-       global_pex_set_active(net, addr);
+               global_pex_set_active(net, addr);
+       }
 
        D("PEX global rx op=%d", hdr->opcode);
        switch (hdr->opcode) {
@@ -1002,6 +1005,9 @@ global_pex_recv(void *msg, size_t msg_len, struct sockaddr_in6 *addr)
                                network_pex_create_host(net, &host_ep, 120);
                }
                break;
+       case PEX_MSG_ENROLL:
+               pex_enroll_recv(data, hdr->len, addr);
+               break;
        }
 }
 
diff --git a/ubus.c b/ubus.c
index 91af1dc20839e7c1750d9d7fe0cfbcfceb12b5f4..2377acc2ef553667d49f43200359a3c526284433 100644 (file)
--- a/ubus.c
+++ b/ubus.c
@@ -1,10 +1,11 @@
 // SPDX-License-Identifier: GPL-2.0-or-later
 /*
- * Copyright (C) 2022 Felix Fietkau <nbd@nbd.name>
+ * Copyright (C) 2022-2024 Felix Fietkau <nbd@nbd.name>
  */
 #include <arpa/inet.h>
 #include <libubus.h>
 #include "unetd.h"
+#include "enroll.h"
 
 static struct ubus_auto_conn conn;
 static struct blob_buf b;
@@ -263,6 +264,155 @@ ubus_reload(struct ubus_context *ctx, struct ubus_object *obj,
        return 0;
 }
 
+static int
+ubus_enroll_start(struct ubus_context *ctx, struct ubus_object *obj,
+                 struct ubus_request_data *req, const char *method,
+                 struct blob_attr *msg)
+{
+       return enroll_start(msg);
+}
+
+static int
+ubus_enroll_stop(struct ubus_context *ctx, struct ubus_object *obj,
+                struct ubus_request_data *req, const char *method,
+                struct blob_attr *msg)
+{
+       enroll_stop();
+       return 0;
+}
+
+enum {
+       ENROLL_PEER_ATTR_ID,
+       ENROLL_PEER_ATTR_SESSION,
+       ENROLL_PEER_ATTR_INFO,
+       __ENROLL_PEER_ATTR_MAX,
+};
+
+static const struct blobmsg_policy enroll_peer_policy[__ENROLL_PEER_ATTR_MAX] = {
+       [ENROLL_PEER_ATTR_ID] = { "id", BLOBMSG_TYPE_STRING },
+       [ENROLL_PEER_ATTR_SESSION] = { "session", BLOBMSG_TYPE_STRING },
+       [ENROLL_PEER_ATTR_INFO] = { "info", BLOBMSG_TYPE_TABLE },
+};
+
+struct enroll_peer_select {
+       uint8_t id[CURVE25519_KEY_SIZE];
+       uint32_t session;
+       bool has_session, has_id;
+};
+
+static int
+ubus_enroll_parse(struct enroll_peer_select *sel, struct blob_attr **tb)
+{
+       struct blob_attr *cur;
+
+       if ((cur = tb[ENROLL_PEER_ATTR_ID]) != NULL) {
+               char *str = blobmsg_get_string(cur);
+
+               if (b64_decode(str, sel->id, sizeof(sel->id)) != CURVE25519_KEY_SIZE)
+                       return -1;
+
+               sel->has_id = true;
+       }
+
+       if ((cur = tb[ENROLL_PEER_ATTR_SESSION]) != NULL) {
+               char *str = blobmsg_get_string(cur);
+               uint32_t id;
+               char *err;
+
+               id = strtoul(str, &err, 16);
+               if (*err)
+                       return -1;
+
+               sel->session = cpu_to_be32(id);
+               sel->has_session = true;
+       }
+
+       return 0;
+}
+
+static bool
+ubus_enroll_match(struct enroll_peer_select *sel, struct enroll_peer *peer)
+{
+       if (sel->has_id &&
+           memcmp(peer->pubkey, sel->id, sizeof(sel->id)) != 0)
+               return false;
+       if (sel->has_session &&
+           memcmp(peer->session_id, &sel->session, sizeof(sel->session)) != 0)
+               return false;
+       return true;
+}
+
+static int
+ubus_enroll_status(struct ubus_context *ctx, struct ubus_object *obj,
+                  struct ubus_request_data *req, const char *method,
+                  struct blob_attr *msg)
+{
+       struct blob_attr *tb[__ENROLL_PEER_ATTR_MAX];
+       struct enroll_state *state = enroll_state();
+       struct enroll_peer_select sel = {};
+       struct enroll_peer *peer;
+       void *a, *c;
+
+       if (!state)
+               return UBUS_STATUS_NO_DATA;
+
+       blobmsg_parse_attr(enroll_peer_policy, __ENROLL_PEER_ATTR_MAX, tb, msg);
+       if (ubus_enroll_parse(&sel, tb))
+               return UBUS_STATUS_INVALID_ARGUMENT;
+
+       blob_buf_init(&b, 0);
+
+       a = blobmsg_open_array(&b, "peers");
+       avl_for_each_element(&state->peers, peer, node) {
+               if (!ubus_enroll_match(&sel, peer))
+                       continue;
+
+               c = blobmsg_open_table(&b, NULL);
+               enroll_peer_info(&b, peer);
+               blobmsg_close_table(&b, c);
+       }
+       blobmsg_close_array(&b, a);
+
+       ubus_send_reply(ctx, req, b.head);
+
+       return 0;
+}
+
+static int
+ubus_enroll_accept(struct ubus_context *ctx, struct ubus_object *obj,
+                  struct ubus_request_data *req, const char *method,
+                  struct blob_attr *msg)
+{
+       struct blob_attr *tb[__ENROLL_PEER_ATTR_MAX];
+       struct enroll_state *state = enroll_state();
+       struct enroll_peer *peer = NULL, *cur;
+       struct enroll_peer_select sel = {};
+
+       if (!state)
+               return UBUS_STATUS_NO_DATA;
+
+       blobmsg_parse_attr(enroll_peer_policy, __ENROLL_PEER_ATTR_MAX, tb, msg);
+       if (ubus_enroll_parse(&sel, tb))
+               return UBUS_STATUS_INVALID_ARGUMENT;
+
+       if (!sel.has_id && !sel.has_session)
+               return UBUS_STATUS_INVALID_ARGUMENT;
+
+       avl_for_each_element(&state->peers, cur, node) {
+               if (!ubus_enroll_match(&sel, cur))
+                       continue;
+               if (peer)
+                       return UBUS_STATUS_NOT_FOUND;
+               peer = cur;
+       }
+
+       if (!peer)
+               return UBUS_STATUS_NOT_FOUND;
+
+       enroll_peer_accept(peer, tb[ENROLL_PEER_ATTR_INFO]);
+
+       return 0;
+}
 
 static const struct ubus_method unetd_methods[] = {
        UBUS_METHOD("network_add", ubus_network_add, network_policy),
@@ -273,6 +423,12 @@ static const struct ubus_method unetd_methods[] = {
        UBUS_METHOD("network_connect", ubus_network_connect, connect_policy),
        UBUS_METHOD_NOARG("reload", ubus_reload),
        UBUS_METHOD("service_get", ubus_service_get, service_policy),
+       UBUS_METHOD("enroll_start", ubus_enroll_start, enroll_start_policy),
+       UBUS_METHOD_MASK("enroll_status", ubus_enroll_status, enroll_peer_policy,
+                       (1 << ENROLL_PEER_ATTR_ID) |
+                       (1 << ENROLL_PEER_ATTR_SESSION)),
+       UBUS_METHOD("enroll_accept", ubus_enroll_accept, enroll_peer_policy),
+       UBUS_METHOD_NOARG("enroll_stop", ubus_enroll_stop),
 };
 
 static struct ubus_object_type unetd_object_type =
@@ -335,11 +491,16 @@ static void unetd_ubus_procd_update(void)
        ubus_invoke(&conn.ctx, id, "set", b.head, NULL, NULL, -1);
 }
 
+void unetd_ubus_notify(const char *type, struct blob_attr *data)
+{
+       ubus_notify(&conn.ctx, &unetd_object, type, data, -1);
+}
+
 void unetd_ubus_network_notify(struct network *net)
 {
        blob_buf_init(&b, 0);
        blobmsg_add_string(&b, "network", network_name(net));
-       ubus_notify(&conn.ctx, &unetd_object, "network_update", b.head, -1);
+       unetd_ubus_notify("network_update", b.head);
        unetd_ubus_procd_update();
 }
 
diff --git a/ubus.h b/ubus.h
index c795622b330b609659a4c7165868cd1ff5dd69bc..1c428948020d21fa07665658c5b5ac42c1602126 100644 (file)
--- a/ubus.h
+++ b/ubus.h
@@ -7,6 +7,7 @@
 
 #ifdef UBUS_SUPPORT
 void unetd_ubus_init(void);
+void unetd_ubus_notify(const char *type, struct blob_attr *data);
 void unetd_ubus_network_notify(struct network *net);
 void unetd_ubus_netifd_update(struct blob_attr *data);
 void unetd_ubus_netifd_add_route(struct network *net, union network_endpoint *ep);
@@ -14,6 +15,9 @@ void unetd_ubus_netifd_add_route(struct network *net, union network_endpoint *ep
 static inline void unetd_ubus_init(void)
 {
 }
+static inline void unetd_ubus_notify(const char *type, struct blob_attr *data)
+{
+}
 static inline void unetd_ubus_network_notify(struct network *net)
 {
 }