summaryrefslogtreecommitdiffstats
path: root/oikobusd.c
diff options
context:
space:
mode:
Diffstat (limited to 'oikobusd.c')
-rw-r--r--oikobusd.c1329
1 files changed, 1329 insertions, 0 deletions
diff --git a/oikobusd.c b/oikobusd.c
new file mode 100644
index 0000000..10041d8
--- /dev/null
+++ b/oikobusd.c
@@ -0,0 +1,1329 @@
+/* See LICENSE file for copyright and license details. */
+#include <sys/auxv.h>
+#include <sys/epoll.h>
+#include <sys/timerfd.h>
+#include <libblake.h>
+#include <libsimple.h>
+#include <libsimple-arg.h>
+
+USAGE("[-a unix-address] [-c config-file] [-p udp-port]");
+
+
+#define UDP_PORT 42712
+#define LOCAL_ADDRESS_PREFIX "@oikobus-"
+
+#define LOCAL_ADDRESS_BUFSIZE (sizeof(LOCAL_ADDRESS_PREFIX) + 3u * sizeof(uintmax_t))
+#define LOCAL_ADDRESS(UID) "%s%ju", LOCAL_ADDRESS_PREFIX, (uintmax_t)(UID)
+
+#define SOCKADDR(ADDR) ((void *)&(ADDR)), ((socklen_t)sizeof(ADDR))
+#define ALPHA64 "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz{}"
+#define MIN_REALM_LEN 1u
+#define MAX_REALM_LEN 510u
+#define CHALLENGE_SIZE 64u /* must be within [1 64] (BLAKE2b key bounds) */
+#define PROOF_SIZE 64u /* must be within [1 64] (BLAKE2b digest bounds) */
+#define CHALLENGE_PEPPER "\xa1\x7d\xcc\x75\x51\xd9\xc2\x2b\x63\xf9\x45\x83\x4b\x4d\xae\x4c"
+
+
+union file;
+
+
+enum file_type {
+ TIMER_FILE,
+ BROADCAST_FILE,
+ NETWORK_FILE,
+ LOCAL_FILE,
+ PEER_FILE,
+ CLIENT_FILE
+};
+
+#define FILE_COMMON\
+ enum file_type type;\
+ int fd;\
+ void (*on_event)(union file *this, uint32_t epoll_events)
+
+struct timer_file {
+ FILE_COMMON;
+ union file *owner;
+};
+
+struct broadcast_file {
+ FILE_COMMON;
+ struct sockaddr_in addr;
+ struct timer_file timer;
+ int64_t timer_inc;
+ struct timespec alarm_time;
+};
+
+struct network_file {
+ FILE_COMMON;
+};
+
+struct local_file {
+ FILE_COMMON;
+};
+
+struct peer_file {
+ FILE_COMMON;
+ uint64_t id;
+ struct sockaddr_in addr;
+ uint8_t *realms;
+ struct handshake_state {
+#define HANDSHAKE_A_INIT HANDSHAKE_A_SEND_ID
+#define HANDSHAKE_B_INIT HANDSHAKE_B_SEND_ID
+ enum handshake_status {
+ HANDSHAKE_A_SEND_ID,
+ HANDSHAKE_A_RECV_ID,
+ HANDSHAKE_A_SEND_REALM,
+ HANDSHAKE_A_RECV_CHALLENGE,
+ HANDSHAKE_A_SEND_PROOF,
+ HANDSHAKE_A_RECV_ACCEPTANCE,
+ HANDSHAKE_B_SEND_ID,
+ HANDSHAKE_B_RECV_ID,
+ HANDSHAKE_B_RECV_REALM,
+ HANDSHAKE_B_SEND_CHALLENGE,
+ HANDSHAKE_B_RECV_PROOF,
+ HANDSHAKE_B_SEND_ACCEPTANCE
+ } status;
+ size_t realm;
+ uint16_t off;
+ uint16_t len;
+ uint16_t head;
+ uint16_t tail;
+ unsigned char buf[512];
+ unsigned char sbuf[512];
+ } *handshake_state;
+};
+
+struct client_file {
+ FILE_COMMON;
+ struct client_file *prev;
+ struct client_file *next;
+};
+
+union file {
+ struct { FILE_COMMON; };
+ struct timer_file timer;
+ struct broadcast_file broadcast;
+ struct network_file network;
+ struct local_file local;
+ struct peer_file peer;
+ struct client_file client;
+};
+
+
+struct broadcast_message {
+#define BROADCAST_MESSAGE_SIZE 10
+ uint64_t id;
+ uint16_t port;
+};
+
+
+struct realm {
+ char *name;
+ char *key;
+};
+
+
+static struct realm *realms = NULL;
+static size_t nrealms = 0u;
+static struct broadcast_message server_identity;
+static int epoll;
+static struct epoll_event *events;
+static int events_size;
+static struct local_file *lsock;
+static struct network_file *nsock;
+static struct broadcast_file *bsock;
+static struct peer_file **peers;
+static size_t npeers;
+static size_t peers_size;
+static uid_t uid;
+static struct client_file clients_head;
+static struct client_file clients_tail;
+
+
+static void
+remove_peer(struct peer_file *peer)
+{
+ size_t i;
+ for (i = 0u; i < npeers; i++) {
+ if (peers[i] == peer) {
+ peers[i] = peers[--npeers];
+ break;
+ }
+ }
+ close(peer->fd);
+ free(peer->handshake_state);
+ free(peer->realms);
+ free(peer);
+}
+
+
+static struct peer_file *
+add_peer(int fd, uint64_t id, const struct sockaddr_in *addr,
+ enum handshake_status status, void (*on_event)(union file *, uint32_t))
+{
+ struct peer_file *file;
+ struct epoll_event ev;
+
+ file = emalloc(sizeof(*file));
+ file->type = PEER_FILE;
+ file->fd = fd;
+ file->id = id;
+ file->addr = *addr;
+ file->on_event = on_event;
+ file->realms = nrealms ? ecalloc((nrealms + 7u) / 8u, sizeof(uint8_t)) : NULL;
+ file->handshake_state = ecalloc(1u, sizeof(*file->handshake_state));
+ file->handshake_state->status = status;
+
+ memset(&ev, 0, sizeof(ev));
+ ev.events = EPOLLIN | EPOLLOUT;
+ ev.data.ptr = file;
+ if (epoll_ctl(epoll, EPOLL_CTL_ADD, file->fd, &ev))
+ eprintf("epoll_ctl <epoll> EPOLL_CTL_ADD <peer tcp socket> {EPOLLIN|EPOLLOUT}:");
+
+ if (npeers == peers_size)
+ peers = ereallocarray(peers, ++peers_size, sizeof(*peers));
+ peers[npeers++] = file;
+
+ return file;
+}
+
+
+static void
+on_client_event(union file *this, uint32_t epoll_events)
+{
+ (void) this;
+ (void) epoll_events;
+ /* TODO */
+}
+
+
+static void
+on_peer_event(union file *this, uint32_t epoll_events)
+{
+ (void) this;
+ (void) epoll_events;
+ /* TODO */
+}
+
+
+static unsigned char *
+handshake_prepare(struct peer_file *this, size_t len)
+{
+ uint16_t n;
+ if (len > sizeof(this->handshake_state->sbuf) - 2u)
+ abort();
+ len += 2u;
+ n = (uint16_t)len;
+ this->handshake_state->off = 0u;
+ this->handshake_state->len = n;
+ n = htons(n);
+ memcpy(&this->handshake_state->sbuf[0u], &n, 2u);
+ return &this->handshake_state->sbuf[2u];
+}
+
+
+static int
+handshake_continue(struct peer_file *this)
+{
+ size_t rem;
+ ssize_t r;
+ while ((rem = this->handshake_state->len - this->handshake_state->off)) {
+ r = send(this->fd, &this->handshake_state->sbuf[this->handshake_state->off], rem, MSG_NOSIGNAL | MSG_DONTWAIT);
+ if (r < 0) {
+ if (errno == EINTR)
+ continue;
+ if (errno == ECONNRESET || errno == EPIPE)
+ return -1;
+ if (errno == EAGAIN)
+ return 0;
+#if EAGAIN != EWOULDBLOCK
+ if (errno == EWOULDBLOCK)
+ return 0;
+#endif
+ weprintf("send <peer tcp socket>:");
+ return -1;
+ }
+ this->handshake_state->off += (uint16_t)r;
+ }
+ this->handshake_state->off = 0u;
+ this->handshake_state->len = 0u;
+ return 1;
+}
+
+
+static int
+handshake_send(struct peer_file *this, const void *message, size_t len)
+{
+ if (!this->handshake_state->len)
+ memcpy(handshake_prepare(this, len), message, len);
+ return handshake_continue(this);
+}
+
+
+static int
+handshake_receive(struct peer_file *this)
+{
+ ssize_t r;
+ uint16_t len = 0u;
+
+ if (this->handshake_state->head != this->handshake_state->tail) {
+ memmove(&this->handshake_state->buf[0u],
+ &this->handshake_state->buf[this->handshake_state->head],
+ this->handshake_state->tail -= this->handshake_state->head);
+ this->handshake_state->off += this->handshake_state->tail;
+ this->handshake_state->tail = this->handshake_state->head = 0u;
+ }
+
+ for (;;) {
+ if (len) {
+ goto check_done;
+ } else if (this->handshake_state->off >= 2u) {
+ memcpy(&len, &this->handshake_state->buf[0u], 2u);
+ len = ntohs(len);
+ check_done:
+ if (this->handshake_state->off >= len)
+ break;
+ }
+
+ r = recv(this->fd, &this->handshake_state->buf[this->handshake_state->off],
+ sizeof(this->handshake_state->buf) - this->handshake_state->off,
+ MSG_DONTWAIT);
+ if (r <= 0) {
+ if (!r)
+ return -1;
+ if (errno == EINTR)
+ continue;
+ if (errno == ECONNRESET || errno == EPIPE)
+ return -1;
+ if (errno == EAGAIN)
+ return 0;
+#if EAGAIN != EWOULDBLOCK
+ if (errno == EWOULDBLOCK)
+ return 0;
+#endif
+ weprintf("recv <peer tcp socket>:");
+ return -1;
+ }
+ this->handshake_state->off += (uint16_t)r;
+ }
+
+ this->handshake_state->tail = this->handshake_state->off;
+ this->handshake_state->head = this->handshake_state->off = len;
+ return 1;
+}
+
+
+static void
+generate_proof(const struct realm *realm, const void *challenge, unsigned char *buf)
+{
+ unsigned char block[LIBBLAKE_BLAKE2B_BLOCK_SIZE];
+ struct libblake_blake2b_params params;
+ struct libblake_blake2b_state state;
+ size_t salt_len;
+ size_t key_len = strlen(realm->key);
+ size_t off = 0u;
+
+ salt_len = strlen(realm->name);
+ salt_len = salt_len < 16u ? salt_len : 16u;
+ memset(&params, 0, sizeof(params));
+ params.digest_len = PROOF_SIZE;
+ params.key_len = CHALLENGE_SIZE;
+ params.fanout = 1u;
+ params.depth = 1u;
+ memcpy(params.salt, realm->name, salt_len);
+ memcpy(params.pepper, CHALLENGE_PEPPER, 16u);
+
+ libblake_blake2b_init(&state, &params);
+
+ memcpy(&block[0u], challenge, CHALLENGE_SIZE);
+ memset(&block[CHALLENGE_SIZE], 0, sizeof(block) - CHALLENGE_SIZE);
+ libblake_blake2b_force_update(&state, block, sizeof(block));
+
+ off = key_len & ~(sizeof(block) - 1u);
+ if (off == key_len)
+ off -= sizeof(block);
+ if (off)
+ off = libblake_blake2b_force_update(&state, realm->key, off);
+ memcpy(&block[0u], realm->key, key_len - off);
+ libblake_blake2b_digest(&state, block, key_len - off, 0, PROOF_SIZE, buf);
+}
+
+
+static void
+peer_handshake(union file *this_, uint32_t epoll_events)
+{
+ unsigned char proof_buf[PROOF_SIZE];
+ struct peer_file *this = &this_->peer;
+ struct epoll_event ev;
+ uint16_t len;
+ size_t i;
+ int r;
+
+#define SEND(...) CHECKED(handshake_send(this, __VA_ARGS__)) goto wait_write
+#define RECV() CHECKED(handshake_receive(this)) goto wait_read
+#define CONT() CHECKED(handshake_continue(this)) goto wait_write
+#define CHECKED(COMMAND)\
+ r = (COMMAND);\
+ if (r < 0) goto remove;\
+ if (r == 0)
+
+ (void) epoll_events;
+
+ switch (this->handshake_state->status) {
+ case HANDSHAKE_A_SEND_ID:
+ SEND(&server_identity.id, 8u);
+ this->handshake_state->status++;
+ /* fall-through */
+
+ case HANDSHAKE_A_RECV_ID:
+ RECV();
+ len = this->handshake_state->off;
+ this->handshake_state->off = 0u;
+ if (len != 10u || memcmp(&this->handshake_state->buf[2u], &this->id, 8u))
+ goto remove;
+ this->handshake_state->status++;
+ /* fall-through */
+
+ case HANDSHAKE_A_SEND_REALM:
+ send_realm:
+ if (this->handshake_state->realm == nrealms) {
+ SEND("", 0u);
+ break;
+ }
+ SEND(realms[this->handshake_state->realm].name,
+ strlen(realms[this->handshake_state->realm].name));
+ this->handshake_state->status++;
+ /* fall-through */
+
+ case HANDSHAKE_A_RECV_CHALLENGE:
+ RECV();
+ len = this->handshake_state->off;
+ this->handshake_state->off = 0u;
+ if (len == 2u) {
+ this->handshake_state->status = HANDSHAKE_A_SEND_REALM;
+ goto send_realm;
+ }
+ if (len != CHALLENGE_SIZE + 2u) {
+ goto remove;
+ }
+ generate_proof(&realms[this->handshake_state->realm],
+ &this->handshake_state->buf[2u],
+ handshake_prepare(this, PROOF_SIZE));
+ this->handshake_state->status++;
+ /* fall-through */
+
+ case HANDSHAKE_A_SEND_PROOF:
+ CONT();
+ this->handshake_state->status++;
+ /* fall-through */
+
+ case HANDSHAKE_A_RECV_ACCEPTANCE:
+ RECV();
+ len = this->handshake_state->off;
+ this->handshake_state->off = 0u;
+ if (len != 3u || this->handshake_state->buf[2u] > 1u)
+ goto remove;
+ if (this->handshake_state->buf[2u]) {
+ uint8_t bit = (uint8_t)(1u << (this->handshake_state->realm % 8u));
+ this->realms[this->handshake_state->realm / 8u] |= bit;
+ }
+ this->handshake_state->realm++;
+ this->handshake_state->status = HANDSHAKE_A_SEND_REALM;
+ goto send_realm;
+
+
+ case HANDSHAKE_B_SEND_ID:
+ if (!handshake_send(this, &server_identity.id, 8u))
+ goto wait_write;
+ this->handshake_state->status++;
+ /* fall-through */
+
+ case HANDSHAKE_B_RECV_ID:
+ RECV();
+ len = this->handshake_state->off;
+ this->handshake_state->off = 0u;
+ if (len != 10u)
+ goto remove;
+ memcpy(&this->id, &this->handshake_state->buf[2u], 8u);
+ if (this->id == server_identity.id)
+ goto remove;
+ for (i = 0u; i < npeers; i++)
+ if (peers[i]->id == this->id && peers[i] != this)
+ goto remove;
+ this->handshake_state->status++;
+ /* fall-through */
+
+ case HANDSHAKE_B_RECV_REALM:
+ recv_realm:
+ RECV();
+ len = this->handshake_state->off;
+ this->handshake_state->off = 0u;
+ if (len <= 2u) {
+ if (len == 2u)
+ break;
+ goto remove;
+ }
+ len = (uint16_t)(len - 2u);
+ for (i = 0u; i < nrealms; i++) {
+ if (memcmp(realms[i].name, &this->handshake_state->buf[2u], len))
+ continue;
+ if (realms[i].name[len])
+ continue;
+ break;
+ }
+ this->handshake_state->realm = i;
+ if (this->handshake_state->realm == nrealms) {
+ handshake_prepare(this, 0u);
+ } else {
+ unsigned char *m = handshake_prepare(this, CHALLENGE_SIZE);
+ for (i = 0u; i < CHALLENGE_SIZE; i++)
+ m[i] = (unsigned char)(rand() & 255);
+ }
+ this->handshake_state->status++;
+ /* fall-through */
+
+ case HANDSHAKE_B_SEND_CHALLENGE:
+ CONT();
+ if (this->handshake_state->realm == nrealms) {
+ this->handshake_state->status = HANDSHAKE_B_RECV_REALM;
+ goto recv_realm;
+ }
+ this->handshake_state->status++;
+ /* fall-through */
+
+ case HANDSHAKE_B_RECV_PROOF:
+ RECV();
+ len = this->handshake_state->off;
+ this->handshake_state->off = 0u;
+ if (len != PROOF_SIZE + 2u)
+ goto remove;
+ generate_proof(&realms[this->handshake_state->realm],
+ &this->handshake_state->sbuf[2u], proof_buf);
+ if (!memcmp(&this->handshake_state->buf[2u], proof_buf, PROOF_SIZE)) {
+ uint8_t bit = (uint8_t)(1u << (this->handshake_state->realm % 8u));
+ this->realms[this->handshake_state->realm / 8u] |= bit;
+ handshake_prepare(this, 1u)[0u] = 1u;
+ } else {
+ handshake_prepare(this, 1u)[0u] = 0u;
+ }
+ this->handshake_state->status++;
+ /* fall-through */
+
+ case HANDSHAKE_B_SEND_ACCEPTANCE:
+ CONT();
+ this->handshake_state->status = HANDSHAKE_B_RECV_REALM;
+ goto recv_realm;
+
+ default:
+ abort();
+ }
+
+ for (i = 0u; i < (nrealms + 7u) / 8u; i++)
+ if (this->realms[i])
+ goto have_a_realm;
+remove:
+ remove_peer(this);
+ return;
+
+have_a_realm:
+ epoll_events = EPOLLIN;
+ this->on_event = &on_peer_event;
+ /* TODO copy over handshake recv buf */
+ free(this->handshake_state);
+ this->handshake_state = NULL;
+
+config_epoll:
+ memset(&ev, 0, sizeof(ev));
+ ev.events = epoll_events;
+ ev.data.ptr = this;
+ if (epoll_ctl(epoll, EPOLL_CTL_MOD, this->fd, &ev))
+ eprintf("epoll_ctl <epoll> EPOLL_CTL_MOD <peer tcp socket> {%s}:",
+ epoll_events == EPOLLIN ? "EPOLLIN" : "EPOLLOUT");
+ return;
+
+wait_write:
+ epoll_events = EPOLLOUT;
+ goto config_epoll;
+wait_read:
+ epoll_events = EPOLLIN;
+ goto config_epoll;
+
+#undef SEND
+#undef RECV
+#undef CONT
+#undef CHECKED
+}
+
+
+static void
+peer_connection_done(union file *this, uint32_t epoll_events)
+{
+ int err = 0;
+ socklen_t len = (socklen_t)sizeof(err);
+
+ if (getsockopt(this->fd, SOL_SOCKET, SO_ERROR, &err, &len))
+ eprintf("getsockopt <peer tcp socket> SOL_SOCKET SO_ERROR");
+ if (len > (socklen_t)sizeof(err))
+ abort();
+
+ if (err && err != EINTR) {
+ weprintf("connect(async) <tcp socket> %s:%u\n",
+ inet_ntoa(this->peer.addr.sin_addr), ntohs(this->peer.addr.sin_port));
+ remove_peer(&this->peer);
+ return;
+ }
+
+ this->on_event = &peer_handshake;
+ peer_handshake(this, epoll_events);
+}
+
+
+static void
+on_network_socket_event(union file *this, uint32_t epoll_events)
+{
+ struct sockaddr_in addr;
+ socklen_t addrlen = (socklen_t)sizeof(addr);
+ struct peer_file *file;
+ int fd;
+
+ (void) epoll_events;
+
+retry:
+ fd = accept4(this->fd, (void *)&addr, &addrlen, SOCK_NONBLOCK);
+ if (fd < 0) {
+ switch (errno) {
+ case EINTR:
+ goto retry;
+ case ECONNABORTED:
+ case ECONNRESET:
+ case EPERM:
+ return;
+ default:
+ eprintf("accept4 <tcp server socket> SOCK_NONBLOCK:");
+ }
+ }
+ if (addrlen > (socklen_t)sizeof(addr)) {
+ weprintf("TCP socket peer name overlong\n");
+ close(fd);
+ return;
+ }
+
+ file = add_peer(fd, 0u, &addr, HANDSHAKE_B_INIT, &peer_handshake);
+ peer_handshake((void *)file, EPOLLOUT);
+}
+
+
+static void
+on_broadcast_socket_event(union file *this, uint32_t epoll_events)
+{
+ struct broadcast_message msg;
+ struct sockaddr_in addr;
+ socklen_t addrlen = (socklen_t)sizeof(addr);
+ ssize_t r;
+ int fd, in_progress;
+ struct peer_file *file;
+ size_t i;
+
+ (void) epoll_events;
+
+retry_recv:
+ r = recvfrom(this->fd, &msg, sizeof(msg), MSG_TRUNC, &addr, &addrlen);
+ if (r < 0) {
+ if (errno == EINTR)
+ goto retry_recv;
+ if (errno == ECONNREFUSED)
+ return;
+ eprintf("recvfrom <broadcast socket>:");
+ }
+ if (addrlen > (socklen_t)sizeof(addr)) {
+ weprintf("TCP socket peer name overlong\n");
+ return;
+ }
+ if (r != BROADCAST_MESSAGE_SIZE || msg.id == server_identity.id)
+ return;
+ for (i = 0u; i < npeers; i++)
+ if (msg.id == peers[i]->id)
+ return;
+
+ fd = socket(PF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
+ if (fd < 0) {
+ weprintf("socket PF_INET SOCK_STREAM|SOCK_NONBLOCK 0:");
+ return;
+ }
+
+ addr.sin_port = msg.port;
+retry_connect:
+ if (!connect(fd, (void *)&addr, addrlen)) {
+ in_progress = 0;
+ } else if (errno == EINPROGRESS) {
+ in_progress = 1;
+ } else if (errno == EINTR) {
+ goto retry_connect;
+ } else {
+ weprintf("connect <tcp socket> %s:%u\n", inet_ntoa(addr.sin_addr), ntohs(msg.port));
+ if (errno != ENETUNREACH && errno != ETIMEDOUT && errno != ECONNREFUSED)
+ exit(1);
+ close(fd);
+ return;
+ }
+
+ file = add_peer(fd, msg.id, &addr, HANDSHAKE_A_INIT,
+ in_progress ? &peer_connection_done : &peer_handshake);
+ if (!in_progress)
+ peer_handshake((void *)file, EPOLLOUT);
+}
+
+
+static void
+broadcast_identity(struct broadcast_file *file)
+{
+ long int rnd;
+ struct itimerspec its;
+
+ if (sendto(file->fd, &server_identity, BROADCAST_MESSAGE_SIZE, 0, SOCKADDR(file->addr)) != BROADCAST_MESSAGE_SIZE)
+ weprintf("sendto <broadcast socket>:");
+
+ file->alarm_time.tv_sec += file->timer_inc;
+ file->timer_inc *= 2;
+ rnd = rand() & 255;
+ rnd *= 1000000l;
+ file->alarm_time.tv_nsec += rnd;
+ if (file->alarm_time.tv_nsec >= 1000000000l) {
+ file->alarm_time.tv_nsec -= 1000000000l;
+ file->alarm_time.tv_sec += 1;
+ }
+
+ memset(&its, 0, sizeof(its));
+ its.it_value = file->alarm_time;
+ if (timerfd_settime(file->timer.fd, 0, &its, NULL))
+ weprintf("timerfd_settime <timerfd for broadcast socket> 0 <single expiry> NULL:");
+}
+
+
+static void
+on_broadcast_timer_event(union file *this, uint32_t epoll_events)
+{
+ (void) epoll_events;
+ broadcast_identity(&this->timer.owner->broadcast);
+}
+
+
+static void
+on_local_socket_event(union file *this, uint32_t epoll_events)
+{
+ struct ucred cred;
+ socklen_t len = (socklen_t)sizeof(cred);
+ int fd;
+ struct client_file *file;
+ struct epoll_event ev;
+
+ (void) epoll_events;
+
+retry:
+ fd = accept4(this->fd, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC);
+ if (fd < 0) {
+ switch (errno) {
+ case EINTR:
+ goto retry;
+ case ECONNABORTED:
+ case ECONNRESET:
+ case EPERM:
+ return;
+ default:
+ eprintf("accept4 <local socket> NULL NULL SOCK_NONBLOCK|SOCK_CLOEXEC:");
+ }
+ }
+
+ if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &len)) {
+ weprintf("getsockopt <local client socket> SOL_SOCKET SO_PEERCRED:");
+ close(fd);
+ return;
+ }
+ if (len != (socklen_t)sizeof(cred))
+ abort();
+ if (cred.uid != uid) {
+ /* TODO maybe it should be possible to open up realms to specific/all
+ * users so they don't need a separate instance running */
+ close(fd);
+ return;
+ }
+
+ file = emalloc(sizeof(*file));
+ file->type = CLIENT_FILE;
+ file->fd = fd;
+ file->on_event = &on_client_event;
+
+ file->next = &clients_tail;
+ file->prev = clients_tail.prev;
+ clients_tail.prev->next = file;
+ clients_tail.prev = file;
+
+ memset(&ev, 0, sizeof(ev));
+ ev.events = EPOLLIN;
+ ev.data.ptr = file;
+ if (epoll_ctl(epoll, EPOLL_CTL_ADD, file->fd, &ev))
+ eprintf("epoll_ctl <epoll> EPOLL_CTL_ADD <client local socket> {EPOLLIN}:");
+}
+
+
+static struct network_file *
+create_network_socket(void)
+{
+ struct sockaddr_in addr;
+ socklen_t addrlen = (socklen_t)sizeof(addr);
+ struct network_file *file;
+ struct epoll_event ev;
+
+ file = emalloc(sizeof(*file));
+ file->type = NETWORK_FILE;
+ file->on_event = &on_network_socket_event;
+
+ file->fd = socket(PF_INET, SOCK_STREAM, 0);
+ if (file->fd < 0)
+ eprintf("socket PF_INET SOCK_STREAM 0:");
+
+ memset(&addr, 0, sizeof(addr));
+ addr.sin_family = AF_INET;
+ addr.sin_addr.s_addr = htonl(INADDR_ANY);
+
+ if (bind(file->fd, (const void *)&addr, addrlen))
+ eprintf("bind <tcp socket> &{AF_INET, 0.0.0.0:0}:");
+
+ if (getsockname(file->fd, &addr, &addrlen))
+ eprintf("getsockname <tcp socket>:");
+ if (addrlen < (socklen_t)(offsetof(struct sockaddr_in, sin_port) + sizeof(addr.sin_port)))
+ abort();
+ server_identity.port = addr.sin_port;
+
+ if (listen(file->fd, SOMAXCONN))
+ eprintf("listen <tcp socket> SOMAXCONN:");
+
+ memset(&ev, 0, sizeof(ev));
+ ev.events = EPOLLIN;
+ ev.data.ptr = file;
+ if (epoll_ctl(epoll, EPOLL_CTL_ADD, file->fd, &ev))
+ eprintf("epoll_ctl <epoll> EPOLL_CTL_ADD <tcp server socket> {EPOLLIN}:");
+
+ return file;
+}
+
+
+static struct broadcast_file *
+create_broadcast_socket(uint16_t port)
+{
+ struct broadcast_file *file;
+ struct epoll_event ev;
+
+ file = emalloc(sizeof(*file));
+ memset(file, 0, sizeof(*file));
+ file->type = BROADCAST_FILE;
+ file->on_event = &on_broadcast_socket_event;
+ file->timer_inc = 1;
+ file->timer.type = TIMER_FILE;
+ file->timer.owner = (void *)file;
+ file->timer.on_event = &on_broadcast_timer_event;
+
+ file->fd = socket(PF_INET, SOCK_DGRAM, 0);
+ if (file->fd < 0)
+ eprintf("socket PF_INET SOCK_DGRAM 0:");
+
+ /* TODO switch to multicast (for portability) */
+ if (setsockopt(file->fd, SOL_SOCKET, SO_REUSEADDR, &(int){1}, (socklen_t)sizeof(int)))
+ eprintf("setsockopt <udp socket> SOL_SOCKET SO_REUSEADDR &{1}:");
+
+ if (setsockopt(file->fd, SOL_SOCKET, SO_BROADCAST, &(int){1}, (socklen_t)sizeof(int)))
+ eprintf("setsockopt <udp socket> SOL_SOCKET SO_BROADCAST &{1}:");
+
+ memset(&file->addr, 0, sizeof(file->addr));
+ file->addr.sin_family = AF_INET;
+ file->addr.sin_port = htons(port);
+ file->addr.sin_addr.s_addr = htonl(INADDR_ANY);
+
+ if (bind(file->fd, SOCKADDR(file->addr)))
+ eprintf("bind <broadcast socket> &{AF_INET, 0.0.0.0:%i}:", port);
+
+ file->addr.sin_addr.s_addr = htonl(INADDR_BROADCAST);
+
+ file->timer.fd = timerfd_create(CLOCK_BOOTTIME, 0);
+ if (file->timer.fd < 0)
+ eprintf("timerfd_create CLOCK_BOOTTIME 0:");
+
+ memset(&ev, 0, sizeof(ev));
+ ev.events = EPOLLIN;
+ ev.data.ptr = file;
+ if (epoll_ctl(epoll, EPOLL_CTL_ADD, file->fd, &ev))
+ eprintf("epoll_ctl <epoll> EPOLL_CTL_ADD <broadcast socket> {EPOLLIN}:");
+ ev.data.ptr = &file->timer;
+ if (epoll_ctl(epoll, EPOLL_CTL_ADD, file->timer.fd, &ev))
+ eprintf("epoll_ctl <epoll> EPOLL_CTL_ADD <timerfd for broadcast socket> {EPOLLIN}:");
+
+ return file;
+}
+
+
+static struct local_file *
+create_local_socket(const char *address)
+{
+ struct sockaddr_un addr;
+ socklen_t addrlen = (socklen_t)sizeof(addr);
+ struct local_file *file;
+ struct epoll_event ev;
+ size_t len;
+ int print_sockname = 0;
+
+ len = strlen(address);
+ if (len > sizeof(addr.sun_path))
+ eprintf("local address overlong: %s", address);
+
+ file = emalloc(sizeof(*file));
+ file->type = LOCAL_FILE;
+ file->on_event = &on_local_socket_event;
+
+ file->fd = socket(PF_LOCAL, SOCK_SEQPACKET, 0);
+ if (file->fd < 0)
+ eprintf("socket PF_LOCAL SOCK_SEQPACKET 0:");
+
+ memset(&addr, 0, sizeof(addr));
+ addr.sun_family = AF_LOCAL;
+ memcpy(addr.sun_path, address, len);
+ if (!len || addr.sun_path[0u] == '@') {
+ addr.sun_path[0u] = '\0';
+ if (len <= 1u) {
+ for (len = 1u; len < sizeof(addr.sun_path); len++)
+ addr.sun_path[len] = ALPHA64[rand() & 63];
+ print_sockname = 1;
+ } else {
+ addrlen = (socklen_t)(offsetof(struct sockaddr_un, sun_path) + len);
+ }
+ }
+
+ if (bind(file->fd, (const void *)&addr, addrlen))
+ eprintf("bind <local socket> &{AF_LOCAL, %c%.*s}:",
+ addr.sun_path[0u] ? addr.sun_path[0u] : '@',
+ (int)len - 1, &addr.sun_path[1u]);
+
+ if (listen(file->fd, SOMAXCONN))
+ eprintf("listen <local socket> SOMAXCONN:");
+
+ memset(&ev, 0, sizeof(ev));
+ ev.events = EPOLLIN;
+ ev.data.ptr = file;
+ if (epoll_ctl(epoll, EPOLL_CTL_ADD, file->fd, &ev))
+ eprintf("epoll_ctl <epoll> EPOLL_CTL_ADD <tcp server socket> {EPOLLIN}:");
+
+ if (print_sockname) {
+ if (printf("@%.*s\n", (int)len - 1, &addr.sun_path[1u]) < 0 || fflush(stdout))
+ eprintf("printf:");
+ }
+
+ return file;
+}
+
+
+static void
+init_process(void)
+{
+ struct timespec ts;
+ unsigned long int auxval;
+ unsigned seed = 0;
+ size_t i;
+ int fd;
+
+ do {
+ fd = open("/dev/null", O_RDWR);
+ if (fd < 0)
+ eprintf("open /dev/null O_RDWR:");
+ if (fd > STDERR_FILENO)
+ close(fd);
+ } while (fd < STDERR_FILENO);
+
+ uid = getuid();
+
+ libblake_init();
+
+ clients_head.prev = NULL;
+ clients_head.next = &clients_tail;
+ clients_tail.prev = &clients_head;
+ clients_tail.next = NULL;
+
+ auxval = getauxval(AT_RANDOM);
+ if (auxval) {
+ unsigned char *r = (unsigned char *)(uintptr_t)auxval;
+ for (i = 0u; i < 16u; i++)
+ seed = seed * 131u + r[i];
+ }
+ if (!clock_gettime(CLOCK_REALTIME, &ts)) {
+ seed ^= (unsigned)ts.tv_sec;
+ seed ^= (unsigned)ts.tv_nsec;
+ }
+ if (!clock_gettime(CLOCK_MONOTONIC, &ts)) {
+ seed ^= (unsigned)ts.tv_sec * (unsigned)2654435761lu;
+ seed ^= (unsigned)ts.tv_nsec;
+ }
+ srand(seed);
+}
+
+
+static void
+load_config(const char *config_file, char **local_address, uint16_t *udp_port)
+{
+ FILE *f;
+ ssize_t r;
+ char *key, *value;
+ char *line = NULL;
+ size_t size = 0u;
+ size_t len, off, i;
+ int fd, old_fd;
+ int udp_set = 0;
+ long int li;
+ unsigned long int lu;
+ char *end, *start;
+ const char *env;
+ char *config_file_free = NULL;
+ size_t config_file_size = 0u;
+
+ if (!config_file) {
+#define SET_PREFIX(PREFIX) SET_PREFIXN(PREFIX, strlen(PREFIX))
+#define SET_PREFIXN(PREFIX, LEN)\
+ do {\
+ if (config_file_size < (LEN) + 32u) {\
+ config_file_size = (LEN) + 32u;\
+ config_file_free = erealloc(config_file_free, config_file_size);\
+ }\
+ start = stpcpy(config_file_free, (PREFIX));\
+ } while (0)
+#define TEST_CONFIG(PATH)\
+ do {\
+ if (!access(PATH, F_OK)) {\
+ config_file = (PATH);\
+ goto open_file;\
+ }\
+ } while (0)
+
+ env = getenv("OIKOBUS_CONFIG");
+ if (env && *env) {
+ config_file = env;
+ goto open_file;
+ }
+
+ env = getenv("XDG_CONFIG_HOME");
+ if (env && *env) {
+ SET_PREFIX(env);
+ stpcpy(start, "/oikobus/config");
+ TEST_CONFIG(config_file_free);
+ stpcpy(start, "/oikobus.conf");
+ TEST_CONFIG(config_file_free);
+ }
+
+ env = getenv("HOME");
+ if (env && *env) {
+ SET_PREFIX(env);
+ } else {
+ struct passwd *pw;
+ retry_getpwuid:
+ pw = getpwuid(uid);
+ if (!pw) {
+ if (errno == EINTR)
+ goto retry_getpwuid;
+ eprintf("getpwuid <real uid>:");
+ }
+ if (!pw->pw_dir || !*pw->pw_dir)
+ goto no_home;
+ SET_PREFIX(pw->pw_dir);
+ }
+ stpcpy(start, "/.config/oikobus/config");
+ TEST_CONFIG(config_file_free);
+ stpcpy(start, "/.config/oikobus.conf");
+ TEST_CONFIG(config_file_free);
+ stpcpy(start, "/.oikobus/config");
+ TEST_CONFIG(config_file_free);
+ stpcpy(start, "/.oikobus.conf");
+ TEST_CONFIG(config_file_free);
+
+ no_home:
+ env = getenv("XDG_CONFIG_DIRS");
+ if (env && *env) {
+ for (off = 0u; env[off]; off = i) {
+ for (i = off; env[i] && env[i] != ':'; i++);
+ len = i - off;
+ if (env[i])
+ i++;
+ if (!len)
+ continue;
+ SET_PREFIXN(&env[off], len);
+ stpcpy(start, "/oikobus/config");
+ TEST_CONFIG(config_file_free);
+ stpcpy(start, "/oikobus.conf");
+ TEST_CONFIG(config_file_free);
+ }
+ }
+
+ TEST_CONFIG("/etc/oikobus/config");
+ TEST_CONFIG("/etc/oikobus.conf");
+
+ eprintf("no configuration file found, use -c/dev/null if you don't want to load one");
+
+#undef SET_PREFIXN
+#undef SET_PREFIX
+#undef TEST_CONFIG
+ } else {
+ if (!strcmp(config_file, "-")) {
+ config_file = "/dev/stdin";
+ fd = STDIN_FILENO;
+ goto open_fd;
+ } else if (!strcmp(config_file, "/dev/stdin")) {
+ fd = STDIN_FILENO;
+ goto open_fd;
+ } else if (!strcmp(config_file, "/dev/stdout")) {
+ fd = STDOUT_FILENO;
+ goto open_fd;
+ } else if (!strcmp(config_file, "/dev/stderr")) {
+ fd = STDERR_FILENO;
+ goto open_fd;
+ }
+
+ if (strstarts(config_file, "/dev/fd/")) {
+ off = sizeof("/dev/fd/") - 1u;
+ } else if (strstarts(config_file, "/proc/self/fd/")) {
+ off = sizeof("/proc/self/fd/") - 1u;
+ } else {
+ goto open_file;
+ }
+
+ if ('0' > config_file[off] || config_file[off] > '9')
+ goto open_file;
+ errno = 0;
+ li = strtol(&config_file[off], &end, 10);
+ if (errno || li < 0 || li > INT_MAX || *end)
+ goto open_file;
+ fd = (int)li;
+ if (fd <= STDERR_FILENO) {
+ open_fd:
+ fd = dup(old_fd = fd);
+ if (fd < 0)
+ eprintf("dup %i:", old_fd);
+ }
+ f = fdopen(fd, "r");
+ goto file_opened;
+ }
+
+open_file:
+ f = fopen(config_file, "r");
+file_opened:
+ if (!f)
+ eprintf("%s:", config_file);
+
+ while ((r = getdelim(&line, &size, '\n', f)) != -1) {
+ len = (size_t)r;
+ if (len && line[len - 1u] == '\n')
+ line[--len] = '\0';
+ if (len && line[len - 1u] == '\r')
+ line[--len] = '\0';
+
+ off = 0u;
+ while (isspace(line[off]))
+ off++;
+ key = &line[off];
+ value = NULL;
+ for (i = off; line[i]; i++) {
+ if (line[i] == '#' || line[i] == ';') {
+ if (i == off || isspace(line[i - 1u])) {
+ line[i] = '\0';
+ len = i;
+ break;
+ }
+ } else if ((line[i] == ':' || line[i] == '=') && !value) {
+ value = &line[i];
+ }
+ }
+
+ while (len && isspace(line[len - 1u]))
+ line[--len] = '\0';
+
+ if (value) {
+ i = (size_t)(value - key);
+ *value++ = '\0';
+ while (isspace(*value))
+ value++;
+ while (i-- && isspace(line[i]))
+ line[i] = '\0';
+ }
+
+ if (nrealms) {
+ if (!strcmp(key, "password")) {
+ if (!value)
+ goto value_missing;
+ if (realms[nrealms - 1u].key)
+ goto duplicate_realm_option;
+ realms[nrealms - 1u].key = estrdup(value);
+ } else {
+ eprintf("%s: realm option '%s' not recognised", config_file, key);
+ }
+ } else if (!value && key[0] == '[' && (end = strchr(key, '\0'))[-1] == ']') {
+ key++;
+ *--end = '\0';
+ len = (size_t)(end - key);
+ if (len < MIN_REALM_LEN || len > MAX_REALM_LEN)
+ eprintf("%s: invalid realm name length: '%s', must "
+ "be between %u and %u bytes (inclusively)",
+ config_file, key, MIN_REALM_LEN, MAX_REALM_LEN);
+ for (i = 0u; i < nrealms; i++)
+ if (!strcmp(key, realms[i].name))
+ eprintf("%s: duplicate section for realm '%s'", config_file, key);
+ realms = ereallocarray(realms, ++nrealms, sizeof(*realms));
+ realms[nrealms - 1u].name = estrdup(key);
+ realms[nrealms - 1u].key = NULL;
+ } else {
+ if (!strcmp(key, "udp-port")) {
+ if (!value)
+ goto value_missing;
+ if (udp_set++)
+ goto duplicate_global_option;
+ errno = 0;
+ lu = strtoul(value, &end, 10);
+ if (errno || lu == 0u || lu > UINT16_MAX || *end)
+ eprintf("%s: global option '%s' not recognised requires a "
+ "non-zero, unsigned 16-bit integer", config_file, key);
+ *udp_port = (uint16_t)lu;
+ } else if (!strcmp(key, "unix-address")) {
+ if (!value)
+ goto value_missing;
+ if (*local_address)
+ goto duplicate_global_option;
+ *local_address = estrdup(value);
+ } else {
+ eprintf("%s: global option '%s' not recognised", config_file, key);
+ }
+ }
+ }
+ if (ferror(f) || fclose(f))
+ eprintf("getline %s:", config_file);
+
+ free(line);
+ free(config_file_free);
+ return;
+
+duplicate_realm_option:
+ eprintf("%s: realm option '%s' configured twice for realm '%s'", config_file, key, realms[nrealms - 1u].name);
+duplicate_global_option:
+ eprintf("%s: global option '%s' configured twice", config_file, key);
+value_missing:
+ eprintf("%s: option '%s' requires a value", config_file, key);
+}
+
+
+static void
+init_server(const char *config_file, const char *local_address_cmdline, uint16_t udp_port_cmdline)
+{
+ uint16_t udp_port = UDP_PORT;
+ const char *local_address = NULL;
+ char *local_address_free = NULL;
+ char local_address_buf[LOCAL_ADDRESS_BUFSIZE];
+ size_t i;
+
+ load_config(config_file, &local_address_free, &udp_port);
+ local_address = local_address_free;
+ if (udp_port_cmdline)
+ udp_port = udp_port_cmdline;
+ if (local_address_cmdline)
+ local_address = local_address_cmdline;
+
+ if (!local_address) {
+ sprintf(local_address_buf, LOCAL_ADDRESS(getuid()));
+ local_address = local_address_buf;
+ }
+
+ for (i = 0u; i < sizeof(server_identity.id); i++)
+ ((unsigned char *)&server_identity.id)[i] = (unsigned char)(rand() & 255);
+
+ events_size = 32;
+ events = ecalloc((size_t)events_size, sizeof(*events));
+
+ epoll = epoll_create1(0);
+ if (epoll < 0)
+ eprintf("create_epoll1 0:");
+
+ lsock = create_local_socket(local_address);
+ nsock = create_network_socket();
+ bsock = create_broadcast_socket(udp_port);
+
+ free(local_address_free);
+}
+
+
+static void
+signal_inited(void)
+{
+ if (dup2(STDERR_FILENO, STDIN_FILENO) != STDIN_FILENO)
+ eprintf("dup2 STDERR_FILENO STDIN_FILENO:");
+ if (dup2(STDERR_FILENO, STDOUT_FILENO) != STDOUT_FILENO)
+ eprintf("dup2 STDERR_FILENO STDOUT_FILENO:");
+}
+
+
+int
+main(int argc, char *argv[])
+{
+ uint16_t udp_port_cmdline = 0u;
+ const char *config_file = NULL;
+ const char *local_address_cmdline = NULL;
+ char *arg;
+ unsigned long int lu;
+ union file *file;
+ int n;
+
+ ARGBEGIN {
+ /* TODO add daemonisation options */
+ /* TODO add realm options */
+ case 'a':
+ if (local_address_cmdline)
+ usage();
+ local_address_cmdline = ARG();
+ if (!*local_address_cmdline)
+ usage();
+ break;
+ case 'c':
+ if (config_file)
+ usage();
+ config_file = ARG();
+ if (!*config_file)
+ usage();
+ break;
+ case 'p':
+ arg = ARG();
+ if ('0' > arg[0u] || arg[0u] > '9' || udp_port_cmdline)
+ usage();
+ errno = 0;
+ lu = strtoul(arg, &arg, 10);
+ if (errno || *arg || lu > UINT16_MAX || !lu)
+ usage();
+ udp_port_cmdline = (uint16_t)lu;
+ break;
+ default:
+ usage();
+ } ARGEND;
+ /* -W is reserved for vendor options */
+
+ if (argc)
+ usage();
+
+ init_process();
+ init_server(config_file, local_address_cmdline, udp_port_cmdline);
+ broadcast_identity(bsock);
+ signal_inited();
+
+ for (;;) {
+ n = epoll_wait(epoll, events, events_size, -1);
+ if (n < 0) {
+ if (errno == EINTR)
+ continue;
+ eprintf("epoll_wait <epoll> <buffer> %i -1:", events_size);
+ }
+
+ while (n--) {
+ file = events[n].data.ptr;
+ (*file->on_event)(file, events[n].events);
+ }
+ }
+}