diff options
| -rw-r--r-- | .gitignore | 19 | ||||
| -rw-r--r-- | LICENSE | 15 | ||||
| -rw-r--r-- | Makefile | 44 | ||||
| -rw-r--r-- | TODO | 3 | ||||
| -rw-r--r-- | config-coverage-gcc.mk | 14 | ||||
| -rw-r--r-- | config.mk | 18 | ||||
| -rw-r--r-- | oikobusd.c | 1329 |
7 files changed, 1442 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f5dd8ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +*\#* +*~ +*.o +*.a +*.t +*.f +*.lo +*.to +*.fo +*.su +*.so +*.so.* +*.dll +*.dylib +*.gch +*.gcov +*.gcno +*.gcda +/oikobusd @@ -0,0 +1,15 @@ +ISC License + +© 2026 Mattias Andrée <m@maandree.se> + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..31190d8 --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +.POSIX: + +CONFIGFILE = config.mk +include $(CONFIGFILE) + +OBJ =\ + oikobusd.o + +HDR = + + +ALL_CPPFLAGS = $(CPPFLAGS) $(FUZZED_CPPFLAGS) +ALL_CFLAGS = $(CFLAGS) $(FUZZED_CFLAGS) +ALL_LDFLAGS = $(LDFLAGS) $(FUZZED_LDFLAGS) + + +all: oikobusd +$(OBJ): $(HDR) + +.c.o: + $(CC) -c -o $@ $< $(ALL_CFLAGS) $(GOV_CFLAGS) $(ALL_CPPFLAGS) $(GOV_CPPFLAGS) + +oikobusd: $(OBJ) + $(CC) -o $@ $(OBJ) $(ALL_LDFLAGS) $(COV_LDFLAGS) + +install: oikobusd + mkdir -p -- "$(DESTDIR)$(PREFIX)/bin" + mkdir -p -- "$(DESTDIR)$(MANPREFIX)/man1/" + cp -- oikobusd "$(DESTDIR)$(PREFIX)/bin/" + cp -- oikobusd.1 "$(DESTDIR)$(MANPREFIX)/man1/" + +uninstall: + -rm -f -- "$(DESTDIR)$(PREFIX)/bin/oikobusd" + -rm -f -- "$(DESTDIR)$(MANPREFIX)/man1/oikobusd.1" + +clean: + -rm -f -- *.o *.a *.lo *.su *.so *.so.* *.gch + -rm -f -- *.gcov *.gcno *.gcda *.t *.to *.f *.fo + -rm -f -- oikobusd + +.SUFFIXES: +.SUFFIXES: .o .c + +.PHONY: all install uninstall clean @@ -0,0 +1,3 @@ +Add support for SIGCONT +Add ability to reexec +Add ability to reload diff --git a/config-coverage-gcc.mk b/config-coverage-gcc.mk new file mode 100644 index 0000000..60f81f8 --- /dev/null +++ b/config-coverage-gcc.mk @@ -0,0 +1,14 @@ +CONFIGFILE_PROPER = config.mk +include $(CONFIGFILE_PROPER) + +CC = $(CC_PREFIX)gcc -std=c99 +GCOV = gcov + +COV_CPPFLAGS = -DCOVERAGE_TEST +COV_CFLAGS = --coverage -g -O0 +COV_LDFLAGS = --coverage -g -O0 + +G = + +coverage: check + $(GCOV) -pr -- *.gcda 2>&1 diff --git a/config.mk b/config.mk new file mode 100644 index 0000000..aec35cd --- /dev/null +++ b/config.mk @@ -0,0 +1,18 @@ +PREFIX = /usr +MANPREFIX = $(PREFIX)/share/man + +CC = cc -std=c17 + +COMMON_SANITIZE = -fsanitize=alignment,shift,signed-integer-overflow,object-size,null,undefined,bounds,address +CLANG_SANITIZE = -O1 $(COMMON_SANITIZE),cfi -flto -fvisibility=hidden -fno-sanitize-trap=cfi +GCC_SANITIZE = -O1 $(COMMON_SANITIZE) +#SANITIZE = $(CLANG_SANITIZE) +#SANITIZE = $(GCC_SANITIZE) + +CPPFLAGS = -D_DEFAULT_SOURCE -D_BSD_SOURCE -D_XOPEN_SOURCE=700 -D_GNU_SOURCE +CFLAGS = $(SANITIZE) +LDFLAGS = $(SANITIZE) -lsimple -lblake + +# libblake must be compiled with support for BLAKE2b + +G = -g 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(¶ms, 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, ¶ms); + + 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); + } + } +} |
