Newer
Older
XinYang_IOS / Carthage / Checkouts / OpenVPNAdapter / Sources / OpenVPN3 / openvpn / omi / openvpn.cpp
@zhangfeng zhangfeng on 7 Dec 2023 25 KB 1.8.0
//    OpenVPN -- An application to securely tunnel IP networks
//               over a single port, with support for SSL/TLS-based
//               session authentication and key exchange,
//               packet encryption, packet authentication, and
//               packet compression.
//
//    Copyright (C) 2012-2020 OpenVPN Inc.
//
//    This program is free software: you can redistribute it and/or modify
//    it under the terms of the GNU Affero General Public License Version 3
//    as published by the Free Software Foundation.
//
//    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 Affero General Public License for more details.
//
//    You should have received a copy of the GNU Affero General Public License
//    along with this program in the COPYING file.
//    If not, see <http://www.gnu.org/licenses/>.

// OpenVPN 3 client with Management Interface

#define OMI_VERSION "1.0.0"

#include <string>
#include <vector>
#include <thread>
#include <memory>
#include <utility>
#include <mutex>
#include <condition_variable>

// don't export core symbols
#define OPENVPN_CORE_API_VISIBILITY_HIDDEN

// should be included before other openvpn includes,
// with the exception of openvpn/log includes
#include <client/ovpncli.cpp>

#include <openvpn/common/platform.hpp>
#include <openvpn/common/exception.hpp>
#include <openvpn/common/file.hpp>
#include <openvpn/common/string.hpp>
#include <openvpn/common/to_string.hpp>
#include <openvpn/common/platform_string.hpp>
#include <openvpn/common/options.hpp>
#include <openvpn/asio/asiosignal.hpp>
#include <openvpn/common/stop.hpp>
#include <openvpn/time/asiotimersafe.hpp>
#include <openvpn/omi/omi.hpp>

using namespace openvpn;

std::string log_version()
{
  return platform_string("OpenVPN Management Interface", OMI_VERSION)
    + " [" SSL_LIB_NAME "] built on " __DATE__ " " __TIME__;
}

class OMI;

class Client : public ClientAPI::OpenVPNClient
{
public:
  Client(OMI* omi)
    : parent(omi)
  {
  }

private:
  bool socket_protect(int socket, std::string remote, bool ipv6) override
  {
    return true;
  }

  virtual bool pause_on_connection_timeout() override
  {
    return false;
  }

  virtual void event(const ClientAPI::Event& ev) override;
  virtual void log(const ClientAPI::LogInfo& msg) override;
  virtual void external_pki_cert_request(ClientAPI::ExternalPKICertRequest& certreq) override;
  virtual void external_pki_sign_request(ClientAPI::ExternalPKISignRequest& signreq) override;

  OMI* parent;
};

class OMI : public OMICore, public ClientAPI::LogReceiver
{
public:
  typedef RCPtr<OMI> Ptr;

  OMI(openvpn_io::io_context& io_context, OptionList opt_arg)
    : OMICore(io_context),
      opt(std::move(opt_arg)),
      reconnect_timer(io_context),
      bytecount_timer(io_context),
      exit_event(io_context),
      log_context(this)
  {
    signals.reset(new ASIOSignals(io_context));
    signal_rearm();
  }

  void start()
  {
    log_setup(OMICore::LogFn(opt));

    OPENVPN_LOG(log_version());

    // command line options
    connection_timeout = opt.get_num<decltype(connection_timeout)>("connection-timeout", 1, 30);
    management_query_passwords = opt.exists("management-query-passwords");
    auth_nocache = opt.exists("auth-nocache");
    management_external_key = opt.exists("management-external-key");
    proto_override = opt.get_default("proto-force", 1, 16, "adaptive");
    remote_override = opt.get_optional("remote-override", 1, 256);
    management_up_down = opt.exists("management-up-down");
    management_query_remote = opt.exists("management-query-remote");
    exit_event_name = opt.get_optional("exit-event-name", 1, 256);

    // passed by OpenVPN GUI to trigger exit
    if (!exit_event_name.empty())
    {
        exit_event.assign(::CreateEvent(NULL, FALSE, FALSE, exit_event_name.c_str()));
        exit_event.async_wait([self = Ptr(this)](const openvpn_io::error_code& error)
			      {
				if (error)
				  return;
				self->stop();
			      });
    }

    // http-proxy-override
    {
      const Option* o = opt.get_ptr("http-proxy-override");
      if (o)
	{
	  http_proxy_host = o->get(1, 128);
	  http_proxy_port = o->get(2, 16);
	}
    }

    // begin listening/connecting on OMI port
    OMICore::start(opt);
  }

