diff options
Diffstat (limited to 'tools')
-rw-r--r-- | tools/CMakeLists.txt | 37 | ||||
-rw-r--r-- | tools/rpki-rov.1 | 29 | ||||
-rw-r--r-- | tools/rpki-rov.c | 231 | ||||
-rw-r--r-- | tools/rtrclient.1 | 111 | ||||
-rw-r--r-- | tools/rtrclient.c | 1013 | ||||
-rw-r--r-- | tools/templates.h.cmake | 22 | ||||
-rw-r--r-- | tools/templates/csv | 3 | ||||
-rw-r--r-- | tools/templates/csvwithheader | 4 | ||||
-rw-r--r-- | tools/templates/default | 3 | ||||
-rw-r--r-- | tools/templates/json | 10 |
10 files changed, 1463 insertions, 0 deletions
diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt new file mode 100644 index 0000000..e101b49 --- /dev/null +++ b/tools/CMakeLists.txt @@ -0,0 +1,37 @@ +add_executable(rtrclient rtrclient.c ${mustach} ${tommyds}) +set_source_files_properties("${mustach}" PROPERTIES COMPILE_FLAGS "-DNO_EXTENSION_FOR_MUSTACH -DNO_OPEN_MEMSTREAM") +set_source_files_properties("${tommyds}" PROPERTIES COMPILE_FLAGS "-Dtommy_malloc=malloc -Dtommy_calloc=calloc -Dtommy_realloc=realloc -Dtommy_free=free -include stdlib.h") +target_link_libraries(rtrclient rtrlib) +install(TARGETS rtrclient DESTINATION bin) +install(FILES "rtrclient.1" DESTINATION "${CMAKE_INSTALL_MANDIR}/man1") + +add_executable(rpki-rov rpki-rov.c) +target_link_libraries(rpki-rov rtrlib) +install(TARGETS rpki-rov DESTINATION bin) +install(FILES "rpki-rov.1" DESTINATION "${CMAKE_INSTALL_MANDIR}/man1") + + +set(rtrclient_pfx_templates default csv csvwithheader json) + +# Generate escaped string sequence for every template file +# This runs at configure time, changes are not picked up automatically +foreach(template_name IN LISTS rtrclient_pfx_templates) + file(READ "templates/${template_name}" template_data HEX) + string(LENGTH "${template_data}" template_length) + math(EXPR template_length "${template_length} - 1") + + set(TEMPLATES "${TEMPLATES}{ .name = \"${template_name}\", .template = \"") + + foreach(iter RANGE 0 ${template_length} 2) + string(SUBSTRING ${template_data} ${iter} 2 line) + set(TEMPLATES "${TEMPLATES}\\x${line}") + endforeach() + set(TEMPLATES "${TEMPLATES} \"},\n") + +endforeach() + +string(STRIP ${TEMPLATES} TEMPLATES) + +CONFIGURE_FILE(templates.h.cmake templates.h) + +include_directories(${CMAKE_CURRENT_BINARY_DIR}) diff --git a/tools/rpki-rov.1 b/tools/rpki-rov.1 new file mode 100644 index 0000000..37ac947 --- /dev/null +++ b/tools/rpki-rov.1 @@ -0,0 +1,29 @@ +.\" +.\" This file is part of RTRlib. +.\" +.\" This file is subject to the terms and conditions of the MIT license. +.\" See the file LICENSE in the top level directory for more details. +.\" +.\" Website: http://rtrlib.realmv6.org/ +.\" +.TH "rpki-rov" "1" +.SH NAME +rpki-rov - rpki route origin validator +.SH SYNOPSIS +.B rpki-rov +.IR HOST +.IR PORT +.SH DESCRIPTION +\fBrpki-rov\fR is a command line interface that implements RPKI route origin validation. +It connects to the rpki server described by \fIhost\fR and \fIport\fR and can be used interactively or via pipes. +The format of the input parameter is always "\fIIP PREFIXLENGTH ASN\fR" followed by a newline. +.SH EXAMPLES +Pipe into stdin +.PP +.nf +.RS +echo "93.175.146.0 24 12654" | rpki-rov rpki.example.com 8282 +.RE +.fi +.SH BUGS +Does currently not support ssh-based rpki server. diff --git a/tools/rpki-rov.c b/tools/rpki-rov.c new file mode 100644 index 0000000..4089a88 --- /dev/null +++ b/tools/rpki-rov.c @@ -0,0 +1,231 @@ +/* + * This file is part of RTRlib. + * + * This file is subject to the terms and conditions of the MIT license. + * See the file LICENSE in the top level directory for more details. + * + * Website: http://rtrlib.realmv6.org/ + */ + +#include "rtrlib/rtrlib.h" + +#include <arpa/inet.h> +#include <errno.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +const int connection_timeout = 20; +enum rtr_mgr_status connection_status = -1; + +static void connection_status_callback(const struct rtr_mgr_group *group __attribute__((unused)), + enum rtr_mgr_status status, + const struct rtr_socket *socket __attribute__((unused)), + void *data __attribute__((unused))) +{ + if (status == RTR_MGR_ERROR) + connection_status = status; +} + +static int connection_error(enum rtr_mgr_status status) +{ + if (status == RTR_MGR_ERROR) { + /* + * Wait for input before printing error to avoid "broken pipe" error + * while communicating with the Python program. + */ + char input[256]; + + if (fgets(input, 256, stdin)) + ; + printf("error\n"); + fflush(stdout); + return 1; + } + return 0; +} + +static int str_to_int(const char *str, int *value) +{ + errno = 0; + int tmp = strtol(str, NULL, 10); + + if (errno != 0) + return 1; + + *value = tmp; + return 0; +} + +int main(int argc, char *argv[]) +{ + /* check arguments, need hostname/IP and port of cache-server */ + if (argc < 3) { + printf("Usage: %s [host] [port]\n", argv[0]); + return EXIT_FAILURE; + } + + struct tr_socket tr_tcp; + struct tr_tcp_config tcp_config = {argv[1], argv[2], NULL, NULL, NULL, 0}; + struct rtr_socket rtr_tcp; + struct rtr_mgr_config *conf; + struct rtr_mgr_group groups[1]; + + /* init a TCP transport and create rtr socket */ + tr_tcp_init(&tcp_config, &tr_tcp); + rtr_tcp.tr_socket = &tr_tcp; + + /* create a rtr_mgr_group array with 1 element */ + groups[0].sockets = malloc(1 * sizeof(struct rtr_socket *)); + groups[0].sockets_len = 1; + groups[0].sockets[0] = &rtr_tcp; + groups[0].preference = 1; + + if (rtr_mgr_init(&conf, groups, 1, 30, 600, 600, NULL, NULL, &connection_status_callback, NULL) < 0) + return EXIT_FAILURE; + + rtr_mgr_start(conf); + + char input[256]; + int sleep_counter = 0; + + /* wait till at least one rtr_mgr_group is synchronized with server */ + while (!rtr_mgr_conf_in_sync(conf)) { + if (connection_error(connection_status)) + return EXIT_FAILURE; + + sleep(1); + sleep_counter++; + if (sleep_counter >= connection_timeout) { + /* + * Wait for input before printing "timeout", + * to avoid "broken pipee error while communicating + * with the Python program + */ + if (fgets(input, 256, stdin)) + ; + printf("timeout\n"); + fflush(stdout); + return EXIT_FAILURE; + } + } + + int counter; + /* loop for input */ + while (1) { + int input_len; + int spaces; + + /* recheck connection, exit on failure */ + if (connection_error(connection_status)) + return EXIT_FAILURE; + + /* try reading from stdin, exit on failure */ + if (!fgets(input, 256, stdin)) { + printf("input error\n"); + return EXIT_FAILURE; + } + + /* remove newline, if present */ + input_len = strlen(input) - 1; + if (input[input_len] == '\n') + input[input_len] = '\0'; + + /* check if there are exactly 3 arguments */ + spaces = 0; + for (counter = 0; counter < input_len; counter++) { + if (input[counter] == ' ' && input[counter + 1] != ' ' && input[counter + 1] != '\0' && + counter != 0) + spaces++; + } + + /* check input matching pattern */ + if (spaces != 2) { + printf("Arguments required: IP Mask ASN\n"); + fflush(stdout); + continue; + } + + char delims[] = " "; + char *input_tok = NULL; + + input_tok = strtok(input, delims); + struct lrtr_ip_addr pref; + char ip[INET6_ADDRSTRLEN]; + + if (strlen(input_tok) > sizeof(ip) - 1) { + fprintf(stderr, "Error: Invalid ip addr\n"); + continue; + } + + memset(ip, 0, sizeof(ip)); + strncpy(ip, input_tok, sizeof(ip) - 1); + + if (lrtr_ip_str_to_addr(ip, &pref) != 0) { + fprintf(stderr, "Error: Invalid ip addr\n"); + continue; + } + + input_tok = strtok(NULL, delims); + int mask; + + if (str_to_int(input_tok, &mask)) { + fprintf(stderr, "Error: Invalid mask\n"); + continue; + } + + input_tok = strtok(NULL, delims); + int asn; + + if (str_to_int(input_tok, &asn)) { + fprintf(stderr, "Error: Invalid asn\n"); + continue; + } + + enum pfxv_state result; + struct pfx_record *reason = NULL; + unsigned int reason_len = 0; + + /* do validation */ + pfx_table_validate_r(groups[0].sockets[0]->pfx_table, &reason, &reason_len, asn, &pref, mask, &result); + + int validity_code = -1; + /* translate validation result */ + if (result == BGP_PFXV_STATE_VALID) + validity_code = 0; + else if (result == BGP_PFXV_STATE_NOT_FOUND) + validity_code = 1; + else if (result == BGP_PFXV_STATE_INVALID) + validity_code = 2; + + /* IP Mask BGP-ASN| */ + printf("%s %d %d|", ip, mask, asn); + + /* ROA-ASN IP MaskMin MaskMax, ... */ + if (reason && (reason_len > 0)) { + unsigned int i; + + for (i = 0; i < reason_len; i++) { + char tmp[100]; + + lrtr_ip_addr_to_str(&reason[i].prefix, tmp, sizeof(tmp)); + printf("%u %s %u %u", reason[i].asn, tmp, reason[i].min_len, reason[i].max_len); + if ((i + 1) < reason_len) + printf(","); + } + } + + /* |validity_code */ + printf("|%d", validity_code); + + printf("\n"); + fflush(stdout); + } + + rtr_mgr_stop(conf); + rtr_mgr_free(conf); + free(groups[0].sockets); + + return EXIT_SUCCESS; +} diff --git a/tools/rtrclient.1 b/tools/rtrclient.1 new file mode 100644 index 0000000..f9123a4 --- /dev/null +++ b/tools/rtrclient.1 @@ -0,0 +1,111 @@ +.\" +.\" This file is part of RTRlib. +.\" +.\" This file is subject to the terms and conditions of the MIT license. +.\" See the file LICENSE in the top level directory for more details. +.\" +.\" Website: http://rtrlib.realmv6.org/ +.\" +.TH "rtrclient" "1" +.SH NAME +rtrclient \- rtr rpki client +.SH SYNOPSIS +.B rtrclient +[\fB\-kph\fR] +.I SOCKETS\fR... +.SH SOCKETS +.B tcp +[\fB\-kpb \fIbindaddr\fR] +.IR HOST +.IR PORT +.br +.B ssh +[\fB\-kpb \fIbindaddr\fR] +.IR HOST +.IR PORT +.IR USERNAME +(\fIPRIVATE_KEY\fR|\fIPASSWORD\fR) +[\fIHOST_KEY\fR] +.SH DESCRIPTION +\fBrtrclient\fR connects to an RPKI/RTR cache server and prints prefix, origin AS, and router key updates. +\fBrtrclient\fR can use plain tcp or ssh transport to connect to an RPKI/RTR cache server. +The amount is not limited and different transport types can be mixed arbitrarily. +.LP +For \fBtcp\fR you must specify the \fIHOST\fR and \fIPORT\fR. +.LP +For \fBssh\fR you must specify the \fIHOST\fR, \fIPORT\fR, \fIUSERNAME\fR and a file containing the \fIPRIVATE_KEY\fR or a \fIPASSWORD\fR. +By default the rtrclient will try to guess which of the two was entered. If you want to explicitly specify this see \fB-w\fR and \fB-s\fR. +You may specify a file containing a list of \fIHOST_KEY\fRs, in the well known +.B SSH_KNOWN_HOSTS +file format. See \fIsshd(8)\fR for details. +.SH OPTIONS +\fB-b \fIbindaddr\fR +.RS 4 +Set explicit bind address +.RE +.B -h +.RS 4 +Print help message +.RE +\fB-k\fR +.RS 4 +Print information about router key updates +.RE +\fB-p\fR +.RS 4 +Print information about prefix and origin AS updates +.RE +\fB-s\fR +.RS 4 +Print information about connection status updates +.RE +\fB-e\fR +.RS 4 +Export ROAs after completing synchronisation and exit +.RE +\fB-t\fR +.RS 4 +Select template for pfx export. May be a build in template (see \fB-l\fR) or a file path to a custom template (see \fBTEMPLATES\fR) +.RE +\fB-l\fR +.RS 4 +Print available templates and exit. Prints specified templated, when used with -t. +.RE +\fB-o\fR +.RS 4 +Output file for export +.RE +\fB-w\fR +.RS 4 +force ssh authentication information to be interpreted as a password +.RE +\fB-s\fR +.RS 4 +force ssh authentication information to be interpreted as a private key +.SH TEMPLATES +Templates can be used to export ROA information in a custom format. They are written in the \fBmustache\fR(\fIhttps://mustache.github.io/\fR) templating language. + +A template should contain a section called \fBroas\fR which may contain the variables \fBprefix\fR, \fBlength\fR, \fBmaxlen\fR and \fBorigin\fR. +The content of this section is expanded for every entry in the ROA table. The special variable \fBlast\fR is true for the last entry of the prefix table. See the json template for a usage example. +.SH EXAMPLES +Print prefix and origin AS updates from a tcp based server +.PP +.nf +.RS +rtrclient tcp -k rpki.example.com 323 +.RE +.fi +.PP +Print prefix and router key updates from a ssh based server +.PP +.nf +.RS +rtrclient ssh -k -p rpki.example.com 22 rtr-ssh ~/.ssh/id_rsa ~/.ssh/known_hosts +.RE +.fi +.PP +Use multiple rtr server, print prefix updates for some +.PP +.nf +.RS +rtrclient tcp -p rpki.example.com 323 tcp rpki2.example.com 323 ssh -p rpki.example.com 22 rtr-ssh ~/.ssh/id_rsa diff --git a/tools/rtrclient.c b/tools/rtrclient.c new file mode 100644 index 0000000..6282fac --- /dev/null +++ b/tools/rtrclient.c @@ -0,0 +1,1013 @@ +/* + * This file is part of RTRlib. + * + * This file is subject to the terms and conditions of the MIT license. + * See the file LICENSE in the top level directory for more details. + * + * Website: http://rtrlib.realmv6.org/ + */ + +#include "templates.h" + +#include "rtrlib/rtrlib.h" + +#include "third-party/mustach.h" +#include "third-party/tommyds/tommyarray.h" +#include "third-party/tommyds/tommyhashlin.h" + +#include <arpa/inet.h> +#include <ctype.h> +#include <errno.h> +#include <netdb.h> +#include <pthread.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/socket.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <unistd.h> + +#ifdef RTRLIB_HAVE_LIBSSH +// constants for utf8 validation +static const uint8_t UTF8_ONE_BYTE_PREFIX = 0b00000000; +static const uint8_t UTF8_ONE_BYTE_MASK = 0b10000000; +static const uint8_t UTF8_TWO_BYTE_PREFIX = 0b11000000; +static const uint8_t UTF8_TWO_BYTE_MASK = 0b11100000; +static const uint8_t UTF8_THREE_BYTE_PREFIX = 0b11100000; +static const uint8_t UTF8_THREE_BYTE_MASK = 0b11110000; +static const uint8_t UTF8_FOUR_BYTE_PREFIX = 0b11110000; +static const uint8_t UTF8_FOUR_BYTE_MASK = 0b11111000; +static const uint8_t UTF8_SUBSEQUENT_BYTE_PREFIX = 0b10000000; +static const uint8_t UTF8_SUBSEQUENT_BYTE_MASK = 0b11000000; +#endif + +__attribute__((format(printf, 1, 2), noreturn)) static void print_error_exit(const char *fmt, ...); + +static bool is_readable_file(const char *str); + +enum socket_type { + SOCKET_TYPE_TCP, +#ifdef RTRLIB_HAVE_LIBSSH + SOCKET_TYPE_SSH, +#endif +}; + +struct socket_config { + struct rtr_socket socket; + struct tr_socket tr_socket; + enum socket_type type; + bool print_pfx_updates; + bool print_spki_updates; + char *bindaddr; + char *host; + char *port; +#ifdef RTRLIB_HAVE_LIBSSH + char *ssh_username; + char *ssh_private_key; + char *ssh_host_key; + char *ssh_password; + bool force_password; + bool force_key; +#endif +}; + +/* activate update callback */ +bool activate_pfx_update_cb = false; +bool activate_spki_update_cb = false; + +/* print all updates regardless of socket configuration */ +bool print_all_pfx_updates = false; +bool print_all_spki_updates = false; + +bool print_status_updates = false; + +bool export_pfx = false; +char *export_file_path = NULL; +const char *template_name = NULL; + +struct socket_config **socket_config = NULL; +size_t socket_count = 0; + +pthread_mutex_t stdout_mutex = PTHREAD_MUTEX_INITIALIZER; + +/** + * @brief Check if argument is NULL and exit if it is + */ +static void *check_alloc_ret(void *ptr) +{ + if (ptr) + return ptr; + + fprintf(stderr, "Memory allocation error\n"); + exit(-1); +} + +/** + * @brief Return value checking malloc wrapper. Exits program on error + */ +static inline void *checked_malloc(size_t size) +{ + return check_alloc_ret(malloc(size)); +} + +/** + * @brief Return value checking realloc wrapper. Exits program on error. + */ +static inline void *checked_realloc(void *ptr, size_t size) +{ + return check_alloc_ret(realloc(ptr, size)); +} + +static struct socket_config *extend_socket_config() +{ + socket_config = checked_realloc(socket_config, sizeof(struct socket_config *) * (socket_count + 1)); + struct socket_config *config = socket_config[socket_count] = checked_malloc(sizeof(struct socket_config)); + + memset(config, 0, sizeof(*config)); + ++socket_count; + + return config; +} + +static const char *get_template(const char *name) +{ + const struct pfx_output_template *current = templates; + + while (current->name) { + if (strcmp(name, current->name) == 0) + return current->template; + + ++current; + } + + if (is_readable_file(name)) { + FILE *template_file = fopen(name, "r"); + + if (fseek(template_file, 0, SEEK_END) == -1) + goto read_error; + + long file_size = ftell(template_file); + + if (file_size == -1) + goto read_error; + + rewind(template_file); + + char *template = checked_malloc(file_size); + + size_t bytes_read = fread(template, 1, file_size, template_file); + + if (bytes_read != (unsigned long)file_size) + goto read_error; + + fclose(template_file); + return template; + +read_error: + fclose(template_file); + print_error_exit("Could not read template"); + } + + print_error_exit("Template \"%s\" not found", name); +} + +static void print_templates(void) +{ + const struct pfx_output_template *current = templates; + + while (current->name) { + if (template_name && strcmp(current->name, template_name) == 0) + printf("%s", current->template); + else if (!template_name) + printf("%s\n", current->name); + + ++current; + } +} + +static bool is_numeric(const char *str) +{ + for (size_t i = 0; i < strlen(str); ++i) { + if (!isdigit(str[i])) + return false; + } + return true; +} + +static bool is_valid_port_number(const char *str) +{ + if (!is_numeric(str)) + return false; + + int port = atoi(str); + + if (port < 1 || port > 65535) + return false; + + return true; +} + +static bool is_resolveable_host(const char *str) +{ + struct addrinfo hints; + struct addrinfo *result = NULL; + + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_UNSPEC; + hints.ai_socktype = SOCK_STREAM; + hints.ai_flags = 0; + hints.ai_protocol = 0; + + int ret = getaddrinfo(str, NULL, &hints, &result); + + freeaddrinfo(result); + + if (ret != 0) + return false; + return true; +} + +static bool is_file(const char *str) +{ + struct stat stat_buf; + + if (stat(str, &stat_buf) == -1) + return false; + + return S_ISREG(stat_buf.st_mode); +} + +static bool is_readable_file(const char *str) +{ + if (access(str, R_OK) != 0) + return false; + + return is_file(str); +} + +#ifdef RTRLIB_HAVE_LIBSSH +static bool is_utf8(const char *str) +{ + size_t len = strlen(str); + size_t i = 0; + + while (i < len) { + // check if current byte is a single utf8 char + if ((str[i] & UTF8_ONE_BYTE_MASK) == UTF8_ONE_BYTE_PREFIX) { + i += 1; + + // check if current byte is the start of a two byte utf8 char and validate subsequent bytes + } else if ((str[i] & UTF8_TWO_BYTE_MASK) == UTF8_TWO_BYTE_PREFIX && i + 1 < len && + (str[i + 1] & UTF8_SUBSEQUENT_BYTE_MASK) == UTF8_SUBSEQUENT_BYTE_PREFIX) { + i += 2; + + // check if current byte is the start of a three byte utf8 char and validate subsequent bytes + } else if ((str[i] & UTF8_THREE_BYTE_MASK) == UTF8_THREE_BYTE_PREFIX && i + 2 < len && + (str[i + 1] & UTF8_SUBSEQUENT_BYTE_MASK) == UTF8_SUBSEQUENT_BYTE_PREFIX && + (str[i + 2] & UTF8_SUBSEQUENT_BYTE_MASK) == UTF8_SUBSEQUENT_BYTE_PREFIX) { + i += 3; + + // check if current byte is the start of a four byte utf8 char and validate subsequent bytes + } else if ((str[i] & UTF8_FOUR_BYTE_MASK) == UTF8_FOUR_BYTE_PREFIX && i + 3 < len && + (str[i + 1] & UTF8_SUBSEQUENT_BYTE_MASK) == UTF8_SUBSEQUENT_BYTE_PREFIX && + (str[i + 2] & UTF8_SUBSEQUENT_BYTE_MASK) == UTF8_SUBSEQUENT_BYTE_PREFIX && + (str[i + 3] & UTF8_SUBSEQUENT_BYTE_MASK) == UTF8_SUBSEQUENT_BYTE_PREFIX) { + i += 4; + + // if none of the conditions matched. The string contains at least one byte that is not valid utf8 + } else { + return false; + } + } + return true; +} +#endif + +struct exporter_state { + bool roa_section; + tommy_count_t current_roa; + tommy_array *roas; +}; + +struct pfx_export_cb_arg { + tommy_array *array; + tommy_hashlin *hashtable; +}; + +static void pfx_export_cb(const struct pfx_record *pfx_record, void *data); + +static tommy_uint32_t hash_pfx_record(const struct pfx_record *record) +{ + struct pfx_record_packed { + uint8_t ip[16]; + uint32_t asn; + uint8_t min_len; + uint8_t max_len; + } __attribute__((packed)); + + struct pfx_record_packed packed_record; + + memset(&packed_record, 0, sizeof(packed_record)); + + if (record->prefix.ver == LRTR_IPV4) + memcpy(&packed_record.ip, &record->prefix.u.addr4, sizeof(struct lrtr_ipv4_addr)); + else + memcpy(&packed_record.ip, &record->prefix.u.addr6, sizeof(struct lrtr_ipv6_addr)); + + packed_record.asn = record->asn; + packed_record.min_len = record->min_len; + packed_record.max_len = record->max_len; + + return tommy_hash_u32(0, &packed_record, sizeof(packed_record)); +} + +static int pfx_record_cmp(const void *data1, const void *data2) +{ + const struct pfx_record *arg1 = data1; + const struct pfx_record *arg2 = data2; + + if ((arg1->asn == arg2->asn) && (arg1->max_len == arg2->max_len) && (arg1->min_len == arg2->min_len) && + lrtr_ip_addr_equal(arg1->prefix, arg2->prefix)) + return 0; + + return 1; +} + +static int template_enter(void *closure, const char *name) +{ + struct exporter_state *state = closure; + + if (strncmp("roas", name, strlen(name)) == 0) { + state->roa_section = true; + state->current_roa = 0; + + return 1; + + } else if (state->current_roa && strcmp("last", name) == 0 && + state->current_roa == tommy_array_size(state->roas) - 1) { + return 1; + } + + return 0; +} + +static int template_leave(void *closure) +{ + struct exporter_state *state = closure; + + state->roa_section = false; + return 0; +} + +static int template_next(void *closure) +{ + struct exporter_state *state = closure; + + if (++state->current_roa >= tommy_array_size(state->roas)) + return 0; + + return 1; +} + +static int template_put(void *closure, const char *name, __attribute__((unused)) int escape, FILE *file) +{ + struct exporter_state *state = closure; + + size_t namelen = strlen(name); + + if (!state->roa_section || namelen == 0) + return MUSTACH_ERROR_SYSTEM; + + struct pfx_record *pfx_record = tommy_array_get(state->roas, state->current_roa); + + if (strncmp("prefix", name, namelen) == 0) { + char prefix_str[INET6_ADDRSTRLEN]; + + lrtr_ip_addr_to_str(&pfx_record->prefix, prefix_str, sizeof(prefix_str)); + fputs(prefix_str, file); + + } else if (strncmp("length", name, namelen) == 0) { + fprintf(file, "%d", pfx_record->min_len); + + } else if (strncmp("maxlen", name, namelen) == 0) { + fprintf(file, "%d", pfx_record->max_len); + + } else if (strncmp("origin", name, namelen) == 0) { + fprintf(file, "%d", pfx_record->asn); + } + + return MUSTACH_OK; +} + +struct mustach_itf template_itf = {.start = NULL, + .put = template_put, + .enter = template_enter, + .next = template_next, + .leave = template_leave}; + +static void print_usage(char **argv) +{ + printf("Usage:\n"); + printf(" %s [-hpkels] [-o file] [-t template] <socket>...\n", argv[0]); + printf("\nSocket:\n"); + printf(" tcp [-hpkb bindaddr] <host> <port>\n"); +#ifdef RTRLIB_HAVE_LIBSSH + printf(" ssh [-hpkb bindaddr] <host> <port> <username> (<private_key> | <password>) [<host_key>]\n"); +#endif + printf("\nOptions:\n"); + printf("-b bindaddr Hostnamne or IP address to connect from\n\n"); + +#ifdef RTRLIB_HAVE_LIBSSH + printf("-w force ssh authentication information to be interpreted as a password\n"); + printf("-r force ssh authentication information to be interpreted as a private key\n\n"); +#endif + + printf("-k Print information about SPKI updates.\n"); + printf("-p Print information about PFX updates.\n"); + printf("-s Print information about connection status updates.\n\n"); + + printf("-e export pfx table and exit\n"); + printf("-o output file for export\n"); + printf("-t template used for export\n"); + printf("-l list available templates\n\n"); + + printf("-h Print this help message.\n"); + + printf("\nExamples:\n"); + printf(" %s tcp rpki-validator.realmv6.org 8283\n", argv[0]); + printf(" %s tcp -k -p rpki-validator.realmv6.org 8283\n", argv[0]); + printf(" %s tcp -k rpki-validator.realmv6.org 8283 tcp -s example.com 323\n", argv[0]); + printf(" %s -kp tcp rpki-validator.realmv6.org 8283 tcp example.com 323\n", argv[0]); +#ifdef RTRLIB_HAVE_LIBSSH + printf(" %s ssh rpki-validator.realmv6.org 22 rtr-ssh", argv[0]); + printf(" ~/.ssh/id_rsa ~/.ssh/known_hosts\n"); + printf(" %s ssh -k -p rpki-validator.realmv6.org 22 rtr-ssh ~/.ssh/id_rsa ~/.ssh/known_hosts\n", argv[0]); + printf(" %s ssh -k -p rpki-validator.realmv6.org 22 rtr-ssh ~/.ssh/id_rsa ~/.ssh/known_hosts", argv[0]); + printf(" ssh -k -p example.com 22 rtr-ssh ~/.ssh/id_rsa_example\n"); + printf(" %s ssh -k -p rpki-validator.realmv6.org 22 rtr-ssh ~/.ssh/id_rsa ~/.ssh/known_hosts", argv[0]); + printf(" tcp -k -p example.com 323\n"); +#endif + + printf(" %s -e tcp rpki-validator.realmv6.org 8283\n", argv[0]); + printf(" %s -e -t csv -o roa.csv tcp rpki-validator.realmv6.org 8283\n", argv[0]); +} + +static void status_fp(const struct rtr_mgr_group *group __attribute__((unused)), enum rtr_mgr_status mgr_status, + const struct rtr_socket *rtr_sock, void *data __attribute__((unused))) +{ + if (print_status_updates) { + pthread_mutex_lock(&stdout_mutex); + printf("RTR-Socket changed connection status to: %s, Mgr Status: %s\n", + rtr_state_to_str(rtr_sock->state), rtr_mgr_status_to_str(mgr_status)); + pthread_mutex_unlock(&stdout_mutex); + } +} + +static void update_cb(struct pfx_table *p __attribute__((unused)), const struct pfx_record rec, const bool added) +{ + char ip[INET6_ADDRSTRLEN]; + + const struct socket_config *config = (const struct socket_config *)rec.socket; + + if (!print_all_pfx_updates && !config->print_pfx_updates) + return; + + pthread_mutex_lock(&stdout_mutex); + if (added) + printf("+ "); + else + printf("- "); + lrtr_ip_addr_to_str(&rec.prefix, ip, sizeof(ip)); + if (socket_count > 1) + printf("%s:%s %-40s %3u - %3u %10u\n", config->host, config->port, ip, rec.min_len, rec.max_len, + rec.asn); + else + printf("%-40s %3u - %3u %10u\n", ip, rec.min_len, rec.max_len, rec.asn); + + pthread_mutex_unlock(&stdout_mutex); +} + +static void update_spki(struct spki_table *s __attribute__((unused)), const struct spki_record record, const bool added) +{ + const struct socket_config *config = (const struct socket_config *)record.socket; + + if (!print_all_spki_updates && !config->print_spki_updates) + return; + + pthread_mutex_lock(&stdout_mutex); + + char c; + + if (added) + c = '+'; + else + c = '-'; + + printf("%c ", c); + printf("HOST: %s:%s\n", config->host, config->port); + printf("ASN: %u\n ", record.asn); + + int i; + int size = sizeof(record.ski); + + printf("SKI: "); + for (i = 0; i < size; i++) { + printf("%02x", record.ski[i]); + if (i < size - 1) + printf(":"); + } + printf("\n "); + + i = 0; + size = sizeof(record.spki); + printf("SPKI: "); + for (i = 0; i < size; i++) { + if ((i % 40 == 0) && (i != 0)) + printf("\n "); + + printf("%02x", record.spki[i]); + if (i < size - 1) + printf(":"); + } + printf("\n"); + + pthread_mutex_unlock(&stdout_mutex); +} + +static void parse_global_opts(int argc, char **argv) +{ + int opt; + + bool print_template = false; + + while ((opt = getopt(argc, argv, "+kphelo:t:s")) != -1) { + switch (opt) { + case 'k': + activate_spki_update_cb = true; + print_all_spki_updates = true; + break; + + case 'p': + activate_pfx_update_cb = true; + print_all_pfx_updates = true; + break; + + case 'e': + export_pfx = true; + break; + + case 'o': + if (export_file_path) + print_error_exit("output file can not be specified more than once"); + + export_file_path = optarg; + break; + + case 't': + template_name = optarg; + break; + + case 'l': + print_template = true; + break; + + case 's': + print_status_updates = true; + break; + + default: + print_usage(argv); + exit(EXIT_FAILURE); + } + } + + if (print_template) { + print_templates(); + exit(EXIT_SUCCESS); + } + + if ((export_file_path || template_name) && !export_pfx) + print_error_exit("Specifying -o or -t without -e does not make sense"); +} + +static void parse_socket_opts(int argc, char **argv, struct socket_config *config) +{ + int opt; + + while ((opt = getopt(argc, argv, "+kphwrb:")) != -1) { + switch (opt) { + case 'k': + activate_spki_update_cb = true; + config->print_spki_updates = true; + break; + + case 'p': + activate_pfx_update_cb = true; + config->print_pfx_updates = true; + break; + + case 'b': + config->bindaddr = optarg; + break; + +#ifdef RTRLIB_HAVE_LIBSSH + case 'w': + if (config->force_key) + print_error_exit("-w and -r are mutually exclusive"); + + config->force_password = true; + break; + + case 'r': + if (config->force_password) + print_error_exit("-w and -r are mutually exclusive"); + + config->force_key = true; + break; +#endif + + default: + print_usage(argv); + exit(EXIT_FAILURE); + } + } +} + +static void print_error_exit(const char *fmt, ...) +{ + va_list argptr; + + va_start(argptr, fmt); + + fprintf(stderr, "error: "); + + vfprintf(stderr, fmt, argptr); + + fprintf(stderr, "\n"); + + va_end(argptr); + + exit(EXIT_FAILURE); +} + +static int parse_cli(int argc, char **argv) +{ + enum cli_parse_state { + CLI_PARSE_STATE_BEGIN, + CLI_PARSE_STATE_GLOBAL_OPTS, + CLI_PARSE_STATE_SOCKET_BEGIN, + CLI_PARSE_STATE_SOCKET_TCP_OPTIONS, + CLI_PARSE_STATE_SOCKET_TCP_HOST, + CLI_PARSE_STATE_SOCKET_TCP_PORT, +#ifdef RTRLIB_HAVE_LIBSSH + CLI_PARSE_STATE_SOCKET_SSH_OPTIONS, + CLI_PARSE_STATE_SOCKET_SSH_HOST, + CLI_PARSE_STATE_SOCKET_SSH_PORT, + CLI_PARSE_STATE_SOCKET_SSH_USERNAME, + CLI_PARSE_STATE_SOCKET_SSH_PRIVATE_KEY_PASSWORD, + CLI_PARSE_STATE_SOCKET_SSH_HOST_KEY, +#endif + CLI_PARSE_STATE_END, + }; + + enum cli_parse_state state = CLI_PARSE_STATE_BEGIN; + + struct socket_config *current_config; + + while (true) { + switch (state) { + case CLI_PARSE_STATE_BEGIN: + optind = 1; + state = CLI_PARSE_STATE_GLOBAL_OPTS; + break; + + case CLI_PARSE_STATE_GLOBAL_OPTS: + parse_global_opts(argc, argv); + + state = CLI_PARSE_STATE_SOCKET_BEGIN; + break; + + case CLI_PARSE_STATE_SOCKET_BEGIN: + current_config = extend_socket_config(); + + if (strncasecmp(argv[optind], "tcp", strlen(argv[optind])) == 0) { + state = CLI_PARSE_STATE_SOCKET_TCP_OPTIONS; + current_config->type = SOCKET_TYPE_TCP; + + /* Check if enough arguments are available + * to configure this socket. + * This does not account for options. + */ + if ((argc - optind) < 2) + print_error_exit("Not enough arguments for tcp socket"); + + } else if (strncasecmp(argv[optind], "ssh", strlen(argv[optind])) == 0) { +#ifdef RTRLIB_HAVE_LIBSSH + state = CLI_PARSE_STATE_SOCKET_SSH_OPTIONS; + current_config->type = SOCKET_TYPE_SSH; + + /* Check if enough arguments are available + * to configure this socket. + * This does not account for options. + */ + if ((argc - optind) < 4) + print_error_exit("Not enough arguments for ssh socket"); +#else + print_error_exit("ssh support disabled at compile time\n"); +#endif + + } else { + print_error_exit("\"%s\" is not a valid socket type\n", argv[optind]); + break; + } + + ++optind; + break; + + case CLI_PARSE_STATE_SOCKET_TCP_OPTIONS: + parse_socket_opts(argc, argv, current_config); + + /* Check again if enough arguments are available, + * accounting for options + */ + if ((argc - optind) < 2) + print_error_exit("Not enough arguments for tcp socket"); + + state = CLI_PARSE_STATE_SOCKET_TCP_HOST; + break; + + case CLI_PARSE_STATE_SOCKET_TCP_HOST: + if (!is_resolveable_host(argv[optind])) + print_error_exit("cannot resolve \"%s\"\n", argv[optind]); + + current_config->host = argv[optind++]; + + state = CLI_PARSE_STATE_SOCKET_TCP_PORT; + break; + + case CLI_PARSE_STATE_SOCKET_TCP_PORT: + if (!is_valid_port_number(argv[optind])) + print_error_exit("\"%s\" is not a valid port number\n", argv[optind]); + + current_config->port = argv[optind++]; + + state = CLI_PARSE_STATE_SOCKET_BEGIN; + break; + +#ifdef RTRLIB_HAVE_LIBSSH + case CLI_PARSE_STATE_SOCKET_SSH_OPTIONS: + parse_socket_opts(argc, argv, current_config); + + /* Check again if enough arguments are available, + * accounting for options + */ + if ((argc - optind) < 4) + print_error_exit("Not enough arguments for ssh socket"); + + state = CLI_PARSE_STATE_SOCKET_SSH_HOST; + break; + + case CLI_PARSE_STATE_SOCKET_SSH_HOST: + if (!is_resolveable_host(argv[optind])) + print_error_exit("cannot resolve \"%s\"\n", argv[optind]); + + current_config->host = argv[optind++]; + + state = CLI_PARSE_STATE_SOCKET_SSH_PORT; + break; + + case CLI_PARSE_STATE_SOCKET_SSH_PORT: + if (!is_valid_port_number(argv[optind])) + print_error_exit("\"%s\" is not a valid port number\n", argv[optind]); + + current_config->port = argv[optind++]; + + state = CLI_PARSE_STATE_SOCKET_SSH_USERNAME; + break; + + case CLI_PARSE_STATE_SOCKET_SSH_USERNAME: + current_config->ssh_username = argv[optind++]; + + state = CLI_PARSE_STATE_SOCKET_SSH_PRIVATE_KEY_PASSWORD; + break; + + case CLI_PARSE_STATE_SOCKET_SSH_PRIVATE_KEY_PASSWORD: + if (current_config->force_key && !is_readable_file(argv[optind])) { + print_error_exit("\"%s\" is not a readable file\n", argv[optind]); + + } else if (!current_config->force_password && !current_config->force_key && + is_readable_file(argv[optind])) { + current_config->ssh_private_key = argv[optind]; + + } else if (!current_config->force_password && is_utf8(argv[optind])) { + fprintf(stderr, "\"%s\" does not seem to be a file. Trying password authentication.\n", + argv[optind]); + fprintf(stderr, "Use -r to force key authentication or -w to silence this warning"); + current_config->ssh_password = argv[optind]; + + } else if (current_config->force_password && !is_utf8(argv[optind])) { + print_error_exit("\"%s\" is not a valid utf8 string", argv[optind]); + + } else if (current_config->force_password && !current_config->force_key && + is_utf8(argv[optind])) { + current_config->ssh_password = argv[optind]; + + } else { + print_error_exit("\"%s\" is neither a readable file nor a valid utf8 string", + argv[optind]); + } + + ++optind; + state = CLI_PARSE_STATE_SOCKET_SSH_HOST_KEY; + break; + + case CLI_PARSE_STATE_SOCKET_SSH_HOST_KEY: + if (strncasecmp(argv[optind], "tcp", strlen(argv[optind])) == 0 || + strncasecmp(argv[optind], "ssh", strlen(argv[optind])) == 0) { + state = CLI_PARSE_STATE_SOCKET_BEGIN; + break; + } + + if (!is_readable_file(argv[optind])) + print_error_exit("\"%s\" is not a readable file\n", argv[optind]); + + current_config->ssh_host_key = argv[optind++]; + + state = CLI_PARSE_STATE_SOCKET_BEGIN; + break; +#endif + + case CLI_PARSE_STATE_END: + return 0; + } + + if (optind >= argc) + state = CLI_PARSE_STATE_END; + } +} + +static void init_sockets(void) +{ + for (size_t i = 0; i < socket_count; ++i) { + struct socket_config *config = socket_config[i]; + struct tr_tcp_config tcp_config = {}; +#ifdef RTRLIB_HAVE_LIBSSH + struct tr_ssh_config ssh_config = {}; +#endif + + switch (config->type) { + case SOCKET_TYPE_TCP: + tcp_config.host = config->host; + tcp_config.port = config->port; + tcp_config.bindaddr = config->bindaddr; + + tr_tcp_init(&tcp_config, &config->tr_socket); + config->socket.tr_socket = &config->tr_socket; + break; + +#ifdef RTRLIB_HAVE_LIBSSH + case SOCKET_TYPE_SSH: + ssh_config.host = config->host; + ssh_config.port = atoi(config->port); + ssh_config.bindaddr = config->bindaddr; + ssh_config.username = config->ssh_username; + ssh_config.client_privkey_path = config->ssh_private_key; + ssh_config.server_hostkey_path = config->ssh_host_key; + ssh_config.password = config->ssh_password; + + tr_ssh_init(&ssh_config, &config->tr_socket); + config->socket.tr_socket = &config->tr_socket; + break; +#endif + } + } +} + +int main(int argc, char **argv) +{ + parse_cli(argc, argv); + + if (socket_count == 0) { + print_usage(argv); + exit(EXIT_FAILURE); + } + + init_sockets(); + + struct rtr_mgr_config *conf; + struct rtr_mgr_group groups[1]; + + groups[0].sockets_len = socket_count; + groups[0].sockets = (struct rtr_socket **)socket_config; + groups[0].preference = 1; + + spki_update_fp spki_update_fp = activate_spki_update_cb ? update_spki : NULL; + pfx_update_fp pfx_update_fp = activate_pfx_update_cb ? update_cb : NULL; + + int ret = rtr_mgr_init(&conf, groups, 1, 30, 600, 600, pfx_update_fp, spki_update_fp, status_fp, NULL); + + if (ret == RTR_ERROR) + fprintf(stderr, "Error in rtr_mgr_init!\n"); + else if (ret == RTR_INVALID_PARAM) + fprintf(stderr, "Invalid params passed to rtr_mgr_init\n"); + + if (!conf) + return EXIT_FAILURE; + + if (!export_pfx && activate_pfx_update_cb && socket_count > 1) + printf("%-40s %-40s %3s %3s %3s\n", "host", "Prefix", "Prefix Length", "", "ASN"); + else if (!export_pfx && activate_pfx_update_cb) + printf("%-40s %3s %3s %3s\n", "Prefix", "Prefix Length", "", "ASN"); + + if (export_pfx) { + const char *template; + + if (template_name) + template = get_template(template_name); + else + template = templates->template; + FILE *export_file = stdout; + + if (export_file_path) { + export_file = fopen(export_file_path, "w"); + if (!export_file) { + char *errormsg = strerror(errno); + + print_error_exit("\"%s\" could not be opened for writing: %s", optarg, errormsg); + } + } + + rtr_mgr_start(conf); + + while (!rtr_mgr_conf_in_sync(conf)) + sleep(1); + + printf("Sync done\n"); + tommy_array prefixes; + tommy_hashlin prefix_hash; + struct pfx_export_cb_arg arg = { + .array = &prefixes, + .hashtable = &prefix_hash, + }; + + tommy_array_init(&prefixes); + tommy_hashlin_init(&prefix_hash); + + pfx_table_for_each_ipv4_record(conf->pfx_table, pfx_export_cb, &arg); + pfx_table_for_each_ipv6_record(conf->pfx_table, pfx_export_cb, &arg); + + struct exporter_state state = { + .roa_section = false, + .current_roa = 0, + .roas = &prefixes, + }; + + fmustach(template, &template_itf, &state, export_file); + + tommy_hashlin_foreach(&prefix_hash, free); + tommy_hashlin_done(&prefix_hash); + tommy_array_done(&prefixes); + + if (export_file != stdout && fclose(export_file) == EOF) { + char *errormsg = strerror(errno); + + print_error_exit("\"Error during write into output file: %s\"", errormsg); + } + + } else { + rtr_mgr_start(conf); + pause(); + } + + rtr_mgr_stop(conf); + rtr_mgr_free(conf); + free(groups[0].sockets); + + return EXIT_SUCCESS; +} + +struct pfx_record_entry { + struct pfx_record record; + tommy_node hash_node; +}; + +static void pfx_export_cb(const struct pfx_record *pfx_record, void *data) +{ + struct pfx_export_cb_arg *arg = data; + tommy_array *roa_array = arg->array; + tommy_hashlin *roa_hashtable = arg->hashtable; + + tommy_hash_t record_hash = hash_pfx_record(pfx_record); + + if (tommy_hashlin_search(roa_hashtable, &pfx_record_cmp, pfx_record, record_hash) != 0) + return; + + struct pfx_record_entry *pfx_record_entry = malloc(sizeof(struct pfx_record_entry)); + + memcpy(&pfx_record_entry->record, pfx_record, sizeof(struct pfx_record)); + + tommy_hashlin_insert(roa_hashtable, &pfx_record_entry->hash_node, pfx_record_entry, record_hash); + tommy_array_insert(roa_array, pfx_record_entry); +} diff --git a/tools/templates.h.cmake b/tools/templates.h.cmake new file mode 100644 index 0000000..338c536 --- /dev/null +++ b/tools/templates.h.cmake @@ -0,0 +1,22 @@ +/* + * This file is part of RTRlib. + * + * This file is subject to the terms and conditions of the MIT license. + * See the file LICENSE in the top level directory for more details. + * + * Website: http://rtrlib.realmv6.org/ + */ + + #include <stddef.h> + + +struct pfx_output_template { + const char *name; + const char *template; +}; + + +const struct pfx_output_template templates[] = { +${TEMPLATES} +{NULL, NULL} +}; diff --git a/tools/templates/csv b/tools/templates/csv new file mode 100644 index 0000000..3b08945 --- /dev/null +++ b/tools/templates/csv @@ -0,0 +1,3 @@ +{{#roas}} +{{prefix}}, {{length}}, {{maxlen}}, {{origin}} +{{/roas}} diff --git a/tools/templates/csvwithheader b/tools/templates/csvwithheader new file mode 100644 index 0000000..01a92bc --- /dev/null +++ b/tools/templates/csvwithheader @@ -0,0 +1,4 @@ +prefix, minlen, maxlen, asn +{{#roas}} +{{prefix}}, {{length}}, {{maxlen}}, {{origin}} +{{/roas}} diff --git a/tools/templates/default b/tools/templates/default new file mode 100644 index 0000000..6fa86df --- /dev/null +++ b/tools/templates/default @@ -0,0 +1,3 @@ +{{#roas}} +{{prefix}}/{{length}}-{{maxlen}} AS {{origin}} +{{/roas}} diff --git a/tools/templates/json b/tools/templates/json new file mode 100644 index 0000000..8b7adb9 --- /dev/null +++ b/tools/templates/json @@ -0,0 +1,10 @@ +[ +{{#roas}} + { + "prefix": "{{prefix}}", + "length": "{{length}}", + "maxlen": "{{maxlen}}", + "origin": "{{origin}}" + }{{^last}},{{/last}} +{{/roas}} +] |