blob: cc9694f12bba3642199840be5d227650381f890b [file] [log] [blame]
Max9a852f22019-01-31 15:58:57 +01001/* Simple Osmocom System Monitor (osysmon): Support for OpenVPN monitoring */
2
3/* (C) 2019 by sysmocom - s.f.m.c. GmbH.
4 * Author: Max Suraev
5 * All Rights Reserved.
6 *
7 * SPDX-License-Identifier: GPL-2.0+
8 *
9 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
Max9a852f22019-01-31 15:58:57 +010018 */
19
20#include <string.h>
21#include <stdbool.h>
22#include <ctype.h>
23#include <errno.h>
24
25#include <osmocom/core/utils.h>
26#include <osmocom/vty/vty.h>
27#include <osmocom/vty/command.h>
28#include <osmocom/core/msgb.h>
29#include <osmocom/netif/stream.h>
30
31#include "osysmon.h"
32#include "client.h"
33#include "value_node.h"
34
35/***********************************************************************
36 * Data model
37 ***********************************************************************/
38
39#define OVPN_LOG(ctx, vpn, fmt, args...) \
40 fprintf(stderr, "OpenVPN [%s]: " fmt, make_authority(ctx, vpn->cfg), ##args)
41
42/* max number of csv in response */
43#define MAX_RESP_COMPONENTS 6
44
45/* a single OpenVPN management interface client */
46struct openvpn_client {
47 /* links to osysmon.openvpn_clients */
48 struct llist_head list;
49 struct host_cfg *cfg;
50 struct osmo_stream_cli *mgmt;
51 /* fields below are parsed from response to 'state' command on mgmt interface */
52 struct host_cfg *rem_cfg;
53 char *tun_ip;
54 bool connected;
55};
56
Pau Espin Pedrol3d137182019-03-15 20:53:35 +010057/*
58 * The string format is documented in https://openvpn.net/community-resources/management-interface/
59 * Conversation loop looks like this (non-null terminated strings):
60 * CLI -> SRV: "state"
61 * SRV -> CLI: "1552674378,CONNECTED,SUCCESS,10.8.0.11,144.76.43.77,1194,," {0x0d, 0xa} "END" {0x0d, 0xa}
62 * More or less comma-separated fields are returned depending on OpenVPN version.
63 * v2.1.3: Sends only up to field 4 incl, eg: "1552674378,CONNECTED,SUCCESS,10.8.0.11,144.76.43.77" {0x0d, 0xa} "END" {0x0d, 0xa}
64 */
Max9a852f22019-01-31 15:58:57 +010065static char *parse_state(struct msgb *msg, struct openvpn_client *vpn)
66{
Pau Espin Pedrola3a92ec2019-03-15 20:39:02 +010067 char tmp[128], buf[128];
Max9a852f22019-01-31 15:58:57 +010068 char *tok;
Pau Espin Pedrol3d137182019-03-15 20:53:35 +010069 unsigned int i;
Max9a852f22019-01-31 15:58:57 +010070 uint8_t *m = msgb_data(msg);
Pau Espin Pedrolb9101322019-03-15 20:09:26 +010071 unsigned int truncated_len = OSMO_MIN(sizeof(tmp) - 1, msgb_length(msg));
Max9a852f22019-01-31 15:58:57 +010072
Pau Espin Pedrolb9101322019-03-15 20:09:26 +010073 if (msgb_length(msg) > truncated_len)
74 OVPN_LOG(msg, vpn, "received message too long (%d >= %u), truncating...\n", msgb_length(msg), truncated_len);
Max9a852f22019-01-31 15:58:57 +010075
76 if (msgb_length(msg) > 0) {
77 if (!isdigit(m[0])) /* skip OpenVPN greetings and alike */
78 return NULL;
79 } else {
80 OVPN_LOG(msg, vpn, "received message is empty, ignoring...\n");
81 return NULL;
82 }
83
Pau Espin Pedrolb9101322019-03-15 20:09:26 +010084 memcpy(tmp, m, truncated_len);
85 tmp[truncated_len] = '\0';
Max9a852f22019-01-31 15:58:57 +010086
Pau Espin Pedrol3d137182019-03-15 20:53:35 +010087 /* Only interested in first line; Remove return carriage, line feed + new line string "END" */
88 for (i = 0; i < truncated_len; i++) {
89 if (!isprint(tmp[i])) {
90 tmp[i] = '\0';
91 break;
92 }
93 }
94
95 for (tok = strtok(tmp, ","), i = 0; tok && i < MAX_RESP_COMPONENTS; tok = strtok(NULL, ",")) {
Pau Espin Pedrol778ac842019-03-15 20:56:42 +010096 /* Parse csv string and pick interesting tokens while ignoring the rest. */
97 switch (i++) {
98 /* case 0: unix/date time, not needed */
99 case 1:
100 update_name(vpn->rem_cfg, tok);
101 break;
102 case 2:
103 snprintf(buf, sizeof(buf), "%s (%s)", vpn->rem_cfg->name, tok);
104 update_name(vpn->rem_cfg, buf);
Pau Espin Pedrold7321342019-03-16 18:26:00 +0100105 break;
Pau Espin Pedrol778ac842019-03-15 20:56:42 +0100106 case 3:
107 osmo_talloc_replace_string(vpn->rem_cfg, &vpn->tun_ip, tok);
108 break;
109 case 4:
110 update_host(vpn->rem_cfg, tok);
111 break;
112 case 5:
113 vpn->rem_cfg->remote_port = atoi(tok);
114 break;
Max9a852f22019-01-31 15:58:57 +0100115 }
116 }
117 return NULL;
118}
119
120static struct openvpn_client *openvpn_client_find_or_make(const struct osysmon_state *os,
121 const char *host, uint16_t port)
122{
123 struct openvpn_client *vpn;
124 llist_for_each_entry(vpn, &os->openvpn_clients, list) {
125 if (match_config(vpn->cfg, host, MATCH_HOST) && vpn->cfg->remote_port == port)
126 return vpn;
127 }
128
129 return NULL;
130}
131
Max6cbdcaf2019-02-21 10:06:19 +0100132static int disconnect_cb(struct osmo_stream_cli *conn)
133{
134 struct openvpn_client *vpn = osmo_stream_cli_get_data(conn);
135
Pau Espin Pedrol53f50732019-03-15 19:18:18 +0100136 update_name(vpn->rem_cfg, "disconnected from openvpn management socket");
Max6cbdcaf2019-02-21 10:06:19 +0100137 vpn->connected = false;
138 talloc_free(vpn->tun_ip);
139 talloc_free(vpn->rem_cfg->remote_host);
140 vpn->tun_ip = NULL;
141 vpn->rem_cfg->remote_host = NULL;
142
143 return 0;
144}
145
Max9a852f22019-01-31 15:58:57 +0100146static int connect_cb(struct osmo_stream_cli *conn)
147{
148 struct openvpn_client *vpn = osmo_stream_cli_get_data(conn);
149
Pau Espin Pedrol53f50732019-03-15 19:18:18 +0100150 update_name(vpn->rem_cfg, "connected to openvpn management socket");
Max6cbdcaf2019-02-21 10:06:19 +0100151 vpn->connected = true;
Max9a852f22019-01-31 15:58:57 +0100152
153 return 0;
154}
155
156static int read_cb(struct osmo_stream_cli *conn)
157{
158 int bytes;
159 struct openvpn_client *vpn = osmo_stream_cli_get_data(conn);
160 struct msgb *msg = msgb_alloc(1024, "OpenVPN");
161 if (!msg) {
162 OVPN_LOG(conn, vpn, "unable to allocate message in callback\n");
163 return 0;
164 }
165
166 bytes = osmo_stream_cli_recv(conn, msg);
167 if (bytes < 0)
Pau Espin Pedrol53f50732019-03-15 19:18:18 +0100168 OVPN_LOG(msg, vpn, "read on openvpn management socket failed (%d)\n", bytes);
Max9a852f22019-01-31 15:58:57 +0100169 else
170 parse_state(msg, vpn);
171
172 msgb_free(msg);
173
174 return 0;
175}
176
177static bool openvpn_client_create(struct osysmon_state *os, const char *name, const char *host, uint16_t port)
178{
179 struct openvpn_client *vpn = openvpn_client_find_or_make(os, host, port);
180 if (vpn)
181 return true;
182
183 vpn = talloc_zero(os, struct openvpn_client);
184 if (!vpn)
185 return false;
186
187 vpn->connected = false;
188
189 vpn->cfg = host_cfg_alloc(vpn, name, host, port);
190 if (!vpn->cfg)
191 goto dealloc;
192
Pau Espin Pedrol53f50732019-03-15 19:18:18 +0100193 vpn->rem_cfg = host_cfg_alloc(vpn, "connecting to openvpn management socket", NULL, 0);
Max9a852f22019-01-31 15:58:57 +0100194 if (!vpn->rem_cfg)
195 goto dealloc;
196
197 vpn->mgmt = make_tcp_client(vpn->cfg);
198 if (!vpn->mgmt) {
199 OVPN_LOG(vpn->rem_cfg, vpn, "failed to create TCP client\n");
200 goto dealloc;
201 }
202
203 /* Wait for 1 minute before attempting to reconnect to management interface */
Pau Espin Pedrol0813db32019-03-15 19:27:04 +0100204 osmo_stream_cli_set_reconnect_timeout(vpn->mgmt, 5);
Max9a852f22019-01-31 15:58:57 +0100205 osmo_stream_cli_set_read_cb(vpn->mgmt, read_cb);
206 osmo_stream_cli_set_connect_cb(vpn->mgmt, connect_cb);
Max6cbdcaf2019-02-21 10:06:19 +0100207 osmo_stream_cli_set_disconnect_cb(vpn->mgmt, disconnect_cb);
Max9a852f22019-01-31 15:58:57 +0100208
209 if (osmo_stream_cli_open(vpn->mgmt) < 0) {
Pau Espin Pedrol53f50732019-03-15 19:18:18 +0100210 OVPN_LOG(vpn->rem_cfg, vpn, "failed to connect to management socket\n");
Max9a852f22019-01-31 15:58:57 +0100211 goto dealloc;
212 }
213
214 osmo_stream_cli_set_data(vpn->mgmt, vpn);
215 llist_add_tail(&vpn->list, &os->openvpn_clients);
216
217 return true;
218
219dealloc:
220 talloc_free(vpn);
221 return false;
222}
223
224static void openvpn_client_destroy(struct openvpn_client *vpn)
225{
226 if (!vpn)
227 return;
228
229 osmo_stream_cli_destroy(vpn->mgmt);
230 llist_del(&vpn->list);
231 talloc_free(vpn);
232}
233
234
235/***********************************************************************
236 * VTY
237 ***********************************************************************/
238
239#define OPENVPN_STR "Configure OpenVPN management interface address\n"
240
241DEFUN(cfg_openvpn, cfg_openvpn_cmd,
242 "openvpn HOST <1-65535>",
243 OPENVPN_STR "Name of the host to connect to\n" "Management interface port\n")
244{
245 uint16_t port = atoi(argv[1]);
246
247 if (!openvpn_client_create(g_oss, "OpenVPN", argv[0], port)) {
248 vty_out(vty, "Failed to create OpenVPN client for %s:%u%s", argv[0], port, VTY_NEWLINE);
249 return CMD_WARNING;
250 }
251
252 return CMD_SUCCESS;
253}
254
255DEFUN(cfg_no_openvpn, cfg_no_openvpn_cmd,
256 "no openvpn HOST <1-65535>",
257 NO_STR OPENVPN_STR "Name of the host to connect to\n" "Management interface port\n")
258{
259 uint16_t port = atoi(argv[1]);
260 struct openvpn_client *vpn = openvpn_client_find_or_make(g_oss, argv[0], port);
261 if (!vpn) {
262 vty_out(vty, "OpenVPN client %s:%u doesn't exist%s", argv[0], port, VTY_NEWLINE);
263 return CMD_WARNING;
264 }
265
266 openvpn_client_destroy(vpn);
267
268 return CMD_SUCCESS;
269}
270
271
272/***********************************************************************
273 * Runtime Code
274 ***********************************************************************/
275
276static int openvpn_client_poll(struct openvpn_client *vpn, struct value_node *parent)
277{
278 char *remote = make_authority(parent, vpn->rem_cfg);
279 struct value_node *vn_host = value_node_find_or_add(parent, make_authority(parent, vpn->cfg));
280 struct msgb *msg = msgb_alloc(128, "state");
281 if (!msg) {
282 value_node_add(vn_host, "msgb", "memory allocation failure");
283 return 0;
284 }
285
286 if (vpn->rem_cfg->name)
287 value_node_add(vn_host, "status", vpn->rem_cfg->name);
288
289 if (vpn->tun_ip)
290 value_node_add(vn_host, "tunnel", vpn->tun_ip);
291
292 if (remote)
293 value_node_add(vn_host, "remote", remote);
294
Max9a852f22019-01-31 15:58:57 +0100295 if (vpn->connected) { /* re-trigger state command */
296 msgb_printf(msg, "state\n");
297 osmo_stream_cli_send(vpn->mgmt, msg);
298 }
299
300 return 0;
301}
302
303/* called once on startup before config file parsing */
304int osysmon_openvpn_init()
305{
306 install_element(CONFIG_NODE, &cfg_openvpn_cmd);
307 install_element(CONFIG_NODE, &cfg_no_openvpn_cmd);
308
309 return 0;
310}
311
312/* called periodically */
313int osysmon_openvpn_poll(struct value_node *parent)
314{
315 int num_vpns = llist_count(&g_oss->openvpn_clients);
316 if (num_vpns) {
317 struct value_node *vn_vpn = value_node_add(parent, "OpenVPN", NULL);
318 struct openvpn_client *vpn;
319 llist_for_each_entry(vpn, &g_oss->openvpn_clients, list)
320 openvpn_client_poll(vpn, vn_vpn);
321 }
322
323 return num_vpns;
324}