  virtual void log(const ClientAPI::LogInfo& msg) override
  {
    openvpn_io::post(io_context, [this, msg]() {
	log_msg(msg);
      });
  }

  void event(const ClientAPI::Event& ev)
  {
    openvpn_io::post(io_context, [this, ev]() {
	event_msg(ev, nullptr);
      });
  }

  void event(const ClientAPI::Event& ev, const ClientAPI::ConnectionInfo& ci)
  {
    openvpn_io::post(io_context, [this, ev, ci]() {
	event_msg(ev, &ci);
      });
  }

  void external_pki_cert_request(ClientAPI::ExternalPKICertRequest& certreq)
  {
    // not currently supported, <cert> must be in config
  }

  void external_pki_sign_request(ClientAPI::ExternalPKISignRequest& signreq)
  {
    try {
      // publish signreq to main thread
      {
	std::lock_guard<std::mutex> lock(epki_mutex);
	epki_signreq = &signreq;
      }

      // message main thread that signreq is published and pending
      openvpn_io::post(io_context, [this]() {
	  epki_sign_request();
	});

      // allow asynchronous stop
      Stop::Scope stop_scope(&async_stop, [this, &signreq]() {
	  {
	    std::lock_guard<std::mutex> lock(epki_mutex);
	    epki_signreq = nullptr;
	    signreq.error = true;
	    signreq.errorText = "External PKI OMI: stop";
	  }
	  epki_cv.notify_all();
	});

      // wait for main thread to signal readiness by nulling epki_signreq ptr
      {
	std::unique_lock<std::mutex> lock(epki_mutex);
	epki_cv.wait(lock, [this]() {
	    return epki_signreq == nullptr;
	  });
      }
    }
    catch (const std::exception& e)
      {
	std::lock_guard<std::mutex> lock(epki_mutex);
	epki_signreq = nullptr;
	signreq.error = true;
	signreq.errorText = std::string("External PKI OMI: ") + e.what();
      }
  }

  void epki_sign_request()
  {
    std::string sr;
    {
      std::lock_guard<std::mutex> lock(epki_mutex);
      if (epki_signreq)
	sr = epki_signreq->data;
    }
    send(">RSA_SIGN:" + sr + "\r\n");
  }

  void epki_sign_reply(const Command& cmd)
  {
    // get base64 signature from command
    std::string sig64;
    for (auto &line : cmd.extra)
      {
	sig64 += line;
      }

    // commit to connection thread
    bool fail = false;
    {
      std::lock_guard<std::mutex> lock(epki_mutex);
      if (epki_signreq)
	{
	  epki_signreq->sig = sig64;
	  epki_signreq = nullptr;
	}
      else
	fail = true;
    }
    if (fail)
      send("ERROR: unsolicited rsa-sig command\r\n");
    else
      {
	epki_cv.notify_all();
	send("SUCCESS: rsa-sig command succeeded\r\n");
      }
  }

  virtual bool omi_command_is_multiline(const std::string& arg0, const Option& o) override
  {
    if (arg0 == "rsa-sig")
      return true;
    return false;
  }

private:
  virtual bool omi_command_in(const std::string& arg0, const Command& cmd) override
  {
    switch (arg0.at(0))
      {
      case 'p':
	{
	  if (is_auth_cmd(arg0))
	    {
	      process_auth_cmd(cmd.option);
	      return false;
	    }
	  break;
	}
      case 'r':
	{
	  if (arg0 == "remote")
	    {
	      process_remote_cmd(cmd.option);
	      return false;
	    }
	  else if (arg0 == "rsa-sig")
	    {
	      epki_sign_reply(cmd);
	      return false;
	    }
	  break;
	}
      case 'u':
	{
	  if (is_auth_cmd(arg0))
	    {
	      process_auth_cmd(cmd.option);
	      return false;
	    }
	  break;
	}
      }
    send("ERROR: unknown command, enter 'help' for more options\r\n");
    return false;
  }

