From 792626a50a505e968de0f67eff50819445ea2c91 Mon Sep 17 00:00:00 2001 From: bjornstahl Date: Sun, 22 Oct 2023 22:25:17 +0200 Subject: [PATCH] (a12) initial directory-tunnel working With this patch the end for tunnel juggling is in sight. This lands the full path from a client requesting that a source be opened in tunnel-mode. The server main process splits out a socket pair and sends to the workers. The workers adds that to the i/o multiplexing and reserves channel-1 for direct forwarding by piggybacking on bstream- transfers. The sink end spawns off a thread that latches into a tunnel pair and feeds that into a socket that authenticates and maps to a regular a12-cl-to-shmif connection, with corresponding feed into the tunnel side of that a12 state machine. The source end double-forks off (* this does perform some of the illegal from-fork actions) and performs a similar dance, though no added threading. A12_IDENT=test ARCAN_CONNPATH=a12://mydirsrv afsrv_terminal arcan-net --tunnel mydirsrv "*test" should be roughly what is needed assuming an up to date and compliant directory server. --- src/a12/a12.c | 55 +++++++++++++++++++++---- src/a12/a12.h | 2 +- src/a12/a12_int.h | 4 +- src/a12/net/dir_cl.c | 58 +++++++++++++++++++++++++- src/a12/net/dir_srv.c | 34 ++++++++++++--- src/a12/net/dir_srv_worker.c | 38 ++++++++++------- src/a12/net/dir_supp.c | 25 +++++++---- src/a12/net/directory.h | 2 + src/a12/net/net.c | 80 +++++++++++++++++++++++++++++------- 9 files changed, 244 insertions(+), 54 deletions(-) diff --git a/src/a12/a12.c b/src/a12/a12.c index d55300df9..f33a10513 100644 --- a/src/a12/a12.c +++ b/src/a12/a12.c @@ -278,7 +278,7 @@ static void send_hello_packet(struct a12_state* S, * reordering would then still need to account for rekeying. */ void a12int_append_out(struct a12_state* S, uint8_t type, - uint8_t* out, size_t out_sz, uint8_t* prepend, size_t prepend_sz) + const uint8_t* const out, size_t out_sz, uint8_t* prepend, size_t prepend_sz) { if (S->state == STATE_BROKEN) return; @@ -500,6 +500,10 @@ static struct a12_state* a12_setup(struct a12_context_options* opt, bool srv) .server = srv }; + for (size_t i = 0; i <= 255; i++){ + res->channels[i].unpack_state.bframe.tmp_fd = -1; + } + size_t len = 0; res->opts = DYNAMIC_MALLOC(sizeof(struct a12_context_options)); if (!res->opts){ @@ -535,6 +539,7 @@ static struct a12_state* a12_setup(struct a12_context_options* opt, bool srv) res->cookie = 0xfeedface; res->out_stream = 1; + res->notify_dynamic = true; return res; } @@ -2184,7 +2189,7 @@ static void process_blob(struct a12_state* S) } struct arcan_shmif_cont* cont = S->channels[S->in_channel].cont; - if (!cont && !S->binary_handler){ + if (!cont && !S->binary_handler && !cbf->tunnel){ a12int_trace(A12_TRACE_SYSTEM, "kind=error:status=EINVAL:" "ch=%d:message=no segment or bhandler mapped", S->in_channel); reset_state(S); @@ -2288,7 +2293,7 @@ static void process_blob(struct a12_state* S) if (free_buf) DYNAMIC_FREE(buf); - if (!S->binary_handler) + if (!S->binary_handler || cbf->tunnel) return; /* is it a streaming transfer or a known size? */ @@ -3448,18 +3453,52 @@ void a12_supply_dynamic_resource(struct a12_state* S, struct a12_dynreq r) } bool - a12_write_tunnel( - struct a12_state* S, uint8_t chid, const char* const buf, size_t buf_sz) + a12_write_tunnel(struct a12_state* S, + uint8_t chid, const uint8_t* const buf, size_t buf_sz) { - return false; + if (!buf_sz) + return false; + + if (!S->channels[chid].active){ + a12int_trace(A12_TRACE_BTRANSFER, "write_tunnel:bad_channel=%"PRIu8, chid); + return false; + } + +/* tunnel packet is a simpler form of binary stream with no rampup, + * multiplexing, checksum, compression, cancellation, ... just straight into + * channel */ + uint8_t outb[1 + 4 + 2] = {0}; + outb[0] = chid; + pack_u16(buf_sz, &outb[5]); + a12int_append_out(S, STATE_BLOB_PACKET, buf, buf_sz, outb, sizeof(outb)); + + a12int_trace( + A12_TRACE_BTRANSFER, "write_tunnel:ch=%"PRIu8":nb=%zu", chid, buf_sz); + return true; } bool a12_set_tunnel_sink(struct a12_state* S, uint8_t chid, int fd) { - if (!S->channels[chid].active) + if (S->channels[chid].active){ + a12int_trace(A12_TRACE_DIRECTORY, "swap_sink:chid=%"PRIu8, chid); + if (0 < S->channels[chid].unpack_state.bframe.tmp_fd) + close(S->channels[chid].unpack_state.bframe.tmp_fd); return false; + } - S->channels[chid].unpack_state.bframe.tmp_fd = fd; + if (-1 == fd){ + S->channels[chid].active = false; + S->channels[chid].unpack_state.bframe.tunnel = false; + S->channels[chid].unpack_state.bframe.tmp_fd = -1; + return true; + } + + S->channels[chid].active = true; + S->channels[chid].unpack_state.bframe = (struct binary_frame){ + .tmp_fd = fd, + .tunnel = true, + .active = true + }; return true; } diff --git a/src/a12/a12.h b/src/a12/a12.h index f80851e79..7ad4330bf 100644 --- a/src/a12/a12.h +++ b/src/a12/a12.h @@ -307,7 +307,7 @@ a12_enqueue_blob( * request_dynamic_resource when there is no direct / usable network path. * Returns false if the channel isn't mapped for that kind of use. */ bool - a12_write_tunnel(struct a12_state*, uint8_t chid, const char* const, size_t); + a12_write_tunnel(struct a12_state*, uint8_t chid, const uint8_t* const, size_t); bool a12_set_tunnel_sink(struct a12_state*, uint8_t chid, int fd); diff --git a/src/a12/a12_int.h b/src/a12/a12_int.h index f7753ab29..55581c874 100644 --- a/src/a12/a12_int.h +++ b/src/a12/a12_int.h @@ -137,6 +137,7 @@ struct binary_frame { int tmp_fd; int type; bool active; + bool tunnel; uint64_t size; uint32_t identifier; uint8_t checksum[16]; @@ -354,7 +355,8 @@ void a12int_stream_fail(struct a12_state* S, uint8_t ch, uint32_t id, int fail); void a12int_stream_ack(struct a12_state* S, uint8_t ch, uint32_t id); void a12int_append_out( - struct a12_state* S, uint8_t type, uint8_t* out, size_t out_sz, + struct a12_state* S, uint8_t type, + const uint8_t* const out, size_t out_sz, uint8_t* prepend, size_t prepend_sz); void a12int_step_vstream(struct a12_state* S, uint32_t id); diff --git a/src/a12/net/dir_cl.c b/src/a12/net/dir_cl.c index 621ad6036..0fa5965cc 100644 --- a/src/a12/net/dir_cl.c +++ b/src/a12/net/dir_cl.c @@ -45,6 +45,48 @@ struct appl_runner_state { int p_stdin; }; +struct tunnel_state { + struct a12_context_options opts; + struct a12_dynreq req; + int fd; +}; + +static void* tunnel_runner(void* t) +{ + struct tunnel_state* ts = t; + char* err = NULL; + struct a12_state* S = a12_client(&ts->opts); + + if (anet_authenticate(S, ts->fd, ts->fd, &err)){ + a12helper_a12srv_shmifcl(NULL, S, NULL, ts->fd, ts->fd); + } + else { + } + + shutdown(ts->fd, SHUT_RDWR); + close(ts->fd); + free(err); + free(ts); + + return NULL; +} + +static void detach_tunnel_runner( + int fd, struct a12_context_options* aopt, struct a12_dynreq* req) +{ + struct tunnel_state* ts = malloc(sizeof(struct tunnel_state)); + ts->opts = *aopt; + ts->req = *req; + ts->opts.pk_lookup_tag = &ts->req; + ts->fd = fd; + + pthread_t pth; + pthread_attr_t pthattr; + pthread_attr_init(&pthattr); + pthread_attr_setdetachstate(&pthattr, PTHREAD_CREATE_DETACHED); + pthread_create(&pth, &pthattr, tunnel_runner, ts); +} + /* * The processing here is a bit problematic. One is that we still use a socket * pair rather than a shmif connection. If this connection is severed or the @@ -82,6 +124,8 @@ static struct pk_response key_auth_fixed(uint8_t pk[static 32], void* tag) */ static void on_source(struct a12_state* S, struct a12_dynreq req, void* tag) { + struct ioloop_shared* I = tag; + /* security: * disable the ephemeral exchange for now, this means the announced identity * when we connect to the directory server will be the one used for the x25519 @@ -96,6 +140,7 @@ static void on_source(struct a12_state* S, struct a12_dynreq req, void* tag) char port[sizeof("65535")]; snprintf(port, sizeof(port), "%"PRIu16, req.port); + snprintf(a12opts.secret, sizeof(a12opts.secret), "%s", req.authk); struct anet_options anet = { .retry_count = 10, @@ -104,7 +149,18 @@ static void on_source(struct a12_state* S, struct a12_dynreq req, void* tag) .port = port }; - snprintf(a12opts.secret, sizeof(a12opts.secret), "%s", req.authk); + if (req.proto == 4){ + int sv[2]; + if (0 != socketpair(AF_UNIX, SOCK_STREAM, 0, sv)){ + a12int_trace(A12_TRACE_DIRECTORY, "tunnel_socketpair_fail"); + return; + } + + a12_set_tunnel_sink(S, 1, sv[0]); + detach_tunnel_runner(sv[1], &a12opts, &req); + return; + } + struct anet_cl_connection con = anet_cl_setup(&anet); if (con.errmsg || !con.state){ fprintf(stderr, "%s", con.errmsg ? con.errmsg : "broken connection state\n"); diff --git a/src/a12/net/dir_srv.c b/src/a12/net/dir_srv.c index 4786952aa..e13576fdc 100644 --- a/src/a12/net/dir_srv.c +++ b/src/a12/net/dir_srv.c @@ -194,17 +194,39 @@ static void dynopen_to_worker(struct dircl* C, struct arg_arr* entry) .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 + * tunnel or .. */ + arcan_event to_sink = cur->endpoint; + +/* for now blindly accept tunneling if requested and permitted */ + if (arg_lookup(entry, "tunnel", 0, NULL)){ + if (!active_clients.opts->allow_tunnel) + goto send_fail; + + int sv[2]; + if (0 != socketpair(AF_UNIX, SOCK_STREAM, 0, sv)) + goto send_fail; + arcan_event ts = { + .category = EVENT_TARGET, + .tgt.kind = TARGET_COMMAND_BCHUNK_IN, + .tgt.message = ".tun" + }; + shmifsrv_enqueue_event(cur->C, &ts, sv[0]); + shmifsrv_enqueue_event(C->C, &ts, sv[1]); + close(sv[0]); + close(sv[1]); + } + + memcpy(to_src.ext.netstate.name, C->pubk, 32); + +/* + * 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 @@ -360,7 +382,7 @@ static void register_source(struct dircl* C, struct arcan_event ev) { if (!a12helper_keystore_accepted(C->pubk, active_clients.opts->allow_src)){ unsigned char* b64 = a12helper_tob64(C->pubk, 32, &(size_t){0}); - + A12INT_DIRTRACE( "dirsv:kind=reject_register:title=%s:role=%d:eperm:key=%s", ev.ext.registr.title, diff --git a/src/a12/net/dir_srv_worker.c b/src/a12/net/dir_srv_worker.c index d9fe8194f..807f3fcdb 100644 --- a/src/a12/net/dir_srv_worker.c +++ b/src/a12/net/dir_srv_worker.c @@ -32,6 +32,8 @@ static struct arcan_shmif_cont shmif_parent_process; static struct a12_state* active_client_state; static struct appl_meta* pending_index; +static struct ioloop_shared* ioloop_shared; +static bool pending_tunnel; static void do_event( struct a12_state* S, struct arcan_shmif_cont* C, struct arcan_event* ev); @@ -294,9 +296,8 @@ static void bchunk_event(struct a12_state *S, * ones are not difficult as such but evaluate the need experimentally first. */ else if (strcmp(ev->tgt.message, ".tun") == 0){ a12int_trace(A12_TRACE_DIRECTORY, "worker:tunnel_acquired:channel=1"); - S->channels[1].active = true; - S->channels[1].unpack_state.bframe.tmp_fd = - arcan_shmif_dupfd(ev->tgt.ioevs[0].iv, -1, true); + a12_set_tunnel_sink(S, 1, arcan_shmif_dupfd(ev->tgt.ioevs[0].iv, -1, true)); + pending_tunnel = true; } } @@ -353,10 +354,17 @@ static void do_event( if (a12_remote_mode(S) == ROLE_SOURCE){ a12int_trace(A12_TRACE_DIRECTORY, "open_to_src"); + struct a12_dynreq dynreq = (struct a12_dynreq){0}; snprintf(dynreq.authk, 12, "%s", cbt->secret); memcpy(dynreq.pubk, ev->ext.netstate.name, 32); + if (pending_tunnel){ + a12int_trace(A12_TRACE_DIRECTORY, "diropen:tunnel_src"); + dynreq.proto = 4; + pending_tunnel = false; + } + a12_supply_dynamic_resource(S, dynreq); return; } @@ -567,16 +575,14 @@ static bool dirsrv_req_open(struct a12_state* S, _Static_assert(sizeof(rq.host) == 46, "wrong host-length"); -/* reserved .tun as hostname is to tell that we have set a channel as tunnel, - * then spawn a processing thread that reads from the tunnel and injects into - * the state machine. */ - if (strcmp(repev.ext.netstate.name, ".tun") == 0){ - a12int_trace(A12_TRACE_DIRECTORY, "diropen:tunnel"); - rq.proto = 3; - pthread_t pth; - pthread_attr_t pthattr; - pthread_attr_init(&pthattr); - pthread_attr_setdetachstate(&pthattr, PTHREAD_CREATE_DETACHED); + /* if there is a tunnel pending (would arrive as a bchunkstate during + * block_synch_request) tag the proto accordingly and spawn our feeder with + * the src descriptor already being set in the thread. */ + if (pending_tunnel){ + a12int_trace(A12_TRACE_DIRECTORY, "diropen:tunnel_sink"); + rq.proto = 4; + *out = rq; + pending_tunnel = false; return rv; } @@ -600,10 +606,9 @@ void anet_directory_srv( netopts->pk_lookup = key_auth_worker; struct anet_dirsrv_opts diropts = {}; - 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( @@ -672,6 +677,9 @@ void anet_directory_srv( .cbt = &cbt, }; + ioloop_shared = &ioloop; + + /* this will loop until client shutdown */ anet_directory_ioloop(&ioloop); arcan_shmif_drop(&shmif_parent_process); diff --git a/src/a12/net/dir_supp.c b/src/a12/net/dir_supp.c index d5f0f648e..c23f1680a 100644 --- a/src/a12/net/dir_supp.c +++ b/src/a12/net/dir_supp.c @@ -32,11 +32,12 @@ void anet_directory_ioloop(struct ioloop_shared* I) { int errmask = POLLERR | POLLNVAL | POLLHUP; - struct pollfd fds[3] = + struct pollfd fds[4] = { {.fd = I->userfd, .events = POLLIN | errmask}, {.fd = I->fdin, .events = POLLIN | errmask}, - {.fd = -1, .events = POLLOUT | errmask} + {.fd = -1, .events = POLLOUT | errmask}, + {.fd = -1, .events = POLLIN | errmask}, }; uint8_t inbuf[9000]; @@ -52,12 +53,11 @@ void anet_directory_ioloop(struct ioloop_shared* I) fds[2].fd = I->fdout; /* regular simple processing loop, wait for DIRECTORY-LIST command */ - while (a12_ok(I->S) && -1 != poll(fds, 3, -1)){ - if ((fds[0].revents | fds[1].revents | fds[2].revents) & errmask){ + while (a12_ok(I->S) && -1 != poll(fds, 4, -1)){ + if ((fds[0].revents | fds[1].revents | fds[2].revents | fds[3].revents) & errmask){ if (fds[0].revents & errmask){ I->on_userfd(I, false); } - break; } @@ -65,6 +65,13 @@ void anet_directory_ioloop(struct ioloop_shared* I) I->on_userfd(I, true); } + if (fds[3].revents & POLLIN){ + uint8_t buf[8832]; + int fd = I->S->channels[1].unpack_state.bframe.tmp_fd; + ssize_t sz = read(fd, buf, sizeof(buf)); + a12_write_tunnel(I->S, 1, buf, (size_t) sz); + } + if ((fds[2].revents & POLLOUT) && outbuf_sz){ ssize_t nw = write(I->fdout, outbuf, outbuf_sz); if (nw > 0){ @@ -75,6 +82,7 @@ void anet_directory_ioloop(struct ioloop_shared* I) if (fds[1].revents & POLLIN){ ssize_t nr = recv(I->fdin, inbuf, 9000, 0); + if (-1 == nr && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR){ a12int_trace(A12_TRACE_DIRECTORY, "shutdown:reason=rw_error"); break; @@ -92,7 +100,7 @@ void anet_directory_ioloop(struct ioloop_shared* I) if (new_ts != ts){ ts = new_ts; if (!I->on_directory(I, dir)) - return; + break; } } } @@ -103,9 +111,12 @@ void anet_directory_ioloop(struct ioloop_shared* I) break; } - fds[0].revents = fds[1].revents = fds[2].revents = 0; + fds[0].revents = fds[1].revents = fds[2].revents = fds[3].revents = 0; fds[2].fd = outbuf_sz ? I->fdout : -1; fds[0].fd = I->userfd; + fds[3].fd = + I->S->channels[1].unpack_state.bframe.tunnel ? + I->S->channels[1].unpack_state.bframe.tmp_fd : -1; } } diff --git a/src/a12/net/directory.h b/src/a12/net/directory.h index d4f90c35a..72fe8aae0 100644 --- a/src/a12/net/directory.h +++ b/src/a12/net/directory.h @@ -112,6 +112,7 @@ struct ioloop_shared { int fdin; int fdout; int userfd; + pthread_mutex_t lock; struct a12_state *S; volatile bool shutdown; @@ -127,5 +128,6 @@ struct ioloop_shared { void* tag; }; +void anet_directory_tunnel_thread(struct ioloop_shared* ios, struct a12_state* S); void anet_directory_ioloop(struct ioloop_shared* S); #endif diff --git a/src/a12/net/net.c b/src/a12/net/net.c index 5a8eee8c0..4a0e71247 100644 --- a/src/a12/net/net.c +++ b/src/a12/net/net.c @@ -213,7 +213,7 @@ static void set_log_trace() FILE* fpek = fopen(buf, "w+"); shmifint_set_log_device(NULL, fpek); - + setvbuf(fpek, NULL, _IOLBF, 0); if (fpek){ a12_set_trace_level(a12_trace_targets, fpek); } @@ -506,7 +506,7 @@ static void dir_a12srv(struct a12_state* S, int fd, void* tag) /* should not be able to happen at this stage, hello won't go through without * the right authk and if you have that you're either source, sink or dirsrv - * and both sink and dirsrv would be able to math the pubk. */ + * and both sink and dirsrv would be able to match the pubk. */ char* msg; if (!anet_authenticate(S, fd, fd, &msg)){ a12int_trace(A12_TRACE_SECURITY, "authentication_failed"); @@ -534,6 +534,22 @@ static void dir_to_shmifsrv(struct a12_state* S, struct a12_dynreq a, void* tag) struct dirstate* ds = tag; ds->req = a; +/* main difference here is that we need to wrap the packets coming in and out + * of the forked child, thus create a socketpair, set one part of the pair as + * the a12_channel bstream sink and the other with the supported read into + * state part. */ + int pre_fd = -1; + int sv[2]; + + if (a.proto == 4){ + if (0 != socketpair(AF_UNIX, SOCK_STREAM, 0, sv)){ + a12int_trace(A12_TRACE_DIRECTORY, "tunnel_socketpair_fail"); + return; + } + a12_set_tunnel_sink(S, 1, sv[0]); + pre_fd = sv[1]; + } + pid_t fpid = fork(); /* Here, we fork() into listening on our registered port, this is the same as @@ -543,9 +559,29 @@ static void dir_to_shmifsrv(struct a12_state* S, struct a12_dynreq a, void* tag) * * When things are a bit more robust, switch to the fork-fexec self approach * anyhow for IPC cleanliness, it's just inheriting the shmifsrc-client setup - * that is a bit of a hassle. */ - + * that is a bit of a hassle. The crutch there is the normal one - we can't + * just build a shmifsrv context from the memory page alone due to the transfer + * socket (doable) and the semaphores where OSX compatibility comes back to + * bite us again and again. Assuming Macs won't ever get any real OS-dev work + * again, the option would be to let them take the punch and swap to blocking + * on the socket and send [a/v/e] unlocks into local mutexes that way because + * this is getting ridiculous. + * + * On the other hand, the main use for this dance is to let the same shmif + * context be re-used with the next directory open request. This can be + * achieved by firing up a new connection point (so -s random), setting that as + * the fallback and letting the tunnel pipe loss capture that. + */ if (fpid == 0){ + struct sigaction oldsig; + sigaction(SIGINT, &(struct sigaction){}, &oldsig); + close(sv[0]); + close(ds->fd); + + if (fork()){ + exit(EXIT_SUCCESS); + } + /* Trust the directory server provided secret. * * A nuanced option here is if to set the accept_n_pk_unknown to 1 or not. @@ -588,7 +624,23 @@ static void dir_to_shmifsrv(struct a12_state* S, struct a12_dynreq a, void* tag) * host so that we don't try to bind the old outbound ip. */ char* errmsg = NULL; ds->aopts->host = NULL; - anet_listen(ds->aopts, &errmsg, dir_a12srv, ds); + +/* With tunnel mode we have a socketpair pre-created, where one end is set as + * the tunnel-channel sink for the a12_state machine to the directory, and + * the other is fed into the channel. + * + * Without tunnel-mode we listen for an inbound connection (or make an outbound + * or send an outbound then listen for an inbound). The mentally more complex + * dance is what happens if we tunnel through a directory that we tunnel .. + */ + if (pre_fd == -1){ + anet_listen(ds->aopts, &errmsg, dir_a12srv, ds); + } + else { + struct a12_state* ast = a12_server(ds->aopts->opts); + dir_a12srv(ast, pre_fd, ds); + } + fprintf(stderr, "%s", errmsg ? errmsg : ""); exit(EXIT_SUCCESS); } @@ -599,19 +651,17 @@ static void dir_to_shmifsrv(struct a12_state* S, struct a12_dynreq a, void* tag) close(ds->fd); return; } +/* In order for tunnel mode to work we need to keep the directory server and + * the ioloop alive. If we inherit a -S due to an ARCAN_CONNPATH or devicehint + * keeping the shmif-context in order to re-share it is also a possibility. */ else { -/* just ignore and return to caller, we might need a monitoring channel for - * multiplexing in tunnel- traffic - and in those cases we need to keep the - * ds->fd (directory connection) alive. the same goes for --exec mode, as we - * can supply more clients through the same directory setup we can keep it - * alive, but that has more considerations -- how many do we want? the other - * connections might require tunneling, so at first it is better to just go 1:1 - * for dirsrv connection and --exec pass. */ + if (pre_fd != -1){ + close(pre_fd); + } + a12int_trace(A12_TRACE_SYSTEM, "client handed off to %d", (int)fpid); - shmifsrv_free(ds->shmif, SHMIFSRV_FREE_LOCAL); - shutdown(ds->fd, SHUT_RDWR); +/* child double-forked so just collect the first */ wait(NULL); - close(ds->fd); } }