blob: 8d88def6fadb5ff17a1ebd1ecb8925e71693f044 [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.
18 *
19 * You should have received a copy of the GNU General Public License
20 * along with this program; if not, write to the Free Software
21 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
22 * MA 02110-1301, USA.
23 */
24
25#include <string.h>
26#include <stdbool.h>
27#include <ctype.h>
28#include <errno.h>
29
30#include <osmocom/core/utils.h>
31#include <osmocom/vty/vty.h>
32#include <osmocom/vty/command.h>
33#include <osmocom/core/msgb.h>
34#include <osmocom/netif/stream.h>
35
36#include "osysmon.h"
37#include "client.h"
38#include "value_node.h"
39
40/***********************************************************************
41 * Data model
42 ***********************************************************************/
43
44#define OVPN_LOG(ctx, vpn, fmt, args...) \
45 fprintf(stderr, "OpenVPN [%s]: " fmt, make_authority(ctx, vpn->cfg), ##args)
46
47/* max number of csv in response */
48#define MAX_RESP_COMPONENTS 6
49
50/* a single OpenVPN management interface client */
51struct openvpn_client {
52 /* links to osysmon.openvpn_clients */
53 struct llist_head list;
54 struct host_cfg *cfg;
55 struct osmo_stream_cli *mgmt;
56 /* fields below are parsed from response to 'state' command on mgmt interface */
57 struct host_cfg *rem_cfg;
58 char *tun_ip;
59 bool connected;
60};
61
Pau Espin Pedrol3d137182019-03-15 20:53:35 +010062/*
63 * The string format is documented in https://openvpn.net/community-resources/management-interface/
64 * Conversation loop looks like this (non-null terminated strings):
65 * CLI -> SRV: "state"
66 * SRV -> CLI: "1552674378,CONNECTED,SUCCESS,10.8.0.11,144.76.43.77,1194,," {0x0d, 0xa} "END" {0x0d, 0xa}
67 * More or less comma-separated fields are returned depending on OpenVPN version.
68 * 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}
69 */
Max9a852f22019-01-31 15:58:57 +010070static char *parse_state(struct msgb *msg, struct openvpn_client *vpn)
71{
Pau Espin Pedrola3a92ec2019-03-15 20:39:02 +010072 char tmp[128], buf[128];
Max9a852f22019-01-31 15:58:57 +010073 char *tok;
Pau Espin Pedrol3d137182019-03-15 20:53:35 +010074 unsigned int i;
Max9a852f22019-01-31 15:58:57 +010075 uint8_t *m = msgb_data(msg);
Pau Espin Pedrolb9101322019-03-15 20:09:26 +010076 unsigned int truncated_len = OSMO_MIN(sizeof(tmp) - 1, msgb_length(msg));
Max9a852f22019-01-31 15:58:57 +010077
Pau Espin Pedrolb9101322019-03-15 20:09:26 +010078 if (msgb_length(msg) > truncated_len)
79 OVPN_LOG(msg, vpn, "received message too long (%d >= %u), truncating...\n", msgb_length(msg), truncated_len);
Max9a852f22019-01-31 15:58:57 +010080
81 if (msgb_length(msg) > 0) {
82 if (!isdigit(m[0])) /* skip OpenVPN greetings and alike */
83 return NULL;
84 } else {
85 OVPN_LOG(msg, vpn, "received message is empty, ignoring...\n");
86 return NULL;
87 }
88
Pau Espin Pedrolb9101322019-03-15 20:09:26 +010089 memcpy(tmp, m, truncated_len);
90 tmp[truncated_len] = '\0';
Max9a852f22019-01-31 15:58:57 +010091
Pau Espin Pedrol3d137182019-03-15 20:53:35 +010092 /* Only interested in first line; Remove return carriage, line feed + new line string "END" */
93 for (i = 0; i < truncated_len; i++) {
94 if (!isprint(tmp[i])) {
95 tmp[i] = '\0';
96 break;
97 }
98 }
99
100 for (tok = strtok(tmp, ","), i = 0; tok && i < MAX_RESP_COMPONENTS; tok = strtok(NULL, ",")) {
Pau Espin Pedrol778ac842019-03-15 20:56:42 +0100101 /* Parse csv string and pick interesting tokens while ignoring the rest. */
102 switch (i++) {
103 /* case 0: unix/date time, not needed */
104 case 1:
105 update_name(vpn->rem_cfg, tok);
106 break;
107 case 2:
108 snprintf(buf, sizeof(buf), "%s (%s)", vpn->rem_cfg->name, tok);
109 update_name(vpn->rem_cfg, buf);
Pau Espin Pedrold7321342019-03-16 18:26:00 +0100110 break;
Pau Espin Pedrol778ac842019-03-15 20:56:42 +0100111 case 3:
112 osmo_talloc_replace_string(vpn->rem_cfg, &vpn->tun_ip, tok);
113 break;
114 case 4:
115 update_host(vpn->rem_cfg, tok);
116 break;
117 case 5:
118 vpn->rem_cfg->remote_port = atoi(tok);
119 break;
Max9a852f22019-01-31 15:58:57 +0100120 }
121 }
122 return NULL;
123}
124
125static struct openvpn_client *openvpn_client_find_or_make(const struct osysmon_state *os,
126 const char *host, uint16_t port)
127{
128 struct openvpn_client *vpn;
129 llist_for_each_entry(vpn, &os->openvpn_clients, list) {
130 if (match_config(vpn->cfg, host, MATCH_HOST) && vpn->cfg->remote_port == port)
131 return vpn;
132 }
133
134 return NULL;
135}
136
Max6cbdcaf2019-02-21 10:06:19 +0100137static int disconnect_cb(struct osmo_stream_cli *conn)
138{
139 struct openvpn_client *vpn = osmo_stream_cli_get_data(conn);
140
Pau Espin Pedrol53f50732019-03-15 19:18:18 +0100141 update_name(vpn->rem_cfg, "disconnected from openvpn management socket");
Max6cbdcaf2019-02-21 10:06:19 +0100142 vpn->connected = false;
143 talloc_free(vpn->tun_ip);
144 talloc_free(vpn->rem_cfg->remote_host);
145 vpn->tun_ip = NULL;
146 vpn->rem_cfg->remote_host = NULL;
147
148 return 0;
149}
150
Max9a852f22019-01-31 15:58:57 +0100151static int connect_cb(struct osmo_stream_cli *conn)
152{
153 struct openvpn_client *vpn = osmo_stream_cli_get_data(conn);
154
Pau Espin Pedrol53f50732019-03-15 19:18:18 +0100155 update_name(vpn->rem_cfg, "connected to openvpn management socket");
Max6cbdcaf2019-02-21 10:06:19 +0100156 vpn->connected = true;
Max9a852f22019-01-31 15:58:57 +0100157
158 return 0;
159}
160
161static int read_cb(struct osmo_stream_cli *conn)
162{
163 int bytes;
164 struct openvpn_client *vpn = osmo_stream_cli_get_data(conn);
165 struct msgb *msg = msgb_alloc(1024, "OpenVPN");
166 if (!msg) {
167 OVPN_LOG(conn, vpn, "unable to allocate message in callback\n");
168 return 0;
169 }
170
171 bytes = osmo_stream_cli_recv(conn, msg);
172 if (bytes < 0)
Pau Espin Pedrol53f50732019-03-15 19:18:18 +0100173 OVPN_LOG(msg, vpn, "read on openvpn management socket failed (%d)\n", bytes);
Max9a852f22019-01-31 15:58:57 +0100174 else
175 parse_state(msg, vpn);
176
177 msgb_free(msg);
178
179 return 0;
180}
181
182static bool openvpn_client_create(struct osysmon_state *os, const char *name, const char *host, uint16_t port)
183{
184 struct openvpn_client *vpn = openvpn_client_find_or_make(os, host, port);
185 if (vpn)
186 return true;
187
188 vpn = talloc_zero(os, struct openvpn_client);
189 if (!vpn)
190 return false;
191
192 vpn->connected = false;
193
194 vpn->cfg = host_cfg_alloc(vpn, name, host, port);
195 if (!vpn->cfg)
196 goto dealloc;
197
Pau Espin Pedrol53f50732019-03-15 19:18:18 +0100198 vpn->rem_cfg = host_cfg_alloc(vpn, "connecting to openvpn management socket", NULL, 0);
Max9a852f22019-01-31 15:58:57 +0100199 if (!vpn->rem_cfg)
200 goto dealloc;
201
202 vpn->mgmt = make_tcp_client(vpn->cfg);
203 if (!vpn->mgmt) {
204 OVPN_LOG(vpn->rem_cfg, vpn, "failed to create TCP client\n");
205 goto dealloc;
206 }
207
208 /* Wait for 1 minute before attempting to reconnect to management interface */
Pau Espin Pedrol0813db32019-03-15 19:27:04 +0100209 osmo_stream_cli_set_reconnect_timeout(vpn->mgmt, 5);
Max9a852f22019-01-31 15:58:57 +0100210 osmo_stream_cli_set_read_cb(vpn->mgmt, read_cb);
211 osmo_stream_cli_set_connect_cb(vpn->mgmt, connect_cb);
Max6cbdcaf2019-02-21 10:06:19 +0100212 osmo_stream_cli_set_disconnect_cb(vpn->mgmt, disconnect_cb);
Max9a852f22019-01-31 15:58:57 +0100213
214 if (osmo_stream_cli_open(vpn->mgmt) < 0) {
Pau Espin Pedrol53f50732019-03-15 19:18:18 +0100215 OVPN_LOG(vpn->rem_cfg, vpn, "failed to connect to management socket\n");
Max9a852f22019-01-31 15:58:57 +0100216 goto dealloc;
217 }
218
219 osmo_stream_cli_set_data(vpn->mgmt, vpn);
220 llist_add_tail(&vpn->list, &os->openvpn_clients);
221
222 return true;
223
224dealloc:
225 talloc_free(vpn);
226 return false;
227}
228
229static void openvpn_client_destroy(struct openvpn_client *vpn)
230{
231 if (!vpn)
232 return;
233
234 osmo_stream_cli_destroy(vpn->mgmt);
235 llist_del(&vpn->list);
236 talloc_free(vpn);
237}
238
239
240/***********************************************************************
241 * VTY
242 ***********************************************************************/
243
244#define OPENVPN_STR "Configure OpenVPN management interface address\n"
245
246DEFUN(cfg_openvpn, cfg_openvpn_cmd,
247 "openvpn HOST <1-65535>",
248 OPENVPN_STR "Name of the host to connect to\n" "Management interface port\n")
249{
250 uint16_t port = atoi(argv[1]);
251
252 if (!openvpn_client_create(g_oss, "OpenVPN", argv[0], port)) {
253 vty_out(vty, "Failed to create OpenVPN client for %s:%u%s", argv[0], port, VTY_NEWLINE);
254 return CMD_WARNING;
255 }
256
257 return CMD_SUCCESS;
258}
259
260DEFUN(cfg_no_openvpn, cfg_no_openvpn_cmd,
261 "no openvpn HOST <1-65535>",
262 NO_STR OPENVPN_STR "Name of the host to connect to\n" "Management interface port\n")
263{
264 uint16_t port = atoi(argv[1]);
265 struct openvpn_client *vpn = openvpn_client_find_or_make(g_oss, argv[0], port);
266 if (!vpn) {
267 vty_out(vty, "OpenVPN client %s:%u doesn't exist%s", argv[0], port, VTY_NEWLINE);
268 return CMD_WARNING;
269 }
270
271 openvpn_client_destroy(vpn);
272
273 return CMD_SUCCESS;
274}
275
276
277/***********************************************************************
278 * Runtime Code
279 ***********************************************************************/
280
281static int openvpn_client_poll(struct openvpn_client *vpn, struct value_node *parent)
282{
283 char *remote = make_authority(parent, vpn->rem_cfg);
284 struct value_node *vn_host = value_node_find_or_add(parent, make_authority(parent, vpn->cfg));
285 struct msgb *msg = msgb_alloc(128, "state");
286 if (!msg) {
287 value_node_add(vn_host, "msgb", "memory allocation failure");
288 return 0;
289 }
290
291 if (vpn->rem_cfg->name)
292 value_node_add(vn_host, "status", vpn->rem_cfg->name);
293
294 if (vpn->tun_ip)
295 value_node_add(vn_host, "tunnel", vpn->tun_ip);
296
297 if (remote)
298 value_node_add(vn_host, "remote", remote);
299
Max9a852f22019-01-31 15:58:57 +0100300 if (vpn->connected) { /* re-trigger state command */
301 msgb_printf(msg, "state\n");
302 osmo_stream_cli_send(vpn->mgmt, msg);
303 }
304
305 return 0;
306}
307
308/* called once on startup before config file parsing */
309int osysmon_openvpn_init()
310{
311 install_element(CONFIG_NODE, &cfg_openvpn_cmd);
312 install_element(CONFIG_NODE, &cfg_no_openvpn_cmd);
313
314 return 0;
315}
316
317/* called periodically */
318int osysmon_openvpn_poll(struct value_node *parent)
319{
320 int num_vpns = llist_count(&g_oss->openvpn_clients);
321 if (num_vpns) {
322 struct value_node *vn_vpn = value_node_add(parent, "OpenVPN", NULL);
323 struct openvpn_client *vpn;
324 llist_for_each_entry(vpn, &g_oss->openvpn_clients, list)
325 openvpn_client_poll(vpn, vn_vpn);
326 }
327
328 return num_vpns;
329}