  virtual void omi_done(const bool eof) override
  {
    //OPENVPN_LOG("OMI DONE eof=" << eof);
  }

  std::vector<ClientAPI::KeyValue> get_peer_info() const
  {
    std::vector<ClientAPI::KeyValue> ret;
    OptionList::IndexMap::const_iterator se = opt.map().find("setenv");
    if (se != opt.map().end())
      {
	for (OptionList::IndexList::const_iterator i = se->second.begin(); i != se->second.end(); ++i)
	  {
	    const Option& o = opt[*i];
	    o.touch();
	    const std::string& k = o.get(1, 64);
	    if (string::starts_with(k, "IV_") || string::starts_with(k, "UV_"))
	      {
		const std::string& v = o.get(2, 256);
		ret.emplace_back(k, v);
	      }
	  }
      }
    return ret;
  }

  virtual void omi_start_connection() override
  {
    try {
      //OPENVPN_LOG("OMI START CONNECTION");

      // reset state
      reconnect_timer.cancel();
      reconnect_reason = "";

      if (!config)
	{
	  config.reset(new ClientAPI::Config);
	  config->guiVersion = "ovpnmi " OMI_VERSION;
	  config->content = get_config(opt);
	  config->peerInfo = get_peer_info();
	  config->connTimeout = connection_timeout;
	  config->protoOverride = proto_override;
	  config->serverOverride = remote_override;
	  config->tunPersist = true;
	  config->googleDnsFallback = true;
	  config->autologinSessions = true;
	  config->compressionMode = "yes";
	  config->proxyHost = http_proxy_host;
	  config->proxyPort = http_proxy_port;
	  config->echo = true;

	  if (management_external_key)
	    config->externalPkiAlias = "EPKI"; // dummy alias

	  did_query_remote = false;
	}

      const ClientAPI::EvalConfig eval = Client::eval_config_static(*config);
      if (eval.error)
	OPENVPN_THROW_EXCEPTION("eval config error: " << eval.message);

      autologin = eval.autologin;

      // for compatibility with openvpn2
      if (eval.windowsDriver == "wintun")
	config->wintun = true;

      if (!eval.autologin && management_query_passwords && !creds)
	query_username_password("Auth", !dc_cookie.empty(), eval.staticChallenge, eval.staticChallengeEcho);
      else if (proxy_need_creds)
	query_username_password("HTTP Proxy", false, "", false);
      else if (management_query_remote && !did_query_remote)
	query_remote(eval.remoteHost, eval.remotePort, eval.remoteProto);
      else
	start_connection_thread();
    }
    catch (const std::exception& e)
      {
	set_final_error(e.what());
	stop();
      }
  }

  void query_username_password(const std::string& type,
			       const bool password_only,
			       const std::string& static_challenge,
			       const bool static_challenge_echo)
  {
    reset_auth_cmd();
    auth_type = type;
    auth_password_only = password_only;

    std::string notify = ">PASSWORD:Need '" + type + "' ";
    if (password_only)
      notify += "password";
    else
      notify += "username/password";

    // static challenge
    if (!static_challenge.empty())
      {
	notify += " SC:";
	if (static_challenge_echo)
	  notify += '1';
	else
	  notify += '0';
	notify += ',';
	notify += static_challenge;
      }

    notify += "\r\n";
    send(notify);
  }

  bool is_auth_cmd(const std::string& arg0) const
  {
    return arg0 == "username" || arg0 == "password";
  }

