diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a9c473f9..cc09abe68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,7 +37,12 @@ * add interactive accept/reject/trust for arcan-net use * reserve 'outbound' domain for interactively added outbound keys * enforce domain separation for allowed pubkeys - * server in directory mode now splits out into sandboxed worker processes + * directory mode now splits out into sandboxed worker processes + * directory mode can now specify permissions per tag-group + * directory mode dynamic push appl from client with permission + * directory mode notification of appl updates and sources coming / leaving + * directory mode registration of dynamic sinks + * directory mode sourcing dynamic sinks (sink-inbound only) ## Terminal * SGR reset fix, add CNL / CPL diff --git a/src/a12/a12.c b/src/a12/a12.c index 16ceea0e1..eefa12ec8 100644 --- a/src/a12/a12.c +++ b/src/a12/a12.c @@ -803,6 +803,68 @@ static void process_srvfirst(struct a12_state* S) } } +static void fill_diropened(struct a12_state* S, struct a12_dynreq r) +{ + uint8_t outb[CONTROL_PACKET_SIZE]; + + build_control_header(S, outb, COMMAND_DIROPENED); + outb[18] = r.proto; + memcpy(&outb[19], r.host, 46); + pack_u16(r.port, &outb[65]); + memcpy(&outb[67], r.authk, 12); + memcpy(&outb[79], r.pubk, 32); + + a12int_append_out(S, STATE_CONTROL_PACKET, outb, CONTROL_PACKET_SIZE, NULL, 0); +} + +static void command_diropened(struct a12_state* S) +{ +/* local- copy the members so the closure is allowed to queue a new one */ + void(* oc)(struct a12_state*, struct a12_dynreq, void* tag) = S->pending_dynamic.closure; + void* tag = S->pending_dynamic.tag; + + S->pending_dynamic.closure = NULL; + S->pending_dynamic.tag = NULL; + + struct a12_dynreq rep = { + .proto = S->decode[18] + }; + + memcpy(rep.host, &S->decode[19], 46); + unpack_u16(&rep.port, &S->decode[65]); + memcpy(rep.authk, &S->decode[67], 12); + memcpy(rep.pubk, &S->decode[79], 32); + + oc(S, rep, tag); +} + +static void command_diropen( + struct a12_state* S, uint8_t mode, + uint8_t kpub_tgt[static 32], uint8_t kpub_src[static 32]) +{ +/* a12.h misuse */ + struct a12_unpack_cfg* C = &S->channels[0].raw; + if (!C->directory_open){ + a12int_trace(A12_TRACE_SECURITY, "kind=warning:diropen_no_handler"); + return; + } + +/* forward the request */ + struct a12_dynreq out = {0}; + uint8_t outb[CONTROL_PACKET_SIZE]; + +/* the implementation is expected to set the authk as it might be outsourced + * to an external oracle that coordinates with the partner in question */ + if (C->directory_open(S, kpub_tgt, kpub_src, mode, &out, C->tag)){ + fill_diropened(S, out); + } +/* failure is just an empty command */ + else { + build_control_header(S, outb, COMMAND_DIROPENED); + a12int_append_out(S, STATE_CONTROL_PACKET, outb, CONTROL_PACKET_SIZE, NULL, 0); + } +} + static void command_cancelstream( struct a12_state* S, uint32_t streamid, uint8_t reason, uint8_t stype) { @@ -2003,6 +2065,20 @@ static void process_control(struct a12_state* S, void (*on_event) command_cancelstream(S, streamid, S->decode[22], S->decode[23]); } break; + case COMMAND_DIROPEN:{ + if (S->opts->local_role == ROLE_DIR){ + command_diropen(S, S->decode[18], &S->decode[19], &S->decode[52]); + } + else + a12int_trace(A12_TRACE_SECURITY, "diropen:wrong_role"); + } + case COMMAND_DIROPENED:{ + if (S->pending_dynamic.active) + command_diropened(S); + else + a12int_trace(A12_TRACE_SECURITY, "diropened:no_pending_request"); + } + break; case COMMAND_PING:{ uint32_t streamid; unpack_u32(&streamid, &S->decode[18]); @@ -2025,8 +2101,16 @@ static void process_control(struct a12_state* S, void (*on_event) * 3. if this is not a rekey response package, send the new pubk in response */ break; case COMMAND_DIRLIST: +/* force- synch dynamic entries */ S->notify_dynamic = S->decode[18]; + a12int_trace(A12_TRACE_DIRECTORY, "dirlist:notify=%d", (int) S->notify_dynamic); send_dirlist(S); +/* notify the directory side that this action occurred */ + on_event(NULL, 0, &(struct arcan_event){ + .category = EVENT_EXTERNAL, + .ext.kind = EVENT_EXTERNAL_MESSAGE, + .ext.message.data = "a12:dirlist" + }, tag); break; case COMMAND_DIRDISCOVER: command_dirdiscover(S, on_event, tag); @@ -3318,9 +3402,46 @@ void a12int_notify_dynamic_resource(struct a12_state* S, } bool a12_request_dynamic_resource(struct a12_state* S, - uint8_t req_pubk[static 32], uint8_t ident_pubk[static 32], + uint8_t ident_pubk[static 32], void(*request_reply)(struct a12_state*, struct a12_dynreq, void* tag), void* tag) { - return false; + if (S->pending_dynamic.active || S->remote_mode != ROLE_DIR){ + return false; + } + +/* role source just need the closure tagged actually */ + S->pending_dynamic.active = true; + S->pending_dynamic.closure = request_reply; + S->pending_dynamic.tag = tag; + + if (S->opts->local_role != ROLE_SINK) + return true; + +/* this key isn't as sensitive as it will only be used to authenticate the + * mediated nested connections ephemeral layer not added to the keystore */ + uint8_t outb[CONTROL_PACKET_SIZE]; + build_control_header(S, outb, COMMAND_DIROPEN); + arcan_random(S->pending_dynamic.key, 32); + memcpy(&outb[19], ident_pubk, 32); + x25519_public_key(S->pending_dynamic.key, &outb[52]); + a12int_append_out(S, STATE_CONTROL_PACKET, outb, CONTROL_PACKET_SIZE, NULL, 0); + + return true; +} + +void a12_set_endpoint(struct a12_state* S, const char* ep) +{ + free(S->endpoint); + S->endpoint = ep ? strdup(ep) : NULL; +} + +const char* a12_get_endpoint(struct a12_state* S) +{ + return S->endpoint; +} + +void a12_supply_dynamic_resource(struct a12_state* S, struct a12_dynreq r) +{ + fill_diropened(S, r); } diff --git a/src/a12/a12.h b/src/a12/a12.h index 38dd8bfcd..31255b89d 100644 --- a/src/a12/a12.h +++ b/src/a12/a12.h @@ -57,12 +57,21 @@ struct pk_response { const uint8_t pub[static 32], const char* name, size_t sz, const char* mode); }; +/* response structure for a directory-open request. */ +struct a12_dynreq { + char host[46]; + char pubk[32]; + uint16_t port; + char authk[12]; + int proto; +}; + struct a12_context_options { /* Provide to enable asymetric key authentication, set valid in the return to * allow the key, otherwise the session may be continued for a random number of * time or bytes before being terminated. If want_session is requested, the * lookup function, if it is able to (legacy) should set got_session in the - * reply and calculate the x25519v shared secret itself. */ + * reply and calculate the x25519 shared secret itself. */ struct pk_response (*pk_lookup)(uint8_t pub[static 32]); /* Client only, provide the private key to use with the connection. All [0] @@ -193,6 +202,15 @@ struct a12_unpack_cfg { /* n BYTES have been written into the buffer allocated through * request_audio_buffer */ void (*signal_audio)(size_t bytes, void* tag); + +/* only used with local_role == MODE_DIRECTORY wherein someone requests + * to open a resource through us. */ + bool (*directory_open)(struct a12_state*, + uint8_t ident_pubk[static 32], + uint8_t ident_req[static 32], + uint8_t mode, + struct a12_dynreq* out, + void* tag); }; void a12_set_destination_raw(struct a12_state*, @@ -588,6 +606,11 @@ struct a12_iostat { size_t packets_pending; /* delta between seqnr and last-seen seqnr */ }; +/* get / set a string representation for logging and similar operations + * where there is a need for a traceable origin e.g. IP address */ +const char* a12_get_endpoint(struct a12_state* S); +void a12_set_endpoint(struct a12_state* S, const char*); + /* * Sample the current rolling state statistics */ @@ -595,21 +618,26 @@ struct a12_iostat a12_state_iostat(struct a12_state* S); /* * Try to negotiate a connection for a directory resource based on an announced - * public key. Provide the public key (or =[0] to use the same key as the - * connection. Only one pending _dynamic is allowed. The callback will be - * triggered with the connection parameters for reaching the source. */ -struct a12_dynreq { - char* host; - uint16_t port; - char* authk; - int proto; -}; - + * public key. An ephemeral keypair will be generated and part of the reply. + * + * This should be used either directly and lets the outer key. + * + * in the a12_state after key derivation. This can be a different keypair if + * you want to be known to the source by a different identity than to the + * directory. The directory will discover the public part of this key however. + * Only one pending _dynamic is allowed. The callback will be triggered with + * the connection parameters for reaching the source or with a null host. + * + * The contents of dynreq will be free:d automatically after callback + * completion. + */ bool a12_request_dynamic_resource(struct a12_state* S, - uint8_t req_pubk[static 32], uint8_t ident_pubk[static 32], + uint8_t ident_pubk[static 32], void(*request_reply)(struct a12_state*, struct a12_dynreq, void* tag), void* tag); +void a12_supply_dynamic_resource(struct a12_state* S, struct a12_dynreq); + /* * debugging / tracing bits, just define a namespace that can be used * for wrapper tools to log with the same syntax and behaviour as the diff --git a/src/a12/a12_int.h b/src/a12/a12_int.h index 2bb6e9db5..fae16bdda 100644 --- a/src/a12/a12_int.h +++ b/src/a12/a12_int.h @@ -77,6 +77,7 @@ enum control_commands { COMMAND_DIRSTATE = 10,/* update / present a new appl */ COMMAND_DIRDISCOVER = 11,/* dynamic source/dir entry */ COMMAND_DIROPEN = 12,/* mediate access to a dyn src/dir */ + COMMAND_DIROPENED = 13,/* replies to DIROPEN (src/sink) */ }; enum hello_mode { @@ -261,6 +262,15 @@ struct a12_state { } congestion_stats; struct a12_iostat stats; +/* tracks a pending dynamic directory resource */ + struct { + bool active; + uint8_t key[32]; + void(* closure)(struct a12_state*, struct a12_dynreq, void* tag); + void* tag; + + } pending_dynamic; + /* populate and forwarded output buffer */ size_t buf_sz[2]; uint8_t* bufs[2]; @@ -317,6 +327,7 @@ struct a12_state { bool cl_firstout; int authentic; int remote_mode; + char* endpoint; /* saved between calls to unpack, see end of a12_unpack for explanation */ bool auth_latched; diff --git a/src/a12/net/HACKING.md b/src/a12/net/HACKING.md index 41d863587..76bca1e9e 100644 --- a/src/a12/net/HACKING.md +++ b/src/a12/net/HACKING.md @@ -553,7 +553,8 @@ while Kpub-me is the public key that the source will connect through in order to differentiate between the credential used to access the directory versus the credential used to access the source. -Mode can be treated as a bitmap of supported open-modes. +Mode can be treated as a bitmap of supported open-modes but is a hint as +the initiator cannot necessarily know the topology of the source. If mode is set to direct-inbound the request is that the other end connects to the request originator (TCP). This will provide the source-IP that was used to @@ -573,17 +574,21 @@ the server connection. ### command - 14, directory-opened - [18 ] Status : (0 failed, 1 direct-in ok, 2 direct-out ok, 2 tunnel ok) -- [19 +16] Address : Status = 1, IPv6 address to the host, - Status = 2, IPv4 address to the host, - Status = 3, tunnel ID. -- [34 +12] Secret : alphanumerical random secret to use with first HELLO +- [19 +46] Address : (string representation, \0 terminated) + Status = 1, IPv3,6 address to the host, + Status = 2, IPv4,6 address to the host, + Status = 3, 0 +- [65..66] Port : connection port or tunnel-id (status=2) +- [67 +12] Secret : alphanumerical random secret to use with first HELLO packet to authenticate. -- [45 +16] Kpub : the other end key (for direct-in) +- [79 +32] Kpub : the other end key. This will be sent to source/source-directory and to sink in response to a -directory-open request. The connection mode must skip the ephemeral handshake -hello-mode and instead use the secret to protect the negotiation packet and -to signal that the connection is mediated via this particular third party. +directory-open request. The connection handshake is just like the initial +one for the directory, but using the authentication secret to protect the +initial HELLO. It is advised to use an ephemeral keypair and the two stage +HELLO to be able to differentiate the keypair used when authenticating to +the directory versus authenticating to the source. ## Event (2), fixed length - [0..7] sequence number : uint64 diff --git a/src/a12/net/dir_cl.c b/src/a12/net/dir_cl.c index ccae3184a..ee0d880fc 100644 --- a/src/a12/net/dir_cl.c +++ b/src/a12/net/dir_cl.c @@ -58,12 +58,17 @@ static struct { static void on_source(struct a12_state* S, struct a12_dynreq req, void* tag) { + printf("waiting on the other end\n"); + /* setup the request: * we might need to listen * we might need to connect * we might need to tunnel via the directory */ +/* wait for a bit */ +/* if that fails, switch to tunnel processing */ + /* when that is done (i.e. we get a connection), chain into the corresponding * source to shmif handler for the new connection. */ } @@ -73,7 +78,8 @@ static void on_cl_event( { if (ev->category == EVENT_EXTERNAL && ev->ext.kind == EVENT_EXTERNAL_NETSTATE){ struct ioloop_shared* I = tag; - printf("source:name=%s\n", ev->ext.netstate.name); + printf("source:name=%s:state=%s\n", + ev->ext.netstate.name, ev->ext.netstate.state == 0 ? "lost" : "found"); if (I->cbt->clopt->applname[0] == '*'){ /* split out Kpub, need to use that in the open request */ size_t i = 0; @@ -86,9 +92,8 @@ static void on_cl_event( /* found it, request to negotiate an open */ if (strcmp(&I->cbt->clopt->applname[1], ev->ext.netstate.name) == 0){ - uint8_t nkey[32] = {0}; a12_request_dynamic_resource(I->S, - (uint8_t*)&ev->ext.netstate.name[i], nkey, on_source, I); + (uint8_t*)&ev->ext.netstate.name[i], on_source, I); } } return; @@ -806,6 +811,7 @@ void anet_directory_cl( * it might be slightly cleaner having an actual directory command for the * thing rather than (ab)using REGISTER here and IDENT for appl-messaging. */ if (opts.dir_source){ + uint8_t nk[32] = {0}; struct arcan_event ev = { .ext.kind = ARCAN_EVENT(REGISTER), .category = EVENT_EXTERNAL, @@ -814,6 +820,7 @@ void anet_directory_cl( snprintf( (char*)ev.ext.registr.title, 64, "%s", opts.ident); a12_channel_enqueue(S, &ev); + a12_request_dynamic_resource(S, nk, opts.dir_source, opts.dir_source_tag); } struct ioloop_shared ioloop = { diff --git a/src/a12/net/dir_srv.c b/src/a12/net/dir_srv.c index 1d65fc16b..71a00bf6e 100644 --- a/src/a12/net/dir_srv.c +++ b/src/a12/net/dir_srv.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -16,6 +17,7 @@ #include #include #include +#include #include "../a12.h" #include "../a12_int.h" @@ -46,6 +48,7 @@ struct dircl { uint16_t pending_id; arcan_event petname; + arcan_event endpoint; uint8_t pubk[32]; bool authenticated; @@ -158,6 +161,111 @@ static void dirlist_to_worker(struct dircl* C) close(fd); } +static void dynopen_to_worker(struct dircl* C, struct arg_arr* entry) +{ + const char* pubk = NULL; + + if (!arg_lookup(entry, "pubk", 0, &pubk) || !pubk) + goto send_fail; + + uint8_t pubk_dec[32]; + if (!a12helper_fromb64((const uint8_t*) pubk, 32, pubk_dec)) + goto send_fail; + + pthread_mutex_lock(&active_clients.sync); + struct dircl* cur = active_clients.root.next; + while (cur){ + if (cur == C || !cur->C || !cur->petname.ext.netstate.name[0]){ + cur = cur->next; + continue; + } + +/* got match, an open question here is if the sources should be consume on use + * or let the load balancing / queueing etc. happen at the source stage. Right + * now in the PoC we assume the source is the listening end and the sink the + * outbound one. We also have a default port for the source (6680) that should + * be possible to change. */ + if (memcmp(cur->pubk, C->pubk, 32) == 0){ + arcan_event to_src = { + .category = EVENT_EXTERNAL, + .ext.kind = EVENT_EXTERNAL_NETSTATE, +/* this does not conflict with dynlist notifications, those are only for SINK */ + .ext.netstate = { + .space = 5 + } + }; + memcpy(to_src.ext.netstate.name, C->pubk, 32); + +/* here is the heuristic spot for setting up NAT hole punching, or allocating a + * tunnel or .. right now just naively forward IP:port, set pubk and secret if + * needed. This could ideally be arranged so that the ordering (listening + * first) delayed locally based on the delta of pings, but then we'd need that + * estimate from the state machine as well. It would at least reduce the + * chances of the outbound connection having to retry if it received the + * trigger first. The lazy option is to just delay the outbound connection in + * the dir_cl for the time being. */ + arcan_event to_sink = cur->endpoint; + +/* Another protocol nuance here is that we're supposed to set an authk secret + * for the outer ephemeral making it possible to match the connection to our + * directory mediated connection versus one that was made through other means + * of discovery. This means that the source end might need to (if it should + * support multiple connection origins) enumerate secrets on the first packet + * increasing the cost somewhat. */ + arcan_event ss = { + .category = EVENT_TARGET, + .ext.kind = TARGET_COMMAND_MESSAGE + }; + + uint8_t secret[8]; + arcan_random(secret, 8); + unsigned char* b64 = a12helper_tob64(secret, 8, &(size_t){0}); + snprintf((char*)ss.tgt.message, + COUNT_OF(ss.tgt.message), "a12:dir_secret=%s", b64); + + shmifsrv_enqueue_event(C->C, &ss, -1); + shmifsrv_enqueue_event(cur->C, &ss, -1); + shmifsrv_enqueue_event(cur->C, &to_src, -1); + shmifsrv_enqueue_event(C->C, &to_sink, -1); + + free(b64); + break; + } + } + pthread_mutex_unlock(&active_clients.sync); + return; + +send_fail: + A12INT_DIRTRACE("dirsv:worker:dynopen_fail"); + shmifsrv_enqueue_event(C->C, &(struct arcan_event){ + .category = EVENT_TARGET, + .tgt.kind = TARGET_COMMAND_REQFAIL + }, -1 + ); +} + +static void dynlist_to_worker(struct dircl* C) +{ + pthread_mutex_lock(&active_clients.sync); + struct dircl* cur = active_clients.root.next; + while (cur){ + if (cur == C || !cur->C || !cur->petname.ext.netstate.name[0]){ + cur = cur->next; + continue; + } + +/* and the dynamic sources separately */ + arcan_event ev = cur->petname; + size_t nl = strlen(ev.ext.netstate.name); + ev.ext.netstate.name[nl] = ':'; + memcpy(&ev.ext.netstate.name[nl+1], cur->pubk, 32); + shmifsrv_enqueue_event(C->C, &ev, -1); + + cur = cur->next; + } + pthread_mutex_unlock(&active_clients.sync); +} + /* * split a worker provided resource identifier id[.resource] into its components * and return the matching appl_meta pairing with [id] (if any) @@ -232,6 +340,21 @@ static int get_state_res( return resfd; } +static bool tag_outbound_name(struct arcan_event* ev, uint8_t kpub[static 32]) +{ +/* pack and append the authenticated pubk */ + size_t len = strlen(ev->ext.netstate.name); + if (len > COUNT_OF(ev->ext.netstate.name) - 34){ + A12INT_DIRTRACE("dirsv:kind=warning_register:name_overflow"); + return false; + } + + ev->ext.netstate.name[len] = ':'; + memcpy(&ev->ext.netstate.name[len+1], kpub, 32); + + return true; +} + static void register_source(struct dircl* C, struct arcan_event ev) { if (!a12helper_keystore_accepted(C->pubk, active_clients.opts->allow_src)){ @@ -260,26 +383,22 @@ static void register_source(struct dircl* C, struct arcan_event ev) A12INT_DIRTRACE("dirsv:kind=register:name=%s", ev.ext.registr.title); -/* pack and append the authenticated pubk */ - size_t len = strlen(ev.ext.registr.title); - if (len > COUNT_OF(ev.ext.registr.title) - 34){ - A12INT_DIRTRACE("dirsv:kind=warning_register:name_overflow"); - return; - } - /* if we just change state / availability, don't permit rename */ - if (C->petname.ext.registr.title[0]){ - if (strcmp(C->petname.ext.registr.title, ev.ext.registr.title) != 0){ + if (C->petname.ext.netstate.name[0]){ + if (strcmp(C->petname.ext.netstate.name, ev.ext.netstate.name) != 0){ A12INT_DIRTRACE("dirsv:kind=warning_register:rename_blocked"); return; } } -/* finally ack the petname and broadcast */ C->petname = ev; - C->type = ev.ext.registr.kind; - ev.ext.registr.title[len] = ':'; - memcpy(&ev.ext.registr.title[len+1], C->pubk, 32); + ev.ext.netstate.state = 1; + +/* finally ack the petname and broadcast */ + if (!tag_outbound_name(&ev, C->pubk)) + return; + + C->type = ev.ext.netstate.type; /* notify everyone interested about the change, the local state machine * will determine whether to forward or not */ @@ -502,6 +621,101 @@ static void handle_bchunk_req(struct dircl* C, char* ext, bool input) }, -1); } +static void dircl_message(struct dircl* C, struct arcan_event ev) +{ + struct arg_arr* entry = arg_unpack((char*)ev.ext.message.data); + if (!entry){ + A12INT_DIRTRACE("dirsv:kind=worker:bad_msg:%s=", ev.ext.message.data); + return; + } + +/* just route authentication through the regular function, caching the reply */ + const char* pubk; + if (!C->authenticated){ +/* regardless of authentication reply, they only get one attempt at this */ + C->authenticated = true; + if (!arg_lookup(entry, "a12", 0, NULL) || !arg_lookup(entry, "pubk", 0, &pubk)) + goto send_fail; + + uint8_t pubk_dec[32]; + if (!a12helper_fromb64((const uint8_t*) pubk, 32, pubk_dec)) + goto send_fail; + + pthread_mutex_lock(&active_clients.sync); + struct pk_response rep = active_clients.opts->a12_cfg->pk_lookup(pubk_dec); + pthread_mutex_unlock(&active_clients.sync); + + if (!rep.authentic) + goto send_fail; + + struct arcan_event ev = { + .category = EVENT_TARGET, + .tgt.kind = TARGET_COMMAND_MESSAGE + }; + unsigned char* b64 = a12helper_tob64(rep.key_pub, 32, &(size_t){0}); + snprintf((char*)&ev.tgt.message, COUNT_OF(ev.tgt.message), "a12:pub=%s", b64); + free(b64); + shmifsrv_enqueue_event(C->C, &ev, -1); + b64 = a12helper_tob64(rep.key_session, 32, &(size_t){0}); + snprintf((char*)&ev.tgt.message, COUNT_OF(ev.tgt.message), "a12:ss=%s", b64); + free(b64); + shmifsrv_enqueue_event(C->C, &ev, -1); + memcpy(C->pubk, pubk_dec, 32); + + return; + } + + if (!arg_lookup(entry, "a12", 0, NULL)) + goto send_fail; + +/* this one comes from a DIRLIST being sent to the worker state machine. The + * worker doesn't retain a synched list and may flip between dynamic + * notification and not. This results in a message being created in a12.c with + * "a12:dirlist" that the worker forwards verbatim, and here we are. */ + if (arg_lookup(entry, "dirlist", 0, NULL)) + dynlist_to_worker(C); + + else if (arg_lookup(entry, "diropen", 0, NULL)) + dynopen_to_worker(C, entry); + +/* missing - forward to Lua VM and appl-script if in ident */ + arg_cleanup(entry); + return; + +send_fail: + shmifsrv_enqueue_event(C->C, &(struct arcan_event){ + .category = EVENT_TARGET, + .tgt.kind = TARGET_COMMAND_MESSAGE, + .tgt.message = "a12:fail" + }, -1 + ); + arg_cleanup(entry); +} + +void handle_netstate(struct dircl* C, arcan_event ev) +{ + ev.ext.netstate.name[COUNT_OF(ev.ext.netstate.name)-1] = '\0'; + + if (ev.ext.netstate.type == 3 || ev.ext.netstate.type == 4){ + A12INT_DIRTRACE("dirsv:kind=worker:set_endpoint=%s", ev.ext.netstate.name); + C->endpoint = ev; + return; + } + + if (ev.ext.netstate.type == 1){ + A12INT_DIRTRACE("dirsv:kind=worker:register_source=%s:kind=%d", + (char*)ev.ext.netstate.name, ev.ext.netstate.type); + register_source(C, ev); + } +/* set sink key */ + else if (ev.ext.netstate.type == 2){ + A12INT_DIRTRACE("dirsv:kind=worker:update_sink_pk"); + memcpy(C->pubk, ev.ext.netstate.name, 32); + } + else + A12INT_DIRTRACE("dirsv:kind=worker:unknown_netstate"); +} + static void* dircl_process(void* P) { struct dircl* C = P; @@ -561,6 +775,8 @@ static void* dircl_process(void* P) shmifsrv_enqueue_event(C->C, &ev, -1); } +/* the applindex need to be set when the worker constructs the state machine, + * while as the list of dynamic sources happens after it is up and running */ dirlist_to_worker(C); ev.tgt.kind = TARGET_COMMAND_ACTIVATE; shmifsrv_enqueue_event(C->C, &ev, -1); @@ -574,9 +790,7 @@ static void* dircl_process(void* P) A12INT_DIRTRACE("dirsv:kind=worker:cl_join=%s", (char*)ev.ext.message.data); } else if (ev.ext.kind == EVENT_EXTERNAL_NETSTATE){ - A12INT_DIRTRACE("dirsv:kind=worker:register_source=%s:kind=%d", - (char*)ev.ext.netstate.name, ev.ext.netstate.type); - register_source(C, ev); + handle_netstate(C, ev); } /* right now we permit the worker to fetch / update their state store of any * appl as the format is id[.resource]. The other option is to use IDENT to @@ -595,77 +809,15 @@ static void* dircl_process(void* P) A12INT_DIRTRACE("dirsv:kind=worker_error:status_no_pending"); } -/* registering as a source / directory? */ - else if (ev.ext.kind == EVENT_EXTERNAL_NETSTATE){ - if (ev.ext.netstate.state == 0){ /* lost */ - } /* this is cheating a bit, SHMIF splits TARGET and EXTERNAL for (srv->cl), (cl->srv) * but by replaying like this we use EXTERNAL as (cl->srv->cl) */ - } /* the generic message passing is first used for sending and authenticating the * keys on the initial connection. If the authentication goes through and IDENT * is used to 'join' an appl the MESSAGE facility should (TOFIX) become a broadcast * domain or wrapped through a Lua VM instance as the server end of the appl. */ else if (ev.ext.kind == EVENT_EXTERNAL_MESSAGE){ - struct arg_arr* entry = arg_unpack((char*)ev.ext.message.data); - if (!entry){ - A12INT_DIRTRACE("dirsv:kind=worker:bad_msg:%s=", ev.ext.message.data); - continue; - } - -/* just route authentication through the regular function, caching the reply */ - const char* pubk; - if (!C->authenticated){ - bool send_fail = false; -/* regardless of authentication reply, they only get one attempt at this */ - C->authenticated = true; - - if (!arg_lookup(entry, "a12", 0, NULL) || !arg_lookup(entry, "pubk", 0, &pubk)){ - send_fail = true; - } - else { - uint8_t pubk_dec[32]; - if (!a12helper_fromb64((const uint8_t*) pubk, 32, pubk_dec)){ - send_fail = true; - } - else { - pthread_mutex_lock(&active_clients.sync); - struct pk_response rep = active_clients.opts->a12_cfg->pk_lookup(pubk_dec); - pthread_mutex_unlock(&active_clients.sync); - if (rep.authentic){ - struct arcan_event ev = { - .category = EVENT_TARGET, - .tgt.kind = TARGET_COMMAND_MESSAGE - }; - unsigned char* b64 = a12helper_tob64(rep.key_pub, 32, &(size_t){0}); - snprintf((char*)&ev.tgt.message, COUNT_OF(ev.tgt.message), "a12:pub=%s", b64); - free(b64); - shmifsrv_enqueue_event(C->C, &ev, -1); - b64 = a12helper_tob64(rep.key_session, 32, &(size_t){0}); - snprintf((char*)&ev.tgt.message, COUNT_OF(ev.tgt.message), "a12:ss=%s", b64); - free(b64); - shmifsrv_enqueue_event(C->C, &ev, -1); - memcpy(C->pubk, pubk_dec, 32); - } - else - send_fail = true; - } - } - - if (send_fail){ - shmifsrv_enqueue_event(C->C, &(struct arcan_event){ - .category = EVENT_TARGET, - .tgt.kind = TARGET_COMMAND_MESSAGE, - .tgt.message = "a12:fail" - }, -1); - } - } - else { - /* TOFIX: MESSAGE into broadcast or route through server-side APPL */ - } - - arg_cleanup(entry); + dircl_message(C, ev); } } @@ -681,6 +833,20 @@ static void* dircl_process(void* P) C->prev->next = C->next; if (C->next) C->next->prev = C->prev; + + /* broadcast the loss */ + struct arcan_event ev = C->petname; + ev.ext.netstate.state = 0; + + if (ev.ext.netstate.name[0]){ + tag_outbound_name(&ev, C->pubk); + struct dircl* cur = active_clients.root.next; + while (cur && ev.ext.netstate.name[0]){ + assert(cur != C); + shmifsrv_enqueue_event(cur->C, &ev, -1); + cur = cur->next; + } + } pthread_mutex_unlock(&active_clients.sync); shmifsrv_free(C->C, true); @@ -751,7 +917,8 @@ void anet_directory_shmifsrv_set(struct anet_dirsrv_opts* opts) /* This is in the parent process, it acts as a 1:1 thread/process which * pools and routes. The other end of this shmif connection is in the * normal */ -void anet_directory_shmifsrv_thread(struct shmifsrv_client* cl) +void anet_directory_shmifsrv_thread( + struct shmifsrv_client* cl, struct a12_state* S) { pthread_t pth; pthread_attr_t pthattr; @@ -759,7 +926,26 @@ void anet_directory_shmifsrv_thread(struct shmifsrv_client* cl) pthread_attr_setdetachstate(&pthattr, PTHREAD_CREATE_DETACHED); struct dircl* newent = malloc(sizeof(struct dircl)); - *newent = (struct dircl){.C = cl}; + *newent = (struct dircl){ + .C = cl, + .endpoint = { + .category = EVENT_EXTERNAL, + .ext.kind = EVENT_EXTERNAL_NETSTATE + } + }; + + const char* endpoint = a12_get_endpoint(S); + if (endpoint){ + char buf[16]; + if (inet_pton(AF_INET, endpoint, buf)){ + newent->endpoint.ext.netstate.space = 3; + } + else if (inet_pton(AF_INET6, endpoint, buf)){ + newent->endpoint.ext.netstate.space = 4; + } + snprintf((char*)newent->endpoint.ext.netstate.name, + COUNT_OF(newent->endpoint.ext.netstate.name), "%s", endpoint); + } pthread_mutex_lock(&active_clients.sync); struct dircl* cur = &active_clients.root; diff --git a/src/a12/net/dir_srv_worker.c b/src/a12/net/dir_srv_worker.c index 72c020b39..76c41eb13 100644 --- a/src/a12/net/dir_srv_worker.c +++ b/src/a12/net/dir_srv_worker.c @@ -79,16 +79,20 @@ static struct evqueue_entry* run_evqueue( * the utility is for synchronous requests to state store, key store etc. */ static bool shmif_block_synch_request(struct arcan_shmif_cont* C, - struct arcan_event ev, struct evqueue_entry* reply, int kind_ok, int kind_fail) + struct arcan_event ev, struct evqueue_entry* reply, + int cat_ok, int kind_ok, int cat_fail, int kind_fail) { *reply = (struct evqueue_entry){0}; - arcan_shmif_enqueue(C, &ev); + + if (ev.ext.kind) + arcan_shmif_enqueue(C, &ev); while (arcan_shmif_wait(C, &ev)){ - if (ev.category != EVENT_TARGET) - continue; - if (ev.tgt.kind == kind_ok || ev.tgt.kind == kind_fail){ +/* exploit the fact that kind is at the same offset regardless of union */ + if ( + (cat_ok == ev.category && ev.tgt.kind == kind_ok) || + (cat_fail == ev.category && ev.tgt.kind == kind_fail)){ reply->ev = ev; reply->next = NULL; return true; @@ -116,13 +120,16 @@ static struct a12_bhandler_res srv_bevent( /* cont is actually wrong here as we haven't set a context for the channel * since it's not being used in the normal fashion - the actual connection * to the coordinating process is through the [tag] that is also a context */ -static void on_srv_event( +static void on_a12srv_event( struct arcan_shmif_cont* cont, int chid, struct arcan_event* ev, void* tag) { struct ioloop_shared* I = tag; struct directory_meta* cbt = I->cbt; struct arcan_shmif_cont* C = cbt->C; + if (ev->category != EVENT_EXTERNAL) + return; + if (ev->ext.kind == EVENT_EXTERNAL_BCHUNKSTATE){ /* sweep the directory, and when found: */ if (!isdigit(ev->ext.bchunk.extensions[0])){ @@ -179,8 +186,16 @@ static void on_srv_event( snprintf(disc.ext.netstate.name, 16, "%s", ev->ext.registr.title); a12int_trace(A12_TRACE_DIRECTORY, "source_register=%s", disc.ext.netstate.name); + arcan_shmif_enqueue(C, &disc); } + +/* Forward messages verbatim, this also latches into the dirlist command which + * will trigger the server to re-synch dynamic sources, but it is a path to get + * external (untrusted) messages to be parsed and should be treated as poison. */ + else if (ev->ext.kind == EVENT_EXTERNAL_MESSAGE){ + arcan_shmif_enqueue(C, ev); + } } static void unpack_index( @@ -321,11 +336,22 @@ static void do_event( { struct directory_meta* cbt = C->user; -/* parent process responsible for verifying and tagging name with petname:kpub */ +/* Parent process responsible for verifying and tagging name with petname:kpub. + * the NETSTATE associated with diropen is part of the on_directory event + * handler and doesn't reach this point. */ if (ev->category == EVENT_EXTERNAL && ev->ext.kind == EVENT_EXTERNAL_NETSTATE){ size_t i = 0; + if (a12_remote_mode(S) == ROLE_SOURCE){ + struct a12_dynreq dynreq = (struct a12_dynreq){0}; + snprintf(dynreq.authk, 12, "%s", cbt->secret); + memcpy(dynreq.pubk, ev->ext.netstate.name, 32); + + a12_supply_dynamic_resource(S, dynreq); + return; + } + for (; i < COUNT_OF(ev->ext.netstate.name); i++){ if (ev->ext.netstate.name[i] == ':'){ ev->ext.netstate.name[i] = '\0'; @@ -342,7 +368,7 @@ static void do_event( a12int_notify_dynamic_resource(S, ev->ext.netstate.name, (uint8_t*)&ev->ext.netstate.name[i], - ev->ext.netstate.type, ev->ext.netstate.type != 0 + ev->ext.netstate.type, ev->ext.netstate.state != 0 ); } @@ -357,7 +383,7 @@ static void do_event( /* Would only be sent to us if the parent a. thinks we're in a specific APPL * via IDENT b. the ruleset tells us to message. Inject into a12_state - * verbatim. THe parent would also need to ensure that the client can't inject + * verbatim. The parent would also need to ensure that the client can't inject * the a12: tag into the MESSAGE or we have a weird form of 'Packets in * Packets'. */ if (!stat || !arg_lookup(stat, "a12", 0, NULL)){ @@ -367,6 +393,12 @@ static void do_event( return; } + const char* secret; + if (arg_lookup(stat, "dir_secret", 0, &secret) && secret){ + free(cbt->secret); + cbt->secret = strdup(secret); + } + /* reserved for other messages */ arg_cleanup(stat); } @@ -464,6 +496,98 @@ static struct pk_response key_auth_worker(uint8_t pk[static 32]) return reply; } +static bool req_open(struct a12_state* S, + uint8_t ident_pubk[static 32], uint8_t ident_req[static 32], + uint8_t mode, + struct a12_dynreq* out, void* tag) +{ + struct directory_meta* cbt = tag; + +/* This is annoying as the sum of the key sizes doesn't fit one message with + * both keys and we don't have a filesystem to use for temporary storage due to + * the sandboxing. Thus we use one netstate to set the current diropen key. + * + * (ok case) + * [wrk_sink] (NETSTATE:space=5:sink) + message(diropen:req) -> [srv] -> + * NETSTATE:space=1:host + NETSTATE:sink -> [wrk_source] + * NETSTATE:space=1:host + NETSTATE:source -> [wrk_sink] + * - connection is setup - + * + * (fail case) + * [wrk_sink] (NETSTATE:sink) + message(diropen:req) -> [srv] -> NETSTATE:lost(pubk) + * + */ + + uint8_t nullk[32] = {0}; + if (memcmp(nullk, ident_pubk, 32) != 0){ + arcan_event idev = { + .category = EVENT_EXTERNAL, + .ext.kind = EVENT_EXTERNAL_NETSTATE, + .ext.netstate = { + .type = 1, + .space = 5 + } + }; + memcpy(idev.ext.netstate.name, ident_pubk, 32); + arcan_shmif_enqueue(cbt->C, &idev); + } + + size_t outl; + unsigned char* req_b64 = a12helper_tob64(ident_req, 32, &outl); + arcan_event reqmsg = { + .category = EVENT_EXTERNAL, + .ext.kind = EVENT_EXTERNAL_MESSAGE + }; + snprintf((char*)reqmsg.ext.message.data, + COUNT_OF(reqmsg.ext.message.data), "a12:diropen:pubk=%s", req_b64); + free(req_b64); + arcan_shmif_enqueue(cbt->C, &reqmsg); + + struct evqueue_entry* rep = malloc(sizeof(struct evqueue_entry)); + bool rv; + +/* just fill-out the dynreq with our connection info. some of this, mainly the + * shared secret, this is delivered as a message re-using the session shared + * secret that was used in authentication. */ +retry_block: + if ((rv = shmif_block_synch_request(cbt->C, + reqmsg, rep, + EVENT_EXTERNAL, EVENT_EXTERNAL_NETSTATE, + EVENT_TARGET, TARGET_COMMAND_REQFAIL))){ + a12int_trace(A12_TRACE_DIRECTORY, "diropen:got_reply"); + arcan_event repev = (run_evqueue(cbt->S, cbt->C, rep))->ev; + +/* if it's not the netstate we're looking for, try again */ + if (repev.ext.netstate.space == 5){ + free_evqueue(rep); + rep = malloc(sizeof(struct evqueue_entry)); + goto retry_block; + } + + struct a12_dynreq rq = { + .port = 6680, + .proto = 1 + }; + +/* split out the port */ + if (repev.ext.netstate.port) + rq.port = repev.ext.netstate.port; + + if (cbt->secret) + snprintf(rq.authk, 12, "%s", cbt->secret); + + _Static_assert(sizeof(rq.host) == 46); + strncpy(rq.host, repev.ext.netstate.name, 45); + a12_supply_dynamic_resource(S, rq); + } + else { + a12int_trace(A12_TRACE_DIRECTORY, "diropen:kind=rejected"); + } + + free_evqueue(rep); + return rv; +} + void anet_directory_srv( struct a12_context_options* netopts, struct anet_dirsrv_opts opts, int fdin, int fdout) { @@ -476,6 +600,7 @@ void anet_directory_srv( struct arg_arr* args; a12int_trace(A12_TRACE_DIRECTORY, "notice:directory-ready:pid=%d", getpid()); + setenv("ARCAN_SHMIF_DEBUG", "1", true); shmif_parent_process = arcan_shmif_open( @@ -527,12 +652,19 @@ void anet_directory_srv( a12_set_bhandler(S, srv_bevent, &cbt); + a12_set_destination_raw(S, 0, + (struct a12_unpack_cfg){ + .directory_open = req_open, + .tag = &cbt + }, sizeof(struct a12_unpack_cfg) + ); + struct ioloop_shared ioloop = { .S = S, .fdin = fdin, .fdout = fdout, .userfd = shmif_parent_process.epipe, - .on_event = on_srv_event, + .on_event = on_a12srv_event, .on_userfd = on_shmif, .lock = PTHREAD_MUTEX_INITIALIZER, .cbt = &cbt, @@ -575,7 +707,9 @@ static int request_parent_resource( int fd = -1; a12int_trace(A12_TRACE_DIRECTORY, "request_parent:%s", ev.ext.bchunk.extensions); - if (shmif_block_synch_request(C, ev, rep, kind, TARGET_COMMAND_REQFAIL)){ + if (shmif_block_synch_request(C, ev, rep, + EVENT_TARGET, kind, + EVENT_TARGET, TARGET_COMMAND_REQFAIL)){ struct evqueue_entry* cur = run_evqueue(S, C, rep); if (cur->ev.tgt.kind == kind){ diff --git a/src/a12/net/directory.h b/src/a12/net/directory.h index 0ab24aba8..da33853d1 100644 --- a/src/a12/net/directory.h +++ b/src/a12/net/directory.h @@ -44,7 +44,7 @@ struct anet_dircl_opts { bool keep_appl; char ident[16]; - void (*dir_source)(struct a12_state*, int fd, void* tag); + void (*dir_source)(struct a12_state*, struct a12_dynreq req, void* tag); void *dir_source_tag; struct appl_meta outapp; @@ -58,6 +58,7 @@ struct directory_meta { struct a12_state* S; struct anet_dircl_opts* clopt; + char* secret; bool in_transfer; uint32_t transfer_id; @@ -80,7 +81,7 @@ void anet_directory_srv( /* * shmif connection to map to a thread for coordination */ -void anet_directory_shmifsrv_thread(struct shmifsrv_client*); +void anet_directory_shmifsrv_thread(struct shmifsrv_client*, struct a12_state*); void anet_directory_shmifsrv_set(struct anet_dirsrv_opts* opts); /* diff --git a/src/a12/net/net.c b/src/a12/net/net.c index 217073539..4170222b0 100644 --- a/src/a12/net/net.c +++ b/src/a12/net/net.c @@ -193,6 +193,10 @@ static int get_bcache_dir() return open(base, O_DIRECTORY | O_CLOEXEC); } +#ifdef DEBUG +extern void shmifint_set_log_device(struct arcan_shmif_cont*, FILE*); +#endif + static void set_log_trace() { #ifdef DEBUG @@ -202,6 +206,9 @@ static void set_log_trace() char buf[sizeof("cl_log_xxxxxx.log")]; snprintf(buf, sizeof(buf), "cl_log_%.6d.log", (int) getpid()); FILE* fpek = fopen(buf, "w+"); + + shmifint_set_log_device(NULL, fpek); + if (fpek){ a12_set_trace_level(a12_trace_targets, fpek); } @@ -230,7 +237,7 @@ static void fork_a12srv(struct a12_state* S, int fd, void* tag) char* argv[] = {global.path_self, "-d", tmptrace, "-S", tmpfd, NULL, NULL}; - /* shmif-server lib will get to waitpid / kill so we don't need to care here */ +/* shmif-server lib will get to waitpid / kill so we don't need to care here */ struct shmifsrv_envp env = { .path = global.path_self, .envv = NULL, @@ -240,7 +247,7 @@ static void fork_a12srv(struct a12_state* S, int fd, void* tag) cl = shmifsrv_spawn_client(env, &clsock, NULL, 0); if (cl){ - anet_directory_shmifsrv_thread(cl); + anet_directory_shmifsrv_thread(cl, S); } a12_channel_close(S); @@ -411,9 +418,12 @@ static void single_a12srv(struct a12_state* S, int fd, void* tag) } } -static void dir_to_shmifsrv(struct a12_state* S, int fd, void* tag) +static void dir_to_shmifsrv(struct a12_state* S, struct a12_dynreq, void* tag) { a12int_trace(A12_TRACE_DIRECTORY, "open_request_negotiated"); +/* here, we fork() into listening on our registered port, with the prefilled + * authk and a specialised key-auth that only ephemerally accepts (unless set + * to trust-transitive), passing the shmifsrv connection along */ } static void a12cl_dispatch( diff --git a/src/shmif/arcan_shmif_event.h b/src/shmif/arcan_shmif_event.h index 97b255946..326e0f618 100644 --- a/src/shmif/arcan_shmif_event.h +++ b/src/shmif/arcan_shmif_event.h @@ -1264,6 +1264,7 @@ enum ARCAN_TARGET_SKIPMODE { struct { uint32_t hitag, lotag; } tagv; + struct { char* dyneval_msg; } mesg; @@ -1482,6 +1483,7 @@ enum ARCAN_TARGET_SKIPMODE { uint8_t space; uint8_t state; uint8_t type; + uint16_t port; } netstate; /*