network: add support for configuring extra peers via a separate json file
[project/unetd.git] / cli.c
1 // SPDX-License-Identifier: GPL-2.0-or-later
2 /*
3 * Copyright (C) 2022 Felix Fietkau <nbd@nbd.name>
4 */
5 #include <sys/stat.h>
6 #include <sys/time.h>
7 #include <sys/mman.h>
8 #include <sys/types.h>
9 #include <sys/socket.h>
10 #include <arpa/inet.h>
11 #include <stdio.h>
12 #include <stdint.h>
13 #include <stdlib.h>
14 #include <fcntl.h>
15 #include <errno.h>
16 #include <libubox/utils.h>
17 #include <libubox/uloop.h>
18 #include <libubox/blobmsg.h>
19 #include <libubox/blobmsg_json.h>
20 #include "edsign.h"
21 #include "ed25519.h"
22 #include "curve25519.h"
23 #include "auth-data.h"
24 #include "pex-msg.h"
25
26 static uint8_t peerkey[EDSIGN_PUBLIC_KEY_SIZE];
27 static uint8_t pubkey[EDSIGN_PUBLIC_KEY_SIZE];
28 static uint8_t seckey[EDSIGN_PUBLIC_KEY_SIZE];
29 static void *net_data;
30 static size_t net_data_len;
31 static uint64_t net_data_version;
32 static struct blob_attr *net_data_hosts;
33 static uint64_t req_id;
34 static struct blob_buf b;
35 static FILE *out_file;
36 static bool quiet;
37 static bool sync_done;
38 static enum {
39 CMD_UNKNOWN,
40 CMD_GENERATE,
41 CMD_PUBKEY,
42 CMD_HOST_PUBKEY,
43 CMD_VERIFY,
44 CMD_SIGN,
45 CMD_DOWNLOAD,
46 CMD_UPLOAD,
47 } cmd;
48
49 #define INFO(...) \
50 do { \
51 if (quiet) \
52 break; \
53 fprintf(stderr, ##__VA_ARGS__); \
54 } while (0)
55
56 static void print_key(const uint8_t *key)
57 {
58 char keystr[B64_ENCODE_LEN(EDSIGN_PUBLIC_KEY_SIZE)];
59
60 if (b64_encode(key, EDSIGN_PUBLIC_KEY_SIZE, keystr, sizeof(keystr)) < 0)
61 return;
62
63 fprintf(out_file, "%s\n", keystr);
64 }
65
66 static int usage(const char *progname)
67 {
68 fprintf(stderr, "Usage: %s [command|options] [<file>]\n"
69 "Commands:\n"
70 " -S Sign file\n"
71 " -V Verify file\n"
72 " -P Get pulic signing key from secret key\n"
73 " -H Get pulic host key from secret key\n"
74 " -G Generate new private key\n"
75 " -D <host>[:<port>] Download network data from unetd\n"
76 " -U <host>[:<port>] Upload network data to unetd\n"
77 "\n"
78 "Options:\n"
79 " -q: Quiet mode - suppress error/info messages\n"
80 " -o <file>: Set output file to <file> (defaults to stdout)\n"
81 " -k <keyfile>|-: Set public key from file or stdin\n"
82 " -K <keyfile>|-: Set secret key from file or stdin\n"
83 " -h <keyfile>|- Set peer private key from file or stdin\n"
84 " (for network data down-/upload)\n"
85 "\n", progname);
86 return 1;
87 }
88
89 static void pex_timeout(struct uloop_timeout *timeout)
90 {
91 uloop_end();
92 }
93
94 static void
95 pex_recv_update_response(const uint8_t *data, size_t len, enum pex_opcode op)
96 {
97 int net_data_len = 0;
98 void *net_data;
99
100 net_data = pex_msg_update_response_recv(data, len, op, &net_data_len, NULL);
101 if (net_data_len < 0)
102 goto out;
103
104 if (!net_data)
105 return;
106
107 if (cmd == CMD_DOWNLOAD) {
108 fwrite(net_data, net_data_len, 1, out_file);
109 sync_done = true;
110 }
111
112 free(net_data);
113
114 out:
115 if (cmd == CMD_DOWNLOAD)
116 uloop_end();
117 }
118
119 static bool
120 pex_get_pubkey(uint8_t *pubkey, const uint8_t *id)
121 {
122 static const struct blobmsg_policy policy = { "key", BLOBMSG_TYPE_STRING };
123 struct blob_attr *cur, *key;
124 int rem;
125
126 blobmsg_for_each_attr(cur, net_data_hosts, rem) {
127 const char *keystr;
128
129 blobmsg_parse(&policy, 1, &key, blobmsg_data(cur), blobmsg_len(cur));
130
131 if (!key)
132 continue;
133
134 keystr = blobmsg_get_string(key);
135 if (b64_decode(keystr, pubkey, CURVE25519_KEY_SIZE) != CURVE25519_KEY_SIZE)
136 continue;
137
138 if (!memcmp(pubkey, id, PEX_ID_LEN))
139 return true;
140 }
141
142 return false;
143 }
144
145 static void
146 pex_handle_update_request(struct sockaddr_in6 *addr, const uint8_t *id, void *data, size_t len)
147 {
148 struct pex_msg_update_send_ctx ctx = {};
149 static uint8_t empty_key[EDSIGN_PUBLIC_KEY_SIZE] = {};
150 uint8_t peerpubkey[EDSIGN_PUBLIC_KEY_SIZE];
151 bool done = false;
152
153 if (!pex_get_pubkey(peerpubkey, id)) {
154 INFO("Could not find public key\n");
155 return;
156 }
157
158 pex_msg_update_response_init(&ctx, empty_key, pubkey,
159 peerpubkey, true, data, net_data, net_data_len);
160 while (!done) {
161 __pex_msg_send(-1, NULL, NULL, 0);
162 done = !pex_msg_update_response_continue(&ctx);
163 }
164 sync_done = true;
165 uloop_end();
166 }
167
168 static void pex_recv(struct pex_hdr *hdr, struct sockaddr_in6 *addr)
169 {
170 struct pex_ext_hdr *ehdr = (void *)(hdr + 1);
171 void *data = (void *)(ehdr + 1);
172 uint32_t len = be32_to_cpu(hdr->len);
173 uint64_t *msg_req_id = data;
174
175 if (hdr->version != 0)
176 return;
177
178 if (memcmp(ehdr->auth_id, pubkey, sizeof(ehdr->auth_id)) != 0)
179 return;
180
181 *(uint64_t *)hdr->id ^= pex_network_hash(pubkey, ehdr->nonce);
182
183 switch (hdr->opcode) {
184 case PEX_MSG_UPDATE_REQUEST:
185 if (cmd != CMD_UPLOAD)
186 break;
187
188 pex_handle_update_request(addr, hdr->id, data, len);
189 break;
190 case PEX_MSG_UPDATE_RESPONSE:
191 case PEX_MSG_UPDATE_RESPONSE_DATA:
192 case PEX_MSG_UPDATE_RESPONSE_NO_DATA:
193 if (len < sizeof(*msg_req_id) || *msg_req_id != req_id)
194 break;
195
196 if (cmd == CMD_DOWNLOAD &&
197 hdr->opcode == PEX_MSG_UPDATE_RESPONSE_NO_DATA) {
198 INFO("No network data available\n");
199 uloop_end();
200 }
201
202 if (cmd == CMD_UPLOAD &&
203 hdr->opcode != PEX_MSG_UPDATE_RESPONSE_NO_DATA) {
204 INFO("Server has newer network data\n");
205 uloop_end();
206 }
207
208 pex_recv_update_response(data, hdr->len, hdr->opcode);
209 break;
210 }
211 }
212
213 static int load_network_data(const char *file)
214 {
215 static const struct blobmsg_policy policy = { "hosts", BLOBMSG_TYPE_TABLE };
216 struct unet_auth_hdr *hdr;
217 struct unet_auth_data *data;
218 const char *json;
219
220 net_data_len = UNETD_NET_DATA_SIZE_MAX;
221 net_data = unet_read_file(file, &net_data_len);
222 if (!net_data) {
223 INFO("failed to read input file %s\n", file);
224 return 1;
225 }
226
227 if (unet_auth_data_validate(NULL, net_data, net_data_len, &net_data_version, &json) < 0) {
228 INFO("input data validation failed\n");
229 return 1;
230 }
231
232 hdr = net_data;
233 data = (struct unet_auth_data *)(hdr + 1);
234 memcpy(pubkey, data->pubkey, sizeof(pubkey));
235
236 blob_buf_init(&b, 0);
237 blobmsg_add_json_from_string(&b, json);
238
239 blobmsg_parse(&policy, 1, &net_data_hosts, blobmsg_data(b.head), blobmsg_len(b.head));
240 if (!net_data_hosts) {
241 INFO("network data is missing the hosts attribute\n");
242 return 1;
243 }
244
245 return 0;
246 }
247
248
249 static int cmd_sync(const char *endpoint, int argc, char **argv)
250 {
251 uint8_t peerpubkey[EDSIGN_PUBLIC_KEY_SIZE];
252 struct uloop_timeout timeout = {
253 .cb = pex_timeout
254 };
255 struct pex_update_request *req;
256 union network_endpoint ep = {};
257 int len;
258
259 if (cmd == CMD_UPLOAD) {
260 if (argc < 1) {
261 INFO("missing file argument\n");
262 return 1;
263 }
264
265 if (load_network_data(argv[0]))
266 return 1;
267 }
268
269 if (network_get_endpoint(&ep, endpoint, UNETD_GLOBAL_PEX_PORT, 0) < 0) {
270 INFO("Invalid hostname/port %s\n", endpoint);
271 return 1;
272 }
273
274 len = ep.sa.sa_family == AF_INET6 ? sizeof(ep.in6) : sizeof(ep.in);
275
276 uloop_init();
277
278 if (pex_open(&ep, len, pex_recv, false) < 0)
279 return 1;
280
281 uloop_timeout_set(&timeout, 5000);
282
283 curve25519_generate_public(peerpubkey, peerkey);
284 req = pex_msg_update_request_init(peerpubkey, peerkey, pubkey, &ep,
285 net_data_version, true);
286 if (!req)
287 return 1;
288
289 req_id = req->req_id;
290 if (__pex_msg_send(-1, NULL, NULL, 0) < 0) {
291 if (!quiet)
292 perror("send");
293 return 1;
294 }
295
296 uloop_run();
297
298 return !sync_done;
299 }
300
301 static int cmd_sign(int argc, char **argv)
302 {
303 struct unet_auth_hdr hdr = {
304 .magic = cpu_to_be32(UNET_AUTH_MAGIC),
305 };
306 struct unet_auth_data *data;
307 struct timeval tv;
308 struct stat st;
309 off_t len;
310 FILE *f;
311
312 if (argc != 1) {
313 INFO("Missing filename\n");
314 return 1;
315 }
316
317 if (gettimeofday(&tv, NULL)) {
318 if (!quiet)
319 perror("gettimeofday");
320 return 1;
321 }
322
323 if (stat(argv[0], &st) ||
324 (f = fopen(argv[0], "r")) == NULL) {
325 INFO("Input file not found\n");
326 return 1;
327 }
328
329 data = calloc(1, sizeof(*data) + st.st_size + 1);
330 data->timestamp = cpu_to_be64(tv.tv_sec);
331 len = fread(data + 1, 1, st.st_size, f);
332 fclose(f);
333
334 if (len != st.st_size) {
335 INFO("Error reading from input file\n");
336 return 1;
337 }
338
339 len += sizeof(*data) + 1;
340
341 memcpy(data->pubkey, pubkey, sizeof(pubkey));
342 edsign_sign(hdr.signature, pubkey, seckey, (const void *)data, len);
343
344 fwrite(&hdr, sizeof(hdr), 1, out_file);
345 fwrite(data, len, 1, out_file);
346
347 free(data);
348
349 return 0;
350 }
351
352 static int cmd_verify(int argc, char **argv)
353 {
354 struct unet_auth_data *data;
355 struct unet_auth_hdr *hdr;
356 struct stat st;
357 off_t len;
358 FILE *f;
359 int ret = 1;
360
361 if (argc != 1) {
362 INFO("Missing filename\n");
363 return 1;
364 }
365
366 if (stat(argv[0], &st) ||
367 (f = fopen(argv[0], "r")) == NULL) {
368 INFO("Input file not found\n");
369 return 1;
370 }
371
372 if (st.st_size <= sizeof(*hdr) + sizeof(*data)) {
373 INFO("Input file too small\n");
374 fclose(f);
375 return 1;
376 }
377
378 hdr = calloc(1, st.st_size);
379 len = fread(hdr, 1, st.st_size, f);
380 fclose(f);
381
382 if (len != st.st_size) {
383 INFO("Error reading from input file\n");
384 return 1;
385 }
386
387 ret = unet_auth_data_validate(pubkey, hdr, len, NULL, NULL);
388 switch (ret) {
389 case -1:
390 INFO("Invalid input data\n");
391 break;
392 case -2:
393 INFO("Public key does not match\n");
394 break;
395 case -3:
396 INFO("Signature verification failed\n");
397 break;
398 }
399
400 free(hdr);
401 return ret;
402 }
403
404 static int cmd_host_pubkey(int argc, char **argv)
405 {
406 curve25519_generate_public(pubkey, seckey);
407 print_key(pubkey);
408
409 return 0;
410 }
411
412 static int cmd_pubkey(int argc, char **argv)
413 {
414 print_key(pubkey);
415
416 return 0;
417 }
418
419 static int cmd_generate(int argc, char **argv)
420 {
421 FILE *f;
422 int ret;
423
424 f = fopen("/dev/urandom", "r");
425 if (!f) {
426 INFO("Can't open /dev/urandom\n");
427 return 1;
428 }
429
430 ret = fread(seckey, sizeof(seckey), 1, f);
431 fclose(f);
432
433 if (ret != 1) {
434 INFO("Can't read data from /dev/urandom\n");
435 return 1;
436 }
437
438 ed25519_prepare(seckey);
439 print_key(seckey);
440
441 return 0;
442 }
443
444 static bool parse_key(uint8_t *dest, const char *str)
445 {
446 char keystr[B64_ENCODE_LEN(EDSIGN_PUBLIC_KEY_SIZE) + 2];
447 FILE *f;
448 int len;
449
450 if (!strcmp(str, "-"))
451 f = stdin;
452 else
453 f = fopen(str, "r");
454
455 if (!f) {
456 INFO("Can't open key file for reading\n");
457 return false;
458 }
459
460 len = fread(keystr, 1, sizeof(keystr) - 1, f);
461 if (f != stdin)
462 fclose(f);
463
464 keystr[len] = 0;
465
466 if (b64_decode(keystr, dest, EDSIGN_PUBLIC_KEY_SIZE) != EDSIGN_PUBLIC_KEY_SIZE) {
467 INFO("Failed to parse key data\n");
468 return false;
469 }
470
471 return true;
472 }
473
474 static bool cmd_needs_peerkey(void)
475 {
476 switch (cmd) {
477 case CMD_DOWNLOAD:
478 return true;
479 default:
480 return false;
481 }
482 }
483
484 static bool cmd_needs_pubkey(void)
485 {
486 switch (cmd) {
487 case CMD_DOWNLOAD:
488 case CMD_VERIFY:
489 return true;
490 default:
491 return false;
492 }
493 }
494
495 static bool cmd_needs_key(void)
496 {
497 switch (cmd) {
498 case CMD_SIGN:
499 case CMD_PUBKEY:
500 case CMD_HOST_PUBKEY:
501 return true;
502 default:
503 return false;
504 }
505 }
506
507 static bool cmd_needs_outfile(void)
508 {
509 switch (cmd) {
510 case CMD_SIGN:
511 case CMD_PUBKEY:
512 case CMD_GENERATE:
513 case CMD_DOWNLOAD:
514 return true;
515 default:
516 return false;
517 }
518 }
519
520 int main(int argc, char **argv)
521 {
522 const char *progname = argv[0];
523 const char *out_filename = NULL;
524 const char *cmd_arg = NULL;
525 bool has_key = false, has_pubkey = false;
526 bool has_peerkey = false;
527 int ret, ch;
528
529 while ((ch = getopt(argc, argv, "h:k:K:o:qD:GHPSU:V")) != -1) {
530 switch (ch) {
531 case 'D':
532 case 'U':
533 case 'G':
534 case 'H':
535 case 'S':
536 case 'P':
537 case 'V':
538 if (cmd != CMD_UNKNOWN)
539 return usage(progname);
540 break;
541 default:
542 break;
543 }
544
545 switch (ch) {
546 case 'q':
547 quiet = true;
548 break;
549 case 'o':
550 out_filename = optarg;
551 break;
552 case 'h':
553 if (has_peerkey)
554 return usage(progname);
555
556 if (!parse_key(peerkey, optarg)) {
557 return 1;
558 }
559
560 has_peerkey = true;
561 break;
562 case 'k':
563 if (has_pubkey)
564 return usage(progname);
565
566 if (!parse_key(pubkey, optarg)) {
567 return 1;
568 }
569
570 has_pubkey = true;
571 break;
572 case 'K':
573 if (has_pubkey)
574 return usage(progname);
575
576 if (!parse_key(seckey, optarg)) {
577 return 1;
578 }
579
580 has_key = true;
581
582 edsign_sec_to_pub(pubkey, seckey);
583 has_pubkey = true;
584 break;
585 case 'U':
586 cmd = CMD_UPLOAD;
587 cmd_arg = optarg;
588 break;
589 case 'D':
590 cmd = CMD_DOWNLOAD;
591 cmd_arg = optarg;
592 break;
593 case 'G':
594 cmd = CMD_GENERATE;
595 break;
596 case 'S':
597 cmd = CMD_SIGN;
598 break;
599 case 'P':
600 cmd = CMD_PUBKEY;
601 break;
602 case 'H':
603 cmd = CMD_HOST_PUBKEY;
604 break;
605 case 'V':
606 cmd = CMD_VERIFY;
607 break;
608 default:
609 return usage(progname);
610 }
611 }
612
613 if (!has_peerkey && cmd_needs_peerkey()) {
614 INFO("Missing -h <key> argument\n");
615 return 1;
616 }
617
618 if (!has_key && cmd_needs_key()) {
619 INFO("Missing -K <key> argument\n");
620 return 1;
621 }
622
623 if (!has_pubkey && cmd_needs_pubkey()) {
624 INFO("Missing -k <key> argument\n");
625 return 1;
626 }
627
628 argc -= optind;
629 argv += optind;
630
631 if (out_filename && cmd_needs_outfile()) {
632 out_file = fopen(out_filename, "w");
633 if (!out_file) {
634 INFO("Failed to open output file\n");
635 return 1;
636 }
637 } else {
638 out_file = stdout;
639 }
640
641 ret = -1;
642 switch (cmd) {
643 case CMD_UPLOAD:
644 case CMD_DOWNLOAD:
645 ret = cmd_sync(cmd_arg, argc, argv);
646 break;
647 case CMD_GENERATE:
648 ret = cmd_generate(argc, argv);
649 break;
650 case CMD_SIGN:
651 ret = cmd_sign(argc, argv);
652 break;
653 case CMD_PUBKEY:
654 ret = cmd_pubkey(argc, argv);
655 break;
656 case CMD_HOST_PUBKEY:
657 ret = cmd_host_pubkey(argc, argv);
658 break;
659 case CMD_VERIFY:
660 ret = cmd_verify(argc, argv);
661 break;
662 case CMD_UNKNOWN:
663 ret = usage(progname);
664 break;
665 }
666
667 if (net_data)
668 free(net_data);
669
670 blob_buf_free(&b);
671
672 if (out_file != stdout) {
673 fclose(out_file);
674 if (ret)
675 unlink(out_filename);
676 }
677
678 return ret;
679 }