  void process_auth_cmd(const Option& o)
  {
    const std::string up = o.get(0, 0);
    const std::string type = o.get(1, 64);
    const std::string cred = o.get(2, 512);
    if (auth_type.empty() || type != auth_type || (up == "username" && auth_password_only))
      {
	send("ERROR: no " + up + " is currently needed at this time\r\n");
	return;
      }
    bool changed = false;
    if (up == "username")
      {
	auth_username = cred;
	changed = true;
      }
    else if (up == "password")
      {
	auth_password = cred;
	changed = true;
      }
    if (changed)
      send("SUCCESS: '" + auth_type + "' " + up + " entered, but not yet verified\r\n");
    if ((!auth_username.empty() || auth_password_only) && !auth_password.empty())
      {
	provide_username_password(auth_type, auth_username, auth_password);
	reset_auth_cmd();
      }
  }

  void reset_auth_cmd()
  {
    auth_type = "";
    auth_password_only = false;
    auth_username = "";
    auth_password = "";
  }

  void provide_username_password(const std::string& type, const std::string& username, const std::string& password)
  {
    if (!dc_cookie.empty())
      {
	creds.reset(new ClientAPI::ProvideCreds);
	creds->dynamicChallengeCookie = dc_cookie;
	try {
	  // response could be whole challenge string in case of connect 2.x
	  ChallengeResponse cr{ auth_password };
	  creds->response = std::string{ cr.get_challenge_text() };
	}
	catch (const ChallengeResponse::dynamic_challenge_parse_error&) {
	  // response contains only challenge text
	  creds->response = auth_password;
	}
	creds->cachePassword = !auth_nocache;
	creds->replacePasswordWithSessionID = true;
      }
    else if (type == "Auth")
      {
	creds.reset(new ClientAPI::ProvideCreds);
	creds->username = username;
	creds->password = password;
	creds->replacePasswordWithSessionID = true;
	creds->cachePassword = !auth_nocache;
      }
    else if (type == "HTTP Proxy")
      {
	if (config)
	  {
	    config->proxyUsername = username;
	    config->proxyPassword = password;
	  }
	proxy_need_creds = false;
      }
    omi_start_connection();
  }

  void query_remote(const std::string& host, const std::string& port, const std::string& proto)
  {
    send(">REMOTE:" + host + ',' + port + ',' + proto + "\r\n");
    remote_pending = true;
  }

  void process_remote_cmd(const Option& o)
  {
    if (!remote_pending)
      {
	send("ERROR: no pending remote query\r\n");
	return;
      }

    std::string host;
    std::string port;
    bool mod = false;

    const std::string type = o.get(1, 16);
    if (type == "MOD")
      {
	host = o.get(2, 256);
	port = o.get_optional(3, 16);
	mod = true;
      }
    else if (type == "ACCEPT")
      {
	;
      }
    else
      {
	send("ERROR: remote type must be MOD or ACCEPT\r\n");
	return;
      }

    send("SUCCESS: remote command succeeded\r\n");
    remote_pending = false;

    if (mod && config)
      {
	config->serverOverride = host;
	// fixme -- override port
      }
    did_query_remote = true;
    omi_start_connection();
  }

  void schedule_bytecount_timer()
  {
    if (get_bytecount())
      {
	bytecount_timer.expires_after(Time::Duration::seconds(get_bytecount()));
	bytecount_timer.async_wait([self=Ptr(this)](const openvpn_io::error_code& error)
				   {
				     if (!error)
				       self->report_bytecount();
				   });
      }
    else
      bytecount_timer.cancel();
  }

  void report_bytecount()
  {
    if (client && get_bytecount())
      {
	const ClientAPI::TransportStats ts = client->transport_stats();
	send(">BYTECOUNT:" + openvpn::to_string(ts.bytesIn) + ',' + openvpn::to_string(ts.bytesOut) + "\r\n");
      }
    schedule_bytecount_timer();
  }

  void start_connection_thread()
  {
    try {
      // reset client instance
      client.reset(new Client(this));

      // evaluate config
      const ClientAPI::EvalConfig eval = client->eval_config(*config);
      if (eval.error)
	OPENVPN_THROW_EXCEPTION("eval config error: " << eval.message);

      // add credentials, if available
      if (creds)
	{
	  const ClientAPI::Status creds_status = client->provide_creds(*creds);
	  if (creds_status.error)
	    OPENVPN_THROW_EXCEPTION("creds error: " << creds_status.message);
	}

      // bytecount
      schedule_bytecount_timer();

      // start connection thread
      thread.reset(new std::thread([this]() {
	    connection_thread();
	  }));
    }
    catch (const std::exception& e)
      {
	set_final_error(e.what());
	stop();
      }
  }

