@@ -2053,48 +2053,20 @@
*/
void
-io_wait_dowork_udp(struct context *c, struct multi_tcp *mtcp, const unsigned int flags)
+get_io_flags_dowork_udp(struct context *c, struct multi_tcp *mtcp, const unsigned int flags)
{
unsigned int socket = 0;
unsigned int tuntap = 0;
- struct event_set_return esr[4];
-
- /* These shifts all depend on EVENT_READ and EVENT_WRITE */
- static uintptr_t socket_shift = 0; /* depends on SOCKET_READ and SOCKET_WRITE */
- static uintptr_t tun_shift = 2; /* depends on TUN_READ and TUN_WRITE */
- static uintptr_t err_shift = 4; /* depends on ES_ERROR */
-#ifdef ENABLE_MANAGEMENT
- static uintptr_t management_shift = 6; /* depends on MANAGEMENT_READ and MANAGEMENT_WRITE */
-#endif
-#ifdef ENABLE_ASYNC_PUSH
- static int file_shift = FILE_SHIFT;
-#endif
-#if defined(TARGET_LINUX) || defined(TARGET_FREEBSD)
- static int dco_shift = DCO_SHIFT; /* Event from DCO linux kernel module */
-#endif
- int i;
+ static uintptr_t err_shift = 4;
/*
- * Decide what kind of events we want to wait for.
- */
- /*c->c2.event_set = mtcp->es; */
- /*event_reset(mtcp->es); */
- /*event_reset(c->c2.event_set); */
-
- /*
- * On win32 we use the keyboard or an event object as a source
- * of asynchronous signals.
+ * Calculate the flags based on the provided 'flags' argument.
*/
if (flags & IOW_WAIT_SIGNAL)
{
wait_signal(mtcp->es, (void *)err_shift);
}
- /*
- * If outgoing data (for TCP/UDP port) pending, wait for ready-to-send
- * status from TCP/UDP port. Otherwise, wait for incoming data on
- * TUN/TAP device.
- */
if (flags & IOW_TO_LINK)
{
if (flags & IOW_SHAPER)
@@ -2180,117 +2152,68 @@
/*
* Configure event wait based on socket, tuntap flags.
*/
- for (i = 0; i < c->c1.link_sockets_num; i++)
+ for (int i = 0; i < c->c1.link_sockets_num; i++)
{
- socket_set(c->c2.link_sockets[i], mtcp->es, socket,
- &c->c2.link_sockets[i]->ev_arg, NULL);
- }
- tun_set(c->c1.tuntap, c->c2.event_set, tuntap, (void *)tun_shift, NULL);
-#if defined(TARGET_LINUX) || defined(TARGET_FREEBSD)
- if (socket & EVENT_READ && c->c2.did_open_tun)
- {
- dco_event_set(&c->c1.tuntap->dco, mtcp->es, (void *)&dco_shift);
- }
-#endif
-
-#ifdef ENABLE_MANAGEMENT
- if (management)
- {
- management_socket_set(management, mtcp->es, (void *)management_shift, NULL);
- }
-#endif
-
-#ifdef ENABLE_ASYNC_PUSH
- /* arm inotify watcher */
- if (c->options.mode == MODE_SERVER)
- {
- event_ctl(mtcp->es, c->c2.inotify_fd, EVENT_READ, (void *)&file_shift);
- }
-#endif
-
- /*
- * Possible scenarios:
- * (1) tcp/udp port has data available to read
- * (2) tcp/udp port is ready to accept more data to write
- * (3) tun dev has data available to read
- * (4) tun dev is ready to accept more data to write
- * (5) we received a signal (handler sets signal_received)
- * (6) timeout (tv) expired
- */
-
- mtcp->event_set_status = ES_ERROR;
-
- if (!c->sig->signal_received)
- {
- if (!(flags & IOW_CHECK_RESIDUAL) || !sockets_read_residual(c))
+ if (proto_is_dgram(c->c2.link_sockets[i]->info.proto))
{
- int status;
-
-#ifdef ENABLE_DEBUG
- if (check_debug_level(D_EVENT_WAIT))
- {
- show_wait_status(c);
- }
-#endif
-
- /*
- * Wait for something to happen.
- */
- status = event_wait(mtcp->es, &c->c2.timeval, esr, SIZE(esr));
-
- check_status(status, "event_wait", NULL, NULL);
-
- if (status > 0)
- {
- /*printf("\nstatus: %d\n", status); */
- int i;
- mtcp->event_set_status = 0;
- for (i = 0; i < status; ++i)
- {
- const struct event_set_return *e = &esr[i];
- uintptr_t shift;
-
- if (e->arg >= MULTI_N)
- {
- struct event_arg *ev_arg = (struct event_arg *)e->arg;
- if (ev_arg->type != EVENT_ARG_LINK_SOCKET)
- {
- mtcp->event_set_status = ES_ERROR;
- msg(D_LINK_ERRORS,
- "io_work: non socket event delivered");
- return;
- }
-
- shift = socket_shift;
- /* mark socket so that the multi code knows where we
- * have pending i/o */
- ev_arg->pending = true;
- }
- else
- {
- shift = (uintptr_t)e->arg;
- }
-
- mtcp->event_set_status |= ((e->rwflags & 3) << shift);
- }
- }
- else if (status == 0)
- {
- mtcp->event_set_status = ES_TIMEOUT;
- }
+ socket_set(c->c2.link_sockets[i], mtcp->es, socket,
+ &c->c2.link_sockets[i]->ev_arg, NULL);
}
}
+ mtcp->udp_flags = socket | tuntap;
+}
- /* 'now' should always be a reasonably up-to-date timestamp */
- update_time();
-
- /* set signal_received if a signal was received */
- if (mtcp->event_set_status & ES_ERROR)
+void
+get_io_flags_udp(struct context *c, struct multi_tcp *mtcp, const unsigned int flags)
+{
+ mtcp->udp_flags = ES_ERROR;
+ if (c->c2.fast_io && (flags & (IOW_TO_TUN | IOW_TO_LINK | IOW_MBUF)))
{
- get_signal(&c->sig->signal_received);
+ /* fast path -- only for TUN/TAP/UDP writes */
+ unsigned int ret = 0;
+ if (flags & IOW_TO_TUN)
+ {
+ ret |= TUN_WRITE;
+ }
+ if (flags & (IOW_TO_LINK | IOW_MBUF))
+ {
+ ret |= SOCKET_WRITE;
+ }
+ mtcp->udp_flags = ret;
}
-
- dmsg(D_EVENT_WAIT, "I/O WAIT status=0x%04x", mtcp->event_set_status);
+ else
+ {
+#ifdef _WIN32
+ bool skip_iowait = flags & IOW_TO_TUN;
+ if (flags & IOW_READ_TUN)
+ {
+ /*
+ * don't read from tun if we have pending write to link,
+ * since every tun read overwrites to_link buffer filled
+ * by previous tun read
+ */
+ skip_iowait = !(flags & IOW_TO_LINK);
+ }
+ if (tuntap_is_wintun(c->c1.tuntap) && skip_iowait)
+ {
+ unsigned int ret = 0;
+ if (flags & IOW_TO_TUN)
+ {
+ ret |= TUN_WRITE;
+ }
+ if (flags & IOW_READ_TUN)
+ {
+ ret |= TUN_READ;
+ }
+ mtcp->udp_flags = ret;
+ }
+ else
+#endif /* ifdef _WIN32 */
+ {
+ /* slow path - delegate to io_wait_dowork_udp to calculate flags */
+ get_io_flags_dowork_udp(c, mtcp, flags);
+ }
+ }
}
void
@@ -2308,10 +2231,10 @@
static uintptr_t management_shift = 6; /* depends on MANAGEMENT_READ and MANAGEMENT_WRITE */
#endif
#ifdef ENABLE_ASYNC_PUSH
- static int file_shift = FILE_SHIFT;
+ static uintptr_t file_shift = FILE_SHIFT;
#endif
#if defined(TARGET_LINUX) || defined(TARGET_FREEBSD)
- static int dco_shift = DCO_SHIFT; /* Event from DCO linux kernel module */
+ static uintptr_t dco_shift = DCO_SHIFT; /* Event from DCO linux kernel module */
#endif
int i;
@@ -2428,7 +2351,7 @@
#if defined(TARGET_LINUX) || defined(TARGET_FREEBSD)
if (socket & EVENT_READ && c->c2.did_open_tun)
{
- dco_event_set(&c->c1.tuntap->dco, c->c2.event_set, (void *)&dco_shift);
+ dco_event_set(&c->c1.tuntap->dco, c->c2.event_set, (void *)dco_shift);
}
#endif
@@ -2443,7 +2366,7 @@
/* arm inotify watcher */
if (c->options.mode == MODE_SERVER)
{
- event_ctl(c->c2.event_set, c->c2.inotify_fd, EVENT_READ, (void *)&file_shift);
+ event_ctl(c->c2.event_set, c->c2.inotify_fd, EVENT_READ, (void *)file_shift);
}
#endif
@@ -69,7 +69,9 @@
extern counter_type link_write_bytes_global;
-void io_wait_dowork_udp(struct context *c, struct multi_tcp *mtcp, const unsigned int flags);
+void get_io_flags_dowork_udp(struct context *c, struct multi_tcp *mtcp, const unsigned int flags);
+
+void get_io_flags_udp(struct context *c, struct multi_tcp *mtcp, const unsigned int flags);
void io_wait_dowork(struct context *c, const unsigned int flags);
@@ -366,58 +368,6 @@
return flags;
}
-static inline void
-io_wait_udp(struct context *c, struct multi_tcp *mtcp, const unsigned int flags)
-{
- if (c->c2.fast_io && (flags & (IOW_TO_TUN|IOW_TO_LINK|IOW_MBUF)))
- {
- /* fast path -- only for TUN/TAP/UDP writes */
- unsigned int ret = 0;
- if (flags & IOW_TO_TUN)
- {
- ret |= TUN_WRITE;
- }
- if (flags & (IOW_TO_LINK|IOW_MBUF))
- {
- ret |= SOCKET_WRITE;
- }
- mtcp->event_set_status = ret;
- }
- else
- {
-#ifdef _WIN32
- bool skip_iowait = flags & IOW_TO_TUN;
- if (flags & IOW_READ_TUN)
- {
- /*
- * don't read from tun if we have pending write to link,
- * since every tun read overwrites to_link buffer filled
- * by previous tun read
- */
- skip_iowait = !(flags & IOW_TO_LINK);
- }
- if (tuntap_is_wintun(c->c1.tuntap) && skip_iowait)
- {
- unsigned int ret = 0;
- if (flags & IOW_TO_TUN)
- {
- ret |= TUN_WRITE;
- }
- if (flags & IOW_READ_TUN)
- {
- ret |= TUN_READ;
- }
- mtcp->event_set_status = ret;
- }
- else
-#endif /* ifdef _WIN32 */
- {
- /* slow path */
- io_wait_dowork_udp(c, mtcp, flags);
- }
- }
-}
-
/*
* This is the core I/O wait function, used for all I/O waits except
* for TCP in server mode.
@@ -230,7 +230,7 @@
if (streq(p[1], "HTTP"))
{
struct http_proxy_options *ho;
- if (ce->proto != PROTO_TCP && ce->proto != PROTO_TCP_CLIENT)
+ if (ce->proto != PROTO_TCP && c->mode != CM_CHILD_TCP)
{
msg(M_WARN, "HTTP proxy support only works for TCP based connections");
return false;
@@ -607,7 +607,10 @@
ce_defined = false;
}
+ int proto = c->options.ce.proto;
c->options.ce = *ce;
+ c->options.ce.proto = proto;
+
#ifdef ENABLE_MANAGEMENT
if (ce_defined && management && management_query_remote_enabled(management))
{
@@ -2628,7 +2631,7 @@
if (found & OPT_P_EXPLICIT_NOTIFY)
{
- if (!proto_is_udp(c->options.ce.proto) && c->options.ce.explicit_exit_notification)
+ if (!proto_is_udp(c->c2.link_sockets[0]->info.proto) && c->options.ce.explicit_exit_notification)
{
msg(D_PUSH, "OPTIONS IMPORT: --explicit-exit-notify can only be used with --proto udp");
c->options.ce.explicit_exit_notification = 0;
@@ -2788,14 +2791,21 @@
int sec = 2;
int backoff = 0;
- switch (c->options.ce.proto)
+ switch (c->mode)
{
- case PROTO_TCP_SERVER:
- sec = 1;
+ case CM_TOP:
+ if (has_udp_in_local_list(&c->options))
+ {
+ sec = c->options.ce.connect_retry_seconds;
+ }
+ else
+ {
+ sec = 1;
+ }
break;
- case PROTO_UDP:
- case PROTO_TCP_CLIENT:
+ case CM_CHILD_UDP:
+ case CM_CHILD_TCP:
sec = c->options.ce.connect_retry_seconds;
break;
}
@@ -2813,7 +2823,7 @@
}
/* Slow down reconnection after 5 retries per remote -- for TCP client or UDP tls-client only */
- if (c->options.ce.proto == PROTO_TCP_CLIENT
+ if (c->mode == CM_CHILD_TCP
|| (c->options.ce.proto == PROTO_UDP && c->options.tls_client))
{
backoff = (c->options.unsuccessful_attempts / c->options.connection_list->len) - 4;
@@ -3293,7 +3303,21 @@
to.server = options->tls_server;
to.replay_window = options->replay_window;
to.replay_time = options->replay_time;
- to.tcp_mode = link_socket_proto_connection_oriented(options->ce.proto);
+
+ if (c->options.ce.local_list->len > 1)
+ {
+ for (int i = 0; i < c->options.ce.local_list->len; i++)
+ {
+ if (proto_is_dgram(c->options.ce.local_list->array[i]->proto))
+ {
+ to.tcp_mode = false;
+ }
+ }
+ }
+ else
+ {
+ to.tcp_mode = link_socket_proto_connection_oriented(c->options.ce.local_list->array[0]->proto);
+ }
to.config_ciphername = c->options.ciphername;
to.config_ncp_ciphers = c->options.ncp_ciphers;
to.transition_window = options->transition_window;
@@ -3338,7 +3362,7 @@
/* should we not xmit any packets until we get an initial
* response from client? */
- if (to.server && options->ce.proto == PROTO_TCP_SERVER)
+ if (to.server && c->mode == CM_CHILD_TCP)
{
to.xmit_hold = true;
}
@@ -4257,20 +4281,13 @@
#ifdef _WIN32
msg(M_INFO, "NOTE: --fast-io is disabled since we are running on Windows");
#else
- if (!proto_is_udp(c->options.ce.proto))
+ if (c->options.shaper)
{
- msg(M_INFO, "NOTE: --fast-io is disabled since we are not using UDP");
+ msg(M_INFO, "NOTE: --fast-io is disabled since we are using --shaper");
}
else
{
- if (c->options.shaper)
- {
- msg(M_INFO, "NOTE: --fast-io is disabled since we are using --shaper");
- }
- else
- {
- c->c2.fast_io = true;
- }
+ c->c2.fast_io = true;
}
#endif
}
@@ -4967,6 +4984,7 @@
/* options */
dest->options = src->options;
+ dest->options.ce.proto = ls->info.proto;
options_detach(&dest->options);
dest->c2.event_set = src->c2.event_set;
@@ -5065,10 +5083,7 @@
dest->c2.es_owned = false;
dest->c2.event_set = NULL;
- if (proto_is_dgram(src->options.ce.proto))
- {
- do_event_set_init(dest, false);
- }
+ do_event_set_init(dest, false);
#ifdef USE_COMP
dest->c2.comp_context = NULL;
@@ -680,11 +680,11 @@
void
multi_tcp_process_io(struct multi_context *m)
{
+ struct multi_tcp *mtcp = m->mtcp;
+ const unsigned int udp_status = mtcp->udp_flags;
const unsigned int mpp_flags = m->top.c2.fast_io
? (MPP_CONDITIONAL_PRE_SELECT | MPP_CLOSE_ON_SIGNAL)
: (MPP_PRE_SELECT | MPP_CLOSE_ON_SIGNAL);
- struct multi_tcp *mtcp = m->mtcp;
- const unsigned int status = mtcp->event_set_status;
int i;
for (i = 0; i < mtcp->n_esr; ++i)
@@ -725,9 +725,10 @@
msg(D_MULTI_ERRORS, "MULTI: mtcp_proc_io: null socket");
break;
}
- socket_reset_listen_persistent(ev_arg->u.ls);
+
if (!proto_is_dgram(ev_arg->u.ls->info.proto))
{
+ socket_reset_listen_persistent(ev_arg->u.ls);
mi = multi_create_instance_tcp(m, ev_arg->u.ls);
if (mi)
{
@@ -737,7 +738,23 @@
}
else
{
- if (status & SOCKET_READ)
+ if (e->arg >= MULTI_N)
+ {
+ struct event_arg *ev_arg = (struct event_arg *)e->arg;
+ if (ev_arg->type != EVENT_ARG_LINK_SOCKET)
+ {
+ mtcp->udp_flags = ES_ERROR;
+ msg(D_LINK_ERRORS,
+ "io_work: non socket event delivered");
+ break;
+ }
+ }
+ else
+ {
+ ev_arg->pending = true;
+ }
+
+ if (udp_status & SOCKET_READ)
{
read_incoming_link(&m->top, ev_arg->u.ls);
if (!IS_SIG(&m->top))
@@ -750,12 +767,12 @@
while (true)
{
multi_get_timeout(m, &m->top.c2.timeval);
- io_wait_udp(&m->top, m->mtcp, p2mp_iow_flags(m));
+ get_io_flags_udp(&m->top, m->mtcp, p2mp_iow_flags(m));
MULTI_CHECK_SIG(m);
multi_process_per_second_timers(m);
- if (m->mtcp->event_set_status == ES_TIMEOUT)
+ if (m->mtcp->udp_flags == ES_TIMEOUT)
{
multi_process_timeout(m, MPP_PRE_SELECT | MPP_CLOSE_ON_SIGNAL);
}
@@ -55,7 +55,7 @@
int n_esr;
int maxevents;
unsigned int tun_rwflags;
- unsigned int event_set_status;
+ unsigned int udp_flags;
#ifdef ENABLE_MANAGEMENT
unsigned int management_persist_flags;
#endif
@@ -198,7 +198,6 @@
if (mroute_extract_openvpn_sockaddr(&real, &m->top.c2.from.dest, true)
&& m->top.c2.buf.len > 0)
{
- /*real.proto = ls->info.proto; */
struct hash_element *he;
const uint32_t hv = hash_value(hash, &real);
struct hash_bucket *bucket = hash_bucket(hash, hv);
@@ -380,9 +379,7 @@
void
multi_process_io_udp(struct multi_context *m)
{
- const unsigned int status = m->mtcp->event_set_status;
- /*p2mp_iow_flags(m); */
- /*m->top.c2.event_set_status; */
+ unsigned int status = m->mtcp->udp_flags;
const unsigned int mpp_flags = m->top.c2.fast_io
? (MPP_CONDITIONAL_PRE_SELECT | MPP_CLOSE_ON_SIGNAL)
: (MPP_PRE_SELECT | MPP_CLOSE_ON_SIGNAL);
@@ -607,6 +607,7 @@
ASSERT(!mi->halt);
mi->halt = true;
+ bool is_dgram = proto_is_dgram(mi->context.c2.link_sockets[0]->info.proto);
dmsg(D_MULTI_DEBUG, "MULTI: multi_close_instance called");
@@ -665,7 +666,7 @@
mi->did_iroutes = false;
}
- if (m->mtcp && !proto_is_dgram(m->top.options.ce.proto))
+ if (!is_dgram)
{
multi_tcp_dereference_instance(m->mtcp, mi);
}
@@ -3406,6 +3407,7 @@
perf_push(PERF_PROC_IN_LINK);
lsi = get_link_socket_info(c);
+ /*lsi = &ls->info; */
orig_buf = c->c2.buf.data;
if (process_incoming_link_part1(c, lsi, floated))
{
@@ -3856,7 +3858,7 @@
while ((he = hash_iterator_next(&hi)))
{
struct multi_instance *mi = (struct multi_instance *) he->value;
- if (!mi->halt)
+ if (!mi->halt && proto_is_dgram(mi->context.options.ce.proto))
{
send_control_channel_string(&mi->context, next_server ? "RESTART,[N]" : "RESTART", D_PUSH);
multi_schedule_context_wakeup(m, mi);
@@ -3894,13 +3896,15 @@
status_close(so);
return false;
}
- else if (proto_is_dgram(m->top.options.ce.proto)
- && is_exit_restart(m->top.sig->signal_received)
- && (m->deferred_shutdown_signal.signal_received == 0)
- && m->top.options.ce.explicit_exit_notification != 0)
+ else if (has_udp_in_local_list(&m->top.options))
{
- multi_push_restart_schedule_exit(m, m->top.options.ce.explicit_exit_notification == 2);
- return false;
+ if (is_exit_restart(m->top.sig->signal_received)
+ && (m->deferred_shutdown_signal.signal_received == 0)
+ && m->top.options.ce.explicit_exit_notification != 0)
+ {
+ multi_push_restart_schedule_exit(m, m->top.options.ce.explicit_exit_notification == 2);
+ return false;
+ }
}
return true;
}
@@ -4198,10 +4202,10 @@
multi_tcp_process_io(multi);
MULTI_CHECK_SIG(multi);
}
- /*else if (status == 0)
- * {
- * multi_tcp_action(multi, NULL, TA_TIMEOUT, false);
- * }*/
+ else if (status == 0)
+ {
+ multi_tcp_action(multi, NULL, TA_TIMEOUT, false);
+ }
perf_pop();
}
@@ -987,10 +987,13 @@
const struct connection_entry *e,
const int i)
{
- setenv_str_i(es, "proto", proto2ascii(e->proto, e->af, false), i);
- /* expected to befor single socket contexts only */
- setenv_str_i(es, "local", e->local_list->array[0]->local, i);
- setenv_str_i(es, "local_port", e->local_list->array[0]->port, i);
+ for (int j = 0; j < e->local_list->len; j++)
+ {
+ setenv_str_i(es, "proto", proto2ascii(e->local_list->array[j]->proto, e->af, false), i);
+ /* expected to befor single socket contexts only */
+ setenv_str_i(es, "local", e->local_list->array[j]->local, i);
+ setenv_str_i(es, "local_port", e->local_list->array[j]->port, i);
+ }
setenv_str_i(es, "remote", e->remote, i);
setenv_str_i(es, "remote_port", e->remote_port, i);
@@ -2455,7 +2458,7 @@
{
struct local_entry *le = ce->local_list->array[i];
- if (proto_is_net(ce->proto)
+ if (proto_is_net(le->proto)
&& string_defined_equal(le->local, ce->remote)
&& string_defined_equal(le->port, ce->remote_port))
{
@@ -3781,6 +3784,12 @@
options_postprocess_mutate_ce(o, o->connection_list->array[i]);
}
+ if (!has_udp_in_local_list(o))
+ {
+ o->fast_io = false;
+ msg(M_INFO, "NOTE: --fast-io is disabled while using multi-socket");
+ }
+
if (o->ce.local_list)
{
for (i = 0; i < o->ce.local_list->len; i++)
@@ -9629,3 +9638,19 @@
err:
gc_free(&gc);
}
+
+bool
+has_udp_in_local_list(const struct options *options)
+{
+ if (options->ce.local_list)
+ {
+ for (int i = 0; i < options->ce.local_list->len; i++)
+ {
+ if (proto_is_dgram(options->ce.local_list->array[i]->proto))
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+}
@@ -913,6 +913,8 @@
bool key_is_external(const struct options *options);
+bool has_udp_in_local_list(const struct options *options);
+
/**
* Returns whether the current configuration has dco enabled.
*/
Attention is currently required from: flichtenheld, plaisthos. Hello plaisthos, flichtenheld, I'd like you to do a code review. Please visit http://gerrit.openvpn.net/c/openvpn/+/440?usp=email to review the following change. Change subject: Using the same wait function for both TCP and UDP ...................................................................... Using the same wait function for both TCP and UDP FORWARD: added a new function to collect UDP flags. MTCP: the mtcp structure has been modified to carry around those flags. The second wait function (in UDP case) has been removed. MULTI: properly remove TCP instances by checking the multi_instance protocol instead of the global one. TLS: set the tls_option xmit_hold bool value to true only in case of TCP child instance to avoid checking the global protocol value. INIT: initialize the c->c2.event_set in the inherit_context_top() by default and not only in case of UDP since we could have multiple different sockets. Change-Id: I81ec69d12abc9a661875c93c7f1bd97e525df55f Signed-off-by: Gianmarco De Gregori <gianmarco@mandelbit.com> --- M src/openvpn/forward.c M src/openvpn/forward.h M src/openvpn/init.c M src/openvpn/mtcp.c M src/openvpn/mtcp.h M src/openvpn/mudp.c M src/openvpn/multi.c M src/openvpn/options.c M src/openvpn/options.h 9 files changed, 177 insertions(+), 244 deletions(-) git pull ssh://gerrit.openvpn.net:29418/openvpn refs/changes/40/440/1