diff --git a/configure.ac b/configure.ac index d98de89..44a040c 100644 --- a/configure.ac +++ b/configure.ac @@ -42,6 +42,7 @@ PKG_CHECK_MODULES(LIBOSMOCORE, libosmocore >= 0.11.0) PKG_CHECK_MODULES(LIBOSMOVTY, libosmovty >= 0.11.0) PKG_CHECK_MODULES(LIBOSMOCTRL, libosmoctrl >= 0.11.0) PKG_CHECK_MODULES(LIBOSMOGSM, libosmogsm >= 0.11.0) +PKG_CHECK_MODULES(LIBOSMONETIF, libosmo-netif >= 0.4.0) PKG_CHECK_MODULES(LIBMNL, libmnl) dnl FIXME: bump to 1.10.0 once it's available on build slaves and remove workaround from osysmon_ping.c PKG_CHECK_MODULES(LIBOPING, liboping >= 1.9.0) diff --git a/contrib/jenkins.sh b/contrib/jenkins.sh index b1529c4..631f95a 100755 --- a/contrib/jenkins.sh +++ b/contrib/jenkins.sh @@ -25,6 +25,9 @@ verify_value_string_arrays_are_terminated.py $(find . -name "*.[hc]") export PKG_CONFIG_PATH="$inst/lib/pkgconfig:$PKG_CONFIG_PATH" export LD_LIBRARY_PATH="$inst/lib" +osmo-build-dep.sh libosmo-abis +osmo-build-dep.sh libosmo-netif "" '--disable-doxygen' + set +x echo echo diff --git a/src/Makefile.am b/src/Makefile.am index f639023..f9b79f2 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -9,6 +9,7 @@ AM_CFLAGS = \ -Wall \ $(LIBOSMOCORE_CFLAGS) \ $(LIBOSMOGSM_CFLAGS) \ + $(LIBOSMONETIF_CFLAGS) \ $(NULL) AM_LDFLAGS = \ @@ -22,12 +23,13 @@ bin_PROGRAMS = \ noinst_LTLIBRARIES = libintern.la libintern_la_SOURCES = simple_ctrl.c client.c -libintern_la_LIBADD = $(LIBOSMOCORE_LIBS) $(LIBOSMOGSM_LIBS) +libintern_la_LIBADD = $(LIBOSMOCORE_LIBS) $(LIBOSMOGSM_LIBS) $(LIBOSMONETIF_LIBS) osmo_sysmon_CFLAGS = $(LIBMNL_CFLAGS) $(LIBOSMOVTY_CFLAGS) $(LIBOPING_CFLAGS) $(AM_CFLAGS) osmo_sysmon_LDADD = $(LDADD) \ $(LIBOSMOVTY_LIBS) \ + $(LIBOSMONETIF_LIBS) \ $(LIBMNL_LIBS) \ $(LIBOPING_LIBS) \ $(NULL) @@ -39,6 +41,7 @@ osmo_sysmon_SOURCES = \ osysmon_rtnl.c \ osysmon_file.c \ osysmon_ping.c \ + osysmon_openvpn.c \ osysmon_main.c \ $(NULL) diff --git a/src/client.c b/src/client.c index 6b37fc6..758884d 100644 --- a/src/client.c +++ b/src/client.c @@ -27,6 +27,7 @@ #include #include +#include #include "client.h" @@ -71,3 +72,24 @@ char *make_authority(void *ctx, const struct host_cfg *cfg) return talloc_asprintf(ctx, "%s:%u", cfg->remote_host, cfg->remote_port); } + +struct osmo_stream_cli *make_tcp_client(struct host_cfg *cfg) +{ + struct osmo_stream_cli *cl = osmo_stream_cli_create(cfg); + if (cl) { + osmo_stream_cli_set_addr(cl, cfg->remote_host); + osmo_stream_cli_set_port(cl, cfg->remote_port); + } + + return cl; +} + +void update_name(struct host_cfg *cfg, const char *new_name) +{ + osmo_talloc_replace_string(cfg, (char **)&cfg->name, new_name); +} + +void update_host(struct host_cfg *cfg, const char *new_host) +{ + osmo_talloc_replace_string(cfg, (char **)&cfg->remote_host, new_host); +} diff --git a/src/client.h b/src/client.h index 605ddd7..d878450 100644 --- a/src/client.h +++ b/src/client.h @@ -23,3 +23,8 @@ struct host_cfg { struct host_cfg *host_cfg_alloc(void *ctx, const char *name, const char *host, uint16_t port); bool match_config(const struct host_cfg *cfg, const char *match, enum match_kind k); char *make_authority(void *ctx, const struct host_cfg *cfg); + +struct osmo_stream_cli *make_tcp_client(struct host_cfg *cfg); + +void update_name(struct host_cfg *cfg, const char *new_name); +void update_host(struct host_cfg *cfg, const char *new_host); diff --git a/src/osysmon.h b/src/osysmon.h index df8bf8d..2f82c47 100644 --- a/src/osysmon.h +++ b/src/osysmon.h @@ -15,6 +15,8 @@ struct osysmon_state { struct rtnl_client_state *rcs; /* list of 'struct ctrl client' */ struct llist_head ctrl_clients; + /* list of 'struct openvpn_client' */ + struct llist_head openvpn_clients; /* list of 'struct netdev' */ struct llist_head netdevs; /* list of 'struct osysmon_file' */ @@ -30,6 +32,7 @@ enum osysmon_vty_node { CTRL_CLIENT_NODE = _LAST_OSMOVTY_NODE + 1, CTRL_CLIENT_GETVAR_NODE, NETDEV_NODE, + OPENVPN_NODE, PING_NODE, }; @@ -48,5 +51,8 @@ int osysmon_sysinfo_poll(struct value_node *parent); int osysmon_ping_init(); int osysmon_ping_poll(struct value_node *parent); +int osysmon_openvpn_init(); +int osysmon_openvpn_poll(struct value_node *parent); + int osysmon_file_init(); int osysmon_file_poll(struct value_node *parent); diff --git a/src/osysmon_main.c b/src/osysmon_main.c index 91d5039..18f6299 100644 --- a/src/osysmon_main.c +++ b/src/osysmon_main.c @@ -199,6 +199,7 @@ int main(int argc, char **argv) g_oss = talloc_zero(NULL, struct osysmon_state); INIT_LLIST_HEAD(&g_oss->ctrl_clients); + INIT_LLIST_HEAD(&g_oss->openvpn_clients); INIT_LLIST_HEAD(&g_oss->netdevs); INIT_LLIST_HEAD(&g_oss->files); @@ -206,6 +207,7 @@ int main(int argc, char **argv) handle_options(argc, argv); osysmon_sysinfo_init(); osysmon_ctrl_init(); + osysmon_openvpn_init(); osysmon_rtnl_init(); ping_init = osysmon_ping_init(); osysmon_file_init(); @@ -231,6 +233,7 @@ int main(int argc, char **argv) while (1) { struct value_node *root = value_node_add(NULL, "root", NULL); + int vpns = osysmon_openvpn_poll(root); osysmon_sysinfo_poll(root); osysmon_ctrl_poll(root); osysmon_rtnl_poll(root); @@ -242,6 +245,10 @@ int main(int argc, char **argv) display_update(root); value_node_del(root); + + if (vpns) + osmo_select_main(0); + sleep(1); } diff --git a/src/osysmon_openvpn.c b/src/osysmon_openvpn.c new file mode 100644 index 0000000..135a532 --- /dev/null +++ b/src/osysmon_openvpn.c @@ -0,0 +1,294 @@ +/* Simple Osmocom System Monitor (osysmon): Support for OpenVPN monitoring */ + +/* (C) 2019 by sysmocom - s.f.m.c. GmbH. + * Author: Max Suraev + * All Rights Reserved. + * + * SPDX-License-Identifier: GPL-2.0+ + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, + * MA 02110-1301, USA. + */ + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "osysmon.h" +#include "client.h" +#include "value_node.h" + +/*********************************************************************** + * Data model + ***********************************************************************/ + +#define OVPN_LOG(ctx, vpn, fmt, args...) \ + fprintf(stderr, "OpenVPN [%s]: " fmt, make_authority(ctx, vpn->cfg), ##args) + +/* max number of csv in response */ +#define MAX_RESP_COMPONENTS 6 + +/* a single OpenVPN management interface client */ +struct openvpn_client { + /* links to osysmon.openvpn_clients */ + struct llist_head list; + struct host_cfg *cfg; + struct osmo_stream_cli *mgmt; + /* fields below are parsed from response to 'state' command on mgmt interface */ + struct host_cfg *rem_cfg; + char *tun_ip; + bool connected; +}; + +static char *parse_state(struct msgb *msg, struct openvpn_client *vpn) +{ + char tmp[128]; + char *tok; + unsigned int i = 0; + uint8_t *m = msgb_data(msg); + + if (msgb_length(msg) > 128) + OVPN_LOG(msg, vpn, "received message too long (%d > %u), truncating...\n", msgb_length(msg), 128); + + if (msgb_length(msg) > 0) { + if (!isdigit(m[0])) /* skip OpenVPN greetings and alike */ + return NULL; + } else { + OVPN_LOG(msg, vpn, "received message is empty, ignoring...\n"); + return NULL; + } + + OSMO_STRLCPY_ARRAY(tmp, (char *)m); + + for (tok = strtok(tmp, ","); tok && i < MAX_RESP_COMPONENTS; tok = strtok(NULL, ",")) { + /* The string format is documented in https://openvpn.net/community-resources/management-interface/ */ + if (tok) { /* Parse csv string and pick interesting tokens while ignoring the rest. */ + switch (i++) { + case 1: + update_name(vpn->rem_cfg, tok); + break; + case 3: + osmo_talloc_replace_string(vpn->rem_cfg, &vpn->tun_ip, tok); + break; + case 4: + update_host(vpn->rem_cfg, tok); + break; + case 5: + vpn->rem_cfg->remote_port = atoi(tok); + break; + } + } + } + return NULL; +} + +static struct openvpn_client *openvpn_client_find_or_make(const struct osysmon_state *os, + const char *host, uint16_t port) +{ + struct openvpn_client *vpn; + llist_for_each_entry(vpn, &os->openvpn_clients, list) { + if (match_config(vpn->cfg, host, MATCH_HOST) && vpn->cfg->remote_port == port) + return vpn; + } + + return NULL; +} + +static int connect_cb(struct osmo_stream_cli *conn) +{ + struct openvpn_client *vpn = osmo_stream_cli_get_data(conn); + + update_name(vpn->rem_cfg, "management interface incompatible"); + vpn->connected = true; /* FIXME: there's no callback for lost connection to drop this flag */ + + return 0; +} + +static int read_cb(struct osmo_stream_cli *conn) +{ + int bytes; + struct openvpn_client *vpn = osmo_stream_cli_get_data(conn); + struct msgb *msg = msgb_alloc(1024, "OpenVPN"); + if (!msg) { + OVPN_LOG(conn, vpn, "unable to allocate message in callback\n"); + return 0; + } + + bytes = osmo_stream_cli_recv(conn, msg); + if (bytes < 0) + OVPN_LOG(msg, vpn, "unable to receive message in callback\n"); + else + parse_state(msg, vpn); + + msgb_free(msg); + + return 0; +} + +static bool openvpn_client_create(struct osysmon_state *os, const char *name, const char *host, uint16_t port) +{ + struct openvpn_client *vpn = openvpn_client_find_or_make(os, host, port); + if (vpn) + return true; + + vpn = talloc_zero(os, struct openvpn_client); + if (!vpn) + return false; + + vpn->connected = false; + + vpn->cfg = host_cfg_alloc(vpn, name, host, port); + if (!vpn->cfg) + goto dealloc; + + vpn->rem_cfg = host_cfg_alloc(vpn, "management interface unavailable", NULL, 0); + if (!vpn->rem_cfg) + goto dealloc; + + vpn->mgmt = make_tcp_client(vpn->cfg); + if (!vpn->mgmt) { + OVPN_LOG(vpn->rem_cfg, vpn, "failed to create TCP client\n"); + goto dealloc; + } + + /* Wait for 1 minute before attempting to reconnect to management interface */ + osmo_stream_cli_set_reconnect_timeout(vpn->mgmt, 60); + osmo_stream_cli_set_read_cb(vpn->mgmt, read_cb); + osmo_stream_cli_set_connect_cb(vpn->mgmt, connect_cb); + + if (osmo_stream_cli_open(vpn->mgmt) < 0) { + OVPN_LOG(vpn->rem_cfg, vpn, "failed to connect to management interface\n"); + goto dealloc; + } + + osmo_stream_cli_set_data(vpn->mgmt, vpn); + llist_add_tail(&vpn->list, &os->openvpn_clients); + + return true; + +dealloc: + talloc_free(vpn); + return false; +} + +static void openvpn_client_destroy(struct openvpn_client *vpn) +{ + if (!vpn) + return; + + osmo_stream_cli_destroy(vpn->mgmt); + llist_del(&vpn->list); + talloc_free(vpn); +} + + +/*********************************************************************** + * VTY + ***********************************************************************/ + +#define OPENVPN_STR "Configure OpenVPN management interface address\n" + +DEFUN(cfg_openvpn, cfg_openvpn_cmd, + "openvpn HOST <1-65535>", + OPENVPN_STR "Name of the host to connect to\n" "Management interface port\n") +{ + uint16_t port = atoi(argv[1]); + + if (!openvpn_client_create(g_oss, "OpenVPN", argv[0], port)) { + vty_out(vty, "Failed to create OpenVPN client for %s:%u%s", argv[0], port, VTY_NEWLINE); + return CMD_WARNING; + } + + return CMD_SUCCESS; +} + +DEFUN(cfg_no_openvpn, cfg_no_openvpn_cmd, + "no openvpn HOST <1-65535>", + NO_STR OPENVPN_STR "Name of the host to connect to\n" "Management interface port\n") +{ + uint16_t port = atoi(argv[1]); + struct openvpn_client *vpn = openvpn_client_find_or_make(g_oss, argv[0], port); + if (!vpn) { + vty_out(vty, "OpenVPN client %s:%u doesn't exist%s", argv[0], port, VTY_NEWLINE); + return CMD_WARNING; + } + + openvpn_client_destroy(vpn); + + return CMD_SUCCESS; +} + + +/*********************************************************************** + * Runtime Code + ***********************************************************************/ + +static int openvpn_client_poll(struct openvpn_client *vpn, struct value_node *parent) +{ + char *remote = make_authority(parent, vpn->rem_cfg); + struct value_node *vn_host = value_node_find_or_add(parent, make_authority(parent, vpn->cfg)); + struct msgb *msg = msgb_alloc(128, "state"); + if (!msg) { + value_node_add(vn_host, "msgb", "memory allocation failure"); + return 0; + } + + if (vpn->rem_cfg->name) + value_node_add(vn_host, "status", vpn->rem_cfg->name); + + if (vpn->tun_ip) + value_node_add(vn_host, "tunnel", vpn->tun_ip); + + if (remote) + value_node_add(vn_host, "remote", remote); + + /* FIXME: there's no way to check client state so this might be triggered even while it's reconnecting */ + if (vpn->connected) { /* re-trigger state command */ + msgb_printf(msg, "state\n"); + osmo_stream_cli_send(vpn->mgmt, msg); + } + + return 0; +} + +/* called once on startup before config file parsing */ +int osysmon_openvpn_init() +{ + install_element(CONFIG_NODE, &cfg_openvpn_cmd); + install_element(CONFIG_NODE, &cfg_no_openvpn_cmd); + + return 0; +} + +/* called periodically */ +int osysmon_openvpn_poll(struct value_node *parent) +{ + int num_vpns = llist_count(&g_oss->openvpn_clients); + if (num_vpns) { + struct value_node *vn_vpn = value_node_add(parent, "OpenVPN", NULL); + struct openvpn_client *vpn; + llist_for_each_entry(vpn, &g_oss->openvpn_clients, list) + openvpn_client_poll(vpn, vn_vpn); + } + + return num_vpns; +}