  void connection_thread()
  {
    openvpn_io::detail::signal_blocker signal_blocker; // signals should be handled by parent thread
    std::string error;
    try {
      const ClientAPI::Status cs = client->connect();
      if (cs.error)
	{
	  error = "connect error: ";
	  if (!cs.status.empty())
	    {
	      error += cs.status;
	      error += ": ";
	    }
	  error += cs.message;
	}
    }
    catch (const std::exception& e)
      {
	error = "connect thread exception: ";
	error += e.what();
      }

    // generate an internal event for client exceptions
    if (!error.empty())
      {
	ClientAPI::Event ev;
	ev.error = true;
	ev.fatal = true;
	ev.name = "CLIENT_EXCEPTION";
	ev.info = error;
	event(ev);
      }
  }

  void join_thread()
  {
    try {
      if (thread)
	thread->join(); // may throw if thread has already exited
    }
    catch (const std::exception& e)
      {
      }
  }

  virtual bool omi_stop() override
  {
    bool ret = false;

    // in case connect thread is blocking in external_pki_sign_request
    async_stop.stop();

    // cancel wait on exit_event
    if (exit_event.is_open())
      exit_event.cancel();

    // stop timers
    reconnect_timer.cancel();
    bytecount_timer.cancel();

    // stop the client
    if (client)
      client->stop();

    // wait for client thread to exit
    join_thread();

    // if there's a final error, dump to management interface
    const std::string fe = get_final_error();
    if (!fe.empty())
      {
	send(string::add_trailing_crlf_copy(fe));
	if (is_errors_to_stderr())
	  std::cerr << fe << std::endl;
	OPENVPN_LOG_STRING(fe + '\n');
	ret = true;
      }

    // cancel signals
    if (signals)
      signals->cancel();

    return ret;
  }

  void retry()
  {
    // wait for client thread to exit
    join_thread();

    // restart connection
    omi_start_connection();
  }

  void deferred_reconnect(const unsigned int seconds, const std::string& reason)
  {
    reconnect_timer.expires_after(Time::Duration::seconds(seconds));
    reconnect_timer.async_wait([self=Ptr(this), reason](const openvpn_io::error_code& error)
			       {
				 if (!error)
				   {
				     self->state_line(gen_state_msg(false, "RECONNECTING", reason));
				     self->retry();
				   }
			       });
  }

  virtual void omi_sigterm() override
  {
    if (client)
      set_final_error(gen_state_msg(true, "EXITING", "exit-with-notification"));
    stop();
  }

  virtual bool omi_is_sighup_implemented() override
  {
    return true;
  }

  virtual void omi_sighup() override
  {
    if (client)
      client->reconnect(1);
  }

  void log_msg(const ClientAPI::LogInfo& msg)
  {
    log_full(msg.text);
  }

  static std::string event_format(const ClientAPI::Event& ev, const ClientAPI::ConnectionInfo* ci)
  {
    const time_t now = ::time(NULL);
    std::string evstr = openvpn::to_string(now) + ',' + ev.name;
    if (ev.name == "CONNECTED" && ci)
      evstr += ",SUCCESS," + ci->vpnIp4 + ',' + ci->serverIp + ',' + ci->serverPort + ",,," + ci->vpnIp6;
    else
      evstr += ',' + ev.info + ",,";
    evstr += "\r\n";
    return evstr;
  }

  static std::string gen_state_msg(const bool prefix, std::string name, std::string info)
  {
    ClientAPI::Event ev;
    ev.name = std::move(name);
    ev.info = std::move(info);
    std::string ret;
    if (prefix)
      ret = ">STATE:";
    ret += event_format(ev, nullptr);
    return ret;
  }

