Index: wgsync/makefile ================================================================== --- wgsync/makefile +++ wgsync/makefile @@ -1,30 +1,37 @@ +B = build +E = ext + pq-inc != pkg-config --cflags libpq pq-lib != pkg-config --libs libpq -cc-flags = -std=c2x $(pq-inc) -ld-flags = $(pq-lib) +wg-inc = -I$E/wglib -B = build +dbg-flags = $(if $(debug),-g -D_cfg_debug,) +cc-flags = -std=c2x $(pq-inc) $(wg-inc) $(dbg-flags) +ld-flags = $(pq-lib) $(dbg-flags) + # link rule .PHONY: wgsync -$B/wgsync: $B/wgsync.o $B/wireguard.o | $B/ +$B/wgsync: $B/wgsync.o $B/pqp.o $B/def.o $B/wireguard.o | $B/ $(CC) $(ld-flags) $^ -o $@ # build rules $B/%.o: src/%.c | $B/ $(CC) $(cc-flags) -c $< -o $@ -$B/wireguard.o: ext/wglib/wireguard.c ext/wglib/wireguard.h | $B/ +$B/wireguard.o: $E/wglib/wireguard.c $E/wglib/wireguard.h | $B/ $(CC) -std=c11 -c $< -o $@ # dep listings -$B/wgsync.o: ext/wglib/wireguard.h +$B/wgsync.o: $E/wglib/wireguard.h src/pqp.h src/def.h +$B/pqp.o: src/pqp.h src/def.h +$B/def.o: src/def.h # fetch rules %/: mkdir -p $@ wg-lib-uri = https://git.zx2c4.com/wireguard-tools/plain/contrib/embeddable-wg-library -ext/wglib/%: | ext/wglib/ +$E/wglib/%: | $E/wglib/ curl $(wg-lib-uri)/$* >$@ ADDED wgsync/src/def.c Index: wgsync/src/def.c ================================================================== --- wgsync/src/def.c +++ wgsync/src/def.c @@ -0,0 +1,28 @@ +#include "def.h" +#include +#include + +uint8_t g_loglevel = +#ifdef _cfg_debug + 4 +#else + 3 +#endif +; + +char thread_local g_logbuf [sizeof g_logbuf]; + +void msg +( uint8_t level, + char* tag, + uint8_t color, + char* msg +) { + if(level > g_loglevel) return; + if(isatty(fileno(stderr))) { + fprintf(stderr, "\x1b[1;3%cm(%s)\x1b[m %s\n", + 0x30 + color, tag, msg); + } else { + fprintf(stderr, "(%s) %s\n", tag, msg); + } +} ADDED wgsync/src/def.h Index: wgsync/src/def.h ================================================================== --- wgsync/src/def.h +++ wgsync/src/def.h @@ -0,0 +1,58 @@ +#pragma once + +/* "feature tests" */ +#define _POSIX_C_SOURCE 200112L + /* seteuid + * getnameinfo */ +#define _GNU_SOURCE + /* IFNAMSIZ [linux] + * getresuid [linux]*/ + +/* arch headers */ +#include +#include + +#define _layout struct __attribute__((__packed__)) +#define _zero(x) memset(&x, sizeof(x), 0) +#define _sz(x) (sizeof(x)/sizeof(x)[0]) +#define __cat(a,b) a##b +#define _cat(a,b) __cat(a,b) +#define _strify(...) #__VA_ARGS__ + +#if __STDC_VERSION__ == 202000L +/* incomplete support */ +#elif __STDC_VERSION__ > 202000L +# define null nullptr +#endif +#if __STDC_VERSION__ <= 202000L +# define null ((void*)0) +# define thread_local _Thread_local +# include +#endif + +extern uint8_t g_loglevel; +extern thread_local char g_logbuf [2 * 1024]; +#define _lprint(...) snprintf(g_logbuf, sizeof g_logbuf, __VA_ARGS__) + +void msg(uint8_t lvl, char* tag, uint8_t color, char* msg); + +#define _msgf(lvl, tag, color, ...) { \ + if (g_loglevel >= lvl) { \ + _lprint(__VA_ARGS__); \ + msg(lvl, tag, color, g_logbuf); \ + } \ +} + +#define _fatal(str) { msg(1, "fatal", 1, (str)); exit(1); } +#define _warn(str) msg(2, "warn", 3, (str)) +#define _info(str) msg(3, "info", 4, (str)) +#define _dbg(str) msg(4, "debug", 2, (str)) + +#define _fatalf(...) _msgf(1, "fatal", 1, __VA_ARGS__) +#define _warnf(...) _msgf(2, "warn", 3, __VA_ARGS__) +#define _infof(...) _msgf(3, "info", 4, __VA_ARGS__) +#define _dbgf(...) _msgf(4, "debug", 2, __VA_ARGS__) + +typedef struct string { + size_t sz; char* ptr; +} string; ADDED wgsync/src/list.h Index: wgsync/src/list.h ================================================================== --- wgsync/src/list.h +++ wgsync/src/list.h @@ -0,0 +1,66 @@ +#ifndef _ll_delete +#define _ll_delete free +#endif + +#ifndef _ll_ffirst +#define _ll_ffirst _cat(first_,_ll_rec) +#endif + +#ifndef _ll_flast +#define _ll_flast _cat(last_,_ll_rec) +#endif + +#ifndef _ll_fnext +#define _ll_fnext _cat(next_,_ll_rec) +#endif + +#ifndef _ll_dropfn +#define _ll_dropfn _cat(_cat(_ll_ns,_),_cat(drop_, _ll_rec)) +#endif + +#ifndef _ll_pushfn +#define _ll_pushfn _cat(_cat(_ll_ns,_),_cat(push_, _ll_rec)) +#endif + +void _ll_dropfn +(_ll_box* box, _ll_obj* obj) { + if(box -> _ll_ffirst == obj) { + if(box -> _ll_flast == obj) { + box -> _ll_ffirst = box -> _ll_flast = null; + } else { + box -> _ll_ffirst = obj -> _ll_fnext; + } + } else { + _ll_obj* a; + if(box -> _ll_flast == obj) { + _ll_iter (box, a) { + if(a->_ll_fnext == obj) { + box -> _ll_flast = a; + a -> _ll_fnext = null; + goto found1; + } + } + _fatal("BUG in last elt deletion routine"); + found1 :; + } else /* in the middle */ { + _ll_iter (box, a) { + if(a->_ll_fnext == obj) { + a->_ll_fnext = obj -> _ll_fnext; + goto found2; + } + } + _fatal("BUG in elt deletion routine"); + found2 :; + } + } + _ll_delete (obj); +} + +#undef _ll_ffirst +#undef _ll_flast +#undef _ll_ns +#undef _ll_box +#undef _ll_obj +#undef _ll_rec +#undef _ll_iter +#undef _ll_delete ADDED wgsync/src/pqp.c Index: wgsync/src/pqp.c ================================================================== --- wgsync/src/pqp.c +++ wgsync/src/pqp.c @@ -0,0 +1,58 @@ +#include "pqp.h" +#include +#include + +typedef _layout pq_array { + int32_t nonempty, nullable, ty, nelts, resv; + char body []; +} pq_array; + +typedef _layout pq_array_elt { + uint32_t sz; /* -1 for null */ + char body []; +} pq_array_elt; + +#include +struct pqp_array* +pqp_array_read(const void* pqary) { + const pq_array* pqa = pqary; + pqp_array* r = calloc(1, sizeof(pqp_array) + + (sizeof(pqp_array_elt) * pqa -> nelts)); + *r = (pqp_array) { + .ty = ntohl(pqa -> ty), + .nullable = ntohl(pqa -> nullable), + .sz = ntohl(pqa -> nelts), + }; + const char* p = pqa -> body; + for(size_t i = 0; i < r -> sz; ++ i) { + const pq_array_elt* e = (void*)p; + r -> elts[i] = (pqp_array_elt) { + .sz = ntohl(e -> sz), + .data = (e -> sz == pq_null ? null : e -> body), + }; + if (e -> sz == pq_null) p += 4; /* null */ + else p += 4 + r->elts[i].sz; + } + return r; +} + +bool pqp_inet_read(const void* pqinet, struct sockaddr* d) { + if(memcmp(pqinet,"\x02\x20\x00\x04",4) == 0) { + uint32_t ip = 0; + for(uint8_t i = 0; i<4; ++i) { + ip |= (uint32_t)(((uint8_t*)pqinet)[4 + i]) << 8*i; + } + struct sockaddr_in* dd = (void*)d; + _zero(*dd); + dd -> sin_family = AF_INET; + dd -> sin_port = 0; + dd -> sin_addr = (struct in_addr){ip}; + } else if(memcmp(pqinet,"\x03\x80\x00\x10",4) == 0) { + struct sockaddr_in6* dd = (void*)d; + _zero(*dd); + dd -> sin6_family = AF_INET6; + dd -> sin6_port = 0; + memcpy(&(dd->sin6_addr), pqinet+4, 16); + } else return false; + return true; +} ADDED wgsync/src/pqp.h Index: wgsync/src/pqp.h ================================================================== --- wgsync/src/pqp.h +++ wgsync/src/pqp.h @@ -0,0 +1,33 @@ +#include "def.h" +#define pq_null ((uint32_t)0xFFFFffff) +#include +#include + +typedef enum pq_array_type { + pq_array_bool = 0x10, + pq_array_int = 0x17, + pq_array_text = 0x19, + pq_array_inet = 0x365, +} pq_array_type; + +typedef struct pqp_array_elt { + uint32_t sz; + const char* data; +} pqp_array_elt; + +typedef struct pqp_array { + size_t sz; + bool nullable; + pq_array_type ty; + pqp_array_elt elts []; +} pqp_array; + +/* for receiving values from pqp_inet_read */ +typedef union pqp_sockstore { + struct sockaddr sock; + struct sockaddr_in sock_in; + struct sockaddr_in6 sock_in6; +} pqp_sockstore; + +struct pqp_array* pqp_array_read(const void* pqary); +bool pqp_inet_read(const void* pqinet, struct sockaddr* d); Index: wgsync/src/wgsync.c ================================================================== --- wgsync/src/wgsync.c +++ wgsync/src/wgsync.c @@ -1,5 +1,395 @@ +#include "def.h" +#include "pqp.h" + +/* libc */ +#include +#include +#include + +/* posix */ +#include +#include +#include +#include + +/* libs */ +#include + #include + +size_t dumpEndpoint(char* d, const wg_endpoint* const e) { + const struct sockaddr* addr; + size_t len; + switch(e->addr.sa_family) { + case AF_INET: addr = (void*)&(e->addr4); len = sizeof e->addr4; break; + case AF_INET6: addr = (void*)&(e->addr6); len = sizeof e->addr6; break; + case 0: strcpy(d, ""); return 16; + default: strcpy(d, ""); return 14; + } + char bip[256], bsrv[16]; + getnameinfo(addr, len, + bip, sizeof bip, + bsrv, sizeof bsrv, + NI_NUMERICHOST | NI_NUMERICSERV); + return sprintf(d, "%s:%s", bip, bsrv); +} + +size_t dumpAllowedIP(char* d, const wg_allowedip* aip) { + union { + struct sockaddr_in ip4; + struct sockaddr_in6 ip6; + } kinds; + size_t len; + switch(aip->family) { + case AF_INET: { + kinds.ip4 = (struct sockaddr_in) { + .sin_family = AF_INET, + .sin_port = 0, + .sin_addr = aip->ip4, + }; + len = sizeof kinds.ip4; + break;} + case AF_INET6: { + kinds.ip6 = (struct sockaddr_in6) { + .sin6_family = AF_INET6, + .sin6_port = 0, + .sin6_flowinfo = 0, + .sin6_scope_id = 0, + .sin6_addr = aip->ip6, + }; + len = sizeof kinds.ip6; + break;} + case 0: strcpy(d, ""); return 7; + default: strcpy(d, ""); return 16; + } + char bip[256], bsrv[2]; + getnameinfo((void*)&kinds, len, + bip, sizeof bip, + bsrv, sizeof bsrv, + NI_NUMERICHOST | NI_NUMERICSERV); + return sprintf(d, "%s/%u", bip, aip->cidr); +} + +bool compare_allowedip +( const wg_allowedip* const a, + const wg_allowedip* const b +) { + if(a -> family != b -> family) return false; + if(a -> cidr != b -> cidr) return false; + switch(a->family) { + case AF_INET: + if(a -> ip4.s_addr != b -> ip4.s_addr) return false; + break; + case AF_INET6: + if(memcmp(a -> ip6.s6_addr, b -> ip6.s6_addr, sizeof(a->ip6.s6_addr)) != 0) + return false; + break; + } + return true; +} + +wg_allowedip +inet_to_allowedip(const char* data) { + pqp_sockstore ss; + if(!pqp_inet_read(data, &ss.sock)) + _fatal("bad IP value in database"); + + wg_allowedip wgip = { + .family = ss.sock.sa_family, + .next_allowedip = null, + }; + switch(ss.sock.sa_family) { + case AF_INET: + wgip.cidr = 32; + wgip.ip4 = ss.sock_in.sin_addr; + break; + case AF_INET6: + wgip.cidr = 128; + wgip.ip6 = ss.sock_in6.sin6_addr; + break; + default: _fatal("unhandled address family"); + } + return wgip; +} + +void wgd_free_peer(wg_peer* peer) { + wg_allowedip *allowedip, *na; + /* from ext/wglib/wireguard.c:1486 */ + for ( + allowedip = peer->first_allowedip, + na = allowedip ? allowedip->next_allowedip : NULL; + allowedip; + allowedip = na, + na = allowedip ? allowedip->next_allowedip : NULL + ) free(allowedip); + /* end import */ + free(peer); +} + +/* linked list manipulation routines */ + +#define _ll_rec peer +#define _ll_box wg_device +#define _ll_obj wg_peer +#define _ll_iter wg_for_each_peer +#define _ll_ns wgd +#include "list.h" + +#define _ll_rec allowedip +#define _ll_box wg_peer +#define _ll_obj wg_allowedip +#define _ll_iter wg_for_each_allowedip +#define _ll_ns wgd_peer +#include "list.h" + +#if 0 +void wgd_drop_peer(wg_device* dev, wg_peer* peer) { + if(dev -> first_peer == peer) { + if(dev -> last_peer == peer) { + dev -> first_peer = dev -> last_peer = null; + } else { + dev -> first_peer = peer -> next_peer; + } + } else { + wg_peer* p; + if(dev -> last_peer == peer) { + wg_for_each_peer(dev, p) { + if(p->next_peer == peer) { + dev -> last_peer = p; + p->next_peer = null; + goto found1; + } + } + _fatal("BUG in last peer deletion routine"); + found1 :; + } else /* in the middle */ { + wg_for_each_peer(dev, p) { + if(p->next_peer == peer) { + p->next_peer = peer -> next_peer; + goto found2; + } + } + _fatal("BUG in peer deletion routine"); + found2 :; + } + } + wgd_free_peer(peer); +} +void wgd_peer_drop_ip(wg_peer* peer, wg_allowedip* ip) { + if(peer -> first_allowedip == ip) { + if(peer -> last_allowedip == ip) { + peer -> first_allowedip = peer -> last_allowedip = null; + } else { + peer -> first_allowedip = peer -> next_allowedip; + } + } else { + wg_allowedip* a; + if(peer -> last_allowedip == ip) { + wg_for_each_allowedip(peer, a) { + if(a->next_allowedip == ip) { + peer -> last_allowedip = a; + a->next_allowedip = null; + goto found1; + } + } + _fatal("BUG in last aIP deletion routine"); + found1 :; + } else /* in the middle */ { + wg_for_each_allowedip(peer, a) { + if(a->next_allowedip == ip) { + a->next_allowedip = ip -> next_allowedip; + goto found2; + } + } + _fatal("BUG in aIP deletion routine"); + found2 :; + } + } + free(ip); +} +#endif + +void syncauth(PGconn* db, const char* wgdev) { + wg_device* wg; + if (wg_get_device(&wg, wgdev)) + _fatal("no wireguard device by that name"); + + bool dirty = false; + size_t peerc = 0; + { wg_peer* p; wg_for_each_peer(wg, p) ++ peerc; }; + + bool valid_peers [peerc]; + _zero(valid_peers); + + PGresult* rows = PQexecPrepared(db, "get_hosts", + 0, null, null, null, 1); + if(!(rows && PQresultStatus(rows) == PGRES_TUPLES_OK)) + _fatal(PQerrorMessage(db)); + + size_t rowc = PQntuples(rows); + for(size_t i = 0; i < rowc; ++i) { + const char* key_b64 = PQgetvalue(rows, i, 0); + + const char* aryraw = PQgetvalue(rows, i, 1); + pqp_array* ips = pqp_array_read(aryraw); + _dbgf("DB has peer %s", key_b64); + if(ips->ty != pq_array_inet) + _fatal("incorrect array type returned from DB"); + + wg_key key; + if (wg_key_from_base64(key, key_b64) < 0) { + _warnf("invalid key in database: %s", key_b64); + continue; + } + + wg_peer* found = null; + { size_t j=0; wg_peer* p; wg_for_each_peer(wg, p) { + if(memcmp(p->public_key, key, sizeof key) == 0) { + _dbgf("validating peer %s", key_b64); + valid_peers[j] = true; + found = p; + break; + } + ++j;}} + + if (found) { + /* compare and update IPs if necessary */ + bool goodIPs [ips -> sz]; _zero(goodIPs); + /* extant IPs that are not marked good by the + * end of the following loop must be deleted + * from memory */ + size_t goodIPc = 0; + for (size_t j = 0; j < ips -> sz; ++j) { + char inetstr[256]; + wg_allowedip aip = inet_to_allowedip(ips -> elts[j].data); + dumpAllowedIP(inetstr, &aip); + _dbgf("IP PG%zu :: %s", j, inetstr); + + size_t l = 0; + wg_allowedip* wgip; + bool foundIP = false; + wg_for_each_allowedip(found, wgip) { + if (compare_allowedip(&aip, wgip)) { + ++goodIPc; goodIPs[l] = true; + foundIP = true; + } + ++l;} + + if(!foundIP) { + /* this IP hasn't been loaded into the + * kernel yet; upload it now */ + _infof("inserting IP PG%zu %s", j, inetstr); + dirty = true; + } + } + + if(goodIPc < ips -> sz) { + size_t l = 0; + wg_allowedip* wgip; + wg_for_each_allowedip(found, wgip) { + char inetstr[256]; + dumpAllowedIP(inetstr, wgip); + _dbgf("IP WG%zu :: %s", l, inetstr); + if(!goodIPs[l]) { + /* this IP is stale, delete it */ + _infof("deleting IP WG%zu %s", l, inetstr); + dirty = true; + } + ++l;} + } + } else { + _infof("inserting key %s", key_b64); + dirty = true; + /* install new peer */ + for (size_t j = 0; j < ips -> sz; ++j) { + char inetstr[256]; + wg_allowedip aip = inet_to_allowedip(ips -> elts[j].data); + dumpAllowedIP(inetstr, &aip); + _dbgf("new IP %zu :: %s", j, inetstr); + } + } + + free(ips); + } + { size_t i=0; wg_peer* p; wg_for_each_peer(wg, p) { + if(valid_peers[i] == false) { + char b64 [128]; + wg_key_to_base64(b64, p->public_key); + _infof("dropping peer %s", b64); + wgd_drop_peer(wg, p); + dirty = true; + } + ++i;}} + + _dbg("final peer list:"); + { size_t j=0; wg_peer* p; wg_for_each_peer(wg, p) { + char b64 [128]; + wg_key_to_base64(b64, p->public_key); + _dbgf("P%zu :: %s", j, b64); + ++j;}} + + if(dirty) wg_set_device(wg); + + PQclear(rows); +} + int main(int argc, char** argv) { + setvbuf(stderr, null, _IONBF, 0); + if (argc < 3) { + _fatal("missing device name"); + } + + const char* arg_mode = argv[1]; + const char* arg_devname = argv[2]; + + /* mostly for the sake of debugging, allow the + * binary to be run from sudo without losing + * postgres peer credentials */ + if(geteuid() == 0) { + char* suid = getenv("SUDO_UID"); + char* susr = getenv("SUDO_USER"); + if(suid) seteuid(atoi(suid)); + if(susr) setenv("USER",getenv("SUDO_USER"), 1); + } + + PGconn* db = PQconnectdb("dbname=domain"); + if(PQstatus(db) != CONNECTION_OK) + _fatal(PQerrorMessage(db)); + + PGresult* q_get_hosts = PQprepare(db, "get_hosts", + "select h.ref, array_remove(array_agg(wgv4::inet)" + "|| array_agg(wgv6::inet), null)" + "from ns, hostref h " + "where ns.host = h.host and kind = 'pubkey' " + " group by h.host, h.ref;", 0, null); + /*"select ns.wgv4::inet, ns.wgv6::inet, h.ref from ns " + "right join hostref h " + "on h.host = ns.host " + "where h.kind = 'pubkey';"*/ + if(!(q_get_hosts && PQresultStatus(q_get_hosts) == PGRES_COMMAND_OK)) + _fatal(PQerrorMessage(db)); + PQclear(q_get_hosts); + + /* we're going to interact with WG now; + * get our superpowers back if we lost them */ + {uid_t svuid; + getresuid(null, null, &svuid); + if (svuid == 0) setuid(0);} + + if(strcmp(arg_mode, "sync") == 0) { + syncauth(db, arg_devname); + } else if(strcmp(arg_mode, "wait") == 0) { + /* foreground daemon */ + } else if(strcmp(arg_mode, "fork") == 0) { + /* background daemon */ + } else { + _fatal("valid modes are sync, wait, and fork"); + } + /* other possibilities: a mode that generates an eventfd + * and provides it on fd4 to a subordinate process, or + * sends it with SCM_RIGHTS */ + + PQfinish(db); return 0; }