  void event_msg(const ClientAPI::Event& ev, const ClientAPI::ConnectionInfo* ci)
  {
    // log events (even if in stopping state)
    {
      ClientAPI::LogInfo li;
      li.text = ev.name;
      if (!ev.info.empty())
	{
	  li.text += " : ";
	  li.text += ev.info;
	}
      if (ev.fatal)
	li.text += " [FATAL-ERR]";
      else if (ev.error)
	li.text += " [ERR]";
      li.text += '\n';
      log_msg(li);
    }

    // if we are in a stopping state, don't process the event
    if (is_stopping())
      return;

    // process events
    if ((ev.name == "AUTH_FAILED" || ev.name == "DYNAMIC_CHALLENGE") && management_query_passwords)
      {
	if (ev.name == "DYNAMIC_CHALLENGE")
	  {
	    dc_cookie = ev.info;
	  }
	else
	  {
	    dc_cookie = "";
	  }

	// handle auth failures
	std::string msg = ">PASSWORD:Verification Failed: 'Auth'";
	if (!ev.info.empty())
	  msg += " ['" + ev.info + "']";
	msg += "\r\n";
	send(msg);

	// reset query state
	creds.reset();
	did_query_remote = false;

	// exit/reconnect
	if (autologin)
	  {
	    set_final_error(">FATAL: auth-failure: " + ev.info + "\r\n");
	    stop();
	  }
	else
	  deferred_reconnect(1, "auth-failure");
      }

    else if (ev.name == "CLIENT_HALT")
      {
	std::string reason = ev.info;
	if (reason.empty())
	  reason = "client was disconnected from server";
	send(">NOTIFY:info,server-pushed-halt," + reason + "\r\n");
	set_final_error(gen_state_msg(true, "EXITING", "exit-with-notification"));
	stop();
      }

    else if (ev.name == "CLIENT_RESTART")
      {
	// fixme -- handle PSID
	std::string reason = ev.info;
	if (reason.empty())
	  reason = "server requested a client reconnect";
	reconnect_reason = "server-pushed-connection-reset";
	send(">NOTIFY:info,"+ reconnect_reason + ',' + reason + "\r\n");
	omi_sighup();
      }

    else if (ev.name == "RECONNECTING")
      {
	ClientAPI::Event nev(ev);
	if (nev.info.empty())
	  nev.info = reconnect_reason;
	reconnect_reason = "";
	state_line(event_format(nev, nullptr));
      }

    else if (ev.name == "PROXY_NEED_CREDS" && management_query_passwords)
      {
	// need proxy credentials, retry
	proxy_need_creds = true;
	state_line(event_format(ev, nullptr));
	retry();
      }

    else if (ev.name == "DISCONNECTED")
      {
	// for now, we ignore DISCONNECTED messages
      }

    else if (ev.fatal)
      {
	// this event is a fatal error
	std::string reason = ev.name;
	if (!ev.info.empty())
	  {
	    reason += ": ";
	    reason += ev.info;
	  }
	set_final_error(">FATAL:" + reason + "\r\n");
	stop();
      }

    else if (ev.name == "ECHO")
      {
	echo_line(openvpn::to_string(::time(NULL)) + ',' + ev.info + "\r\n");
      }

    else if (ev.name == "CONNECTED")
      {
	// generate >UPDOWN: event
	if (management_up_down)
	  emit_up_down("UP");

	// reset pre-connection state
	creds.reset();
	reconnect_reason = "";

	// generate a TCP_CONNECT event if TCP connection
	if (ci && string::starts_with(ci->serverProto, "TCP"))
	  state_line(gen_state_msg(false, "TCP_CONNECT", ""));

	// push the event string to state notification/history
	state_line(event_format(ev, ci));
      }

    else
      {
	// push the event string to state notification/history
	state_line(event_format(ev, ci));
      }
  }

  void emit_up_down(const std::string& state)
  {
    std::string out = ">UPDOWN:" + state + "\r\n";
    out += ">UPDOWN:ENV,END\r\n";
    send(out);
  }

  void set_final_error(const std::string& err)
  {
    if (!err.empty())
      final_error = string::trim_crlf_copy(err);
  }

  std::string get_final_error()
  {
    return final_error;
  }

  void signal(const openvpn_io::error_code& error, int signum)
  {
    if (!error && !is_stopping())
      {
	OPENVPN_LOG("ASIO SIGNAL " << signum);
	switch (signum)
	  {
	  case SIGINT:
	  case SIGTERM:
	    omi_sigterm();
	    break;
#if !defined(OPENVPN_PLATFORM_WIN)
	  case SIGHUP:
	    omi_sighup();
	    signal_rearm();
	    break;
#endif
	  }
      }
  }

  void signal_rearm()
  {
    signals->register_signals_all([self=Ptr(this)](const openvpn_io::error_code& error, int signal_number)
				  {
				    self->signal(error, signal_number);
				  });
  }

  // options
  OptionList opt;

  // general
  std::unique_ptr<Client> client;
  std::unique_ptr<ClientAPI::Config> config;
  std::unique_ptr<ClientAPI::ProvideCreds> creds;
  std::unique_ptr<std::thread> thread;
  std::string final_error;
  Stop async_stop;

  // timeout
  int connection_timeout = 0;

  // auth
  bool management_query_passwords = false;
  bool auth_nocache = false;
  std::string auth_type;
  bool auth_password_only = false;
  std::string auth_username;
  std::string auth_password;
  std::string dc_cookie;

  // remote override
  bool management_query_remote = false;
  bool did_query_remote = false;
  bool remote_pending = false;
  std::string remote_override;

  // protocol override (udp/tcp)
  std::string proto_override;

  // proxy
  std::string http_proxy_host;
  std::string http_proxy_port;
  bool proxy_need_creds = false;

  // reconnections
  std::string reconnect_reason;

  // reconnect
  AsioTimerSafe reconnect_timer;

  // bytecount
  AsioTimerSafe bytecount_timer;

  // external PKI
  bool management_external_key = false;
  std::mutex epki_mutex;
  std::condition_variable epki_cv;
  ClientAPI::ExternalPKISignRequest* epki_signreq = nullptr; // protected by epki_mutex

  // up/down
  bool management_up_down = false;

  // autologin
  bool autologin = false;

  // signals
  ASIOSignals::Ptr signals;

  typedef openvpn_io::windows::object_handle AsioEvent;
  AsioEvent exit_event;
  std::string exit_event_name;

  Log::Context log_context; // should be initialized last
};

void Client::event(const ClientAPI::Event& ev)
{
  if (ev.name == "CONNECTED")
    {
      const ClientAPI::ConnectionInfo ci = connection_info();
      parent->event(ev, ci);
    }
  else
    parent->event(ev);
}

void Client::log(const ClientAPI::LogInfo& msg)
{
  parent->log(msg);
}

void Client::external_pki_cert_request(ClientAPI::ExternalPKICertRequest& certreq)
{
  parent->external_pki_cert_request(certreq);
}

void Client::external_pki_sign_request(ClientAPI::ExternalPKISignRequest& signreq)
{
  parent->external_pki_sign_request(signreq);
}

int run(OptionList opt)
{
  openvpn_io::io_context io_context(1);
  bool io_context_run_called = false;
  int ret = 0;
  OMI::Ptr omi;

  try {
#if _WIN32_WINNT >= 0x0600 // Vista+
    TunWin::NRPT::delete_rule(); // delete stale NRPT rules
#endif
    omi.reset(new OMI(io_context, std::move(opt)));
    omi->start();
    io_context_run_called = true;
    io_context.run();
    omi->stop();
  }
  catch (const std::exception& e)
    {
      if (omi)
	omi->stop();
      if (io_context_run_called)
	io_context.poll(); // execute completion handlers,
      std::cerr << "openvpn: run loop exception: " << e.what() << std::endl;
      ret = 1;
    }
  return ret;
}

int main(int argc, char *argv[])
{
  int ret = 0;

  try {
    if (argc >= 2)
      {
	ret = run(OptionList::parse_from_argv_static(string::from_argv(argc, argv, true)));
      }
    else
      {
	std::cout << log_version() << std::endl;
	std::cout << "Usage: openvpn [args...]" << std::endl;
	ret = 2;
      }
  }
  catch (const std::exception& e)
    {
      std::cerr << "openvpn: " << e.what() << std::endl;
      ret = 1;
    }
  return ret;
}