diff --git a/.github/workflows/ci-scripts-build.yml b/.github/workflows/ci-scripts-build.yml index aa07a1b2..1e9e6608 100644 --- a/.github/workflows/ci-scripts-build.yml +++ b/.github/workflows/ci-scripts-build.yml @@ -145,6 +145,12 @@ jobs: sudo apt-get update sudo apt-get -y install g++-mingw-w64-x86-64 cmake gdb qemu-system-x86 libssl-dev if: runner.os == 'Linux' + - name: Setup python + uses: actions/setup-python@v4 + with: + python-version: "3.11" + - name: Python deps + run: pip install pyOpenSSL - name: Host Info run: openssl version -a - name: Automatic core dumper analysis @@ -158,7 +164,8 @@ jobs: - name: Generate Docs if: matrix.doc run: | - sudo apt-get -y install doxygen python-is-python3 python3-breathe + sudo apt-get -y install doxygen + pip install breathe Sphinx make -C documentation - name: otool if: ${{ always() && runner.os == 'macOS' }} diff --git a/ioc/iochooks.cpp b/ioc/iochooks.cpp index 0feeb592..a1cefdb4 100644 --- a/ioc/iochooks.cpp +++ b/ioc/iochooks.cpp @@ -202,6 +202,17 @@ void pvxrefdiff() { } } +void pvxreconfigure() +{ + if (auto pPvxsServer = pvxsServer.load()) { + printf("Reconfiguring QSRV\n"); + pPvxsServer->reconfigure(server::Config::from_env()); + pvxsr(0); // print new configuration + } else { + fprintf(stderr, "Warning: QSRV not running\n"); + } +} + } // namespace /** @@ -302,6 +313,9 @@ void pvxsBaseRegistrar() { "Save the current set of instance counters for reference by later pvxrefdiff.\n").implementation<&pvxrefsave>(); IOCShCommand<>("pvxrefdiff", "Show different of current instance counts with those when pvxrefsave was called.\n").implementation<&pvxrefdiff>(); + IOCShCommand<>("pvxreconfigure", + "Reconfigure QSRV using current values of EPICS_PVA*\n") + .implementation<&pvxreconfigure>(); // Initialise the PVXS Server initialisePvxsServer(); diff --git a/src/Makefile b/src/Makefile index 7dc2a03e..ad43943a 100644 --- a/src/Makefile +++ b/src/Makefile @@ -15,6 +15,12 @@ ifeq (YES,$(EVENT2_HAS_OPENSSL)) USR_CPPFLAGS += -DPVXS_ENABLE_OPENSSL endif +# set to NO to disable handling of $SSLKEYLOGFILE +PVXS_ENABLE_SSLKEYLOGFILE ?= YES + +PVXS_ENABLE_SSLKEYLOGFILE_YES = -DPVXS_ENABLE_SSLKEYLOGFILE +USR_CPPFLAGS += $(PVXS_ENABLE_SSLKEYLOGFILE_$(PVXS_ENABLE_SSLKEYLOGFILE)) + ifdef T_A ifneq ($(CONFIG_LOADED),YES) $(error Toolchain inspection failed $(MAKEFILE_LIST)) @@ -111,6 +117,10 @@ LIB_SRCS += clientget.cpp LIB_SRCS += clientmon.cpp LIB_SRCS += clientdiscover.cpp +ifeq (YES,$(EVENT2_HAS_OPENSSL)) +LIB_SRCS += ossl.cpp +endif + LIB_LIBS += Com # special case matching configure/RULES_PVXS_MODULE diff --git a/src/client.cpp b/src/client.cpp index 83db0713..3b102d89 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -87,10 +87,13 @@ RemoteError::~RemoteError() {} Finished::~Finished() {} -Connected::Connected(const std::string& peerName) +Connected::Connected(const std::string& peerName, + const epicsTime& time, + const std::shared_ptr &cred) :std::runtime_error("Connected") ,peerName(peerName) - ,time(epicsTime::getCurrent()) + ,time(time) + ,cred(cred) {} Connected::~Connected() {} @@ -216,7 +219,7 @@ void Channel::disconnect(const std::shared_ptr& self) name.c_str()); } else if(context->state==ContextImpl::Running) { // reconnect to specific server - conn = Connection::build(context, forcedServer, true); + conn = Connection::build(context, forcedServer, true, false); // TODO: how to TLS? conn->pending[cid] = self; state = Connecting; @@ -273,10 +276,13 @@ std::shared_ptr ConnectBuilder::exec() op->chan = Channel::build(context, op->_name, server); bool cur = op->_connected = op->chan->state==Channel::Active; - if(cur && op->_onConn) - op->_onConn(); - else if(!cur && op->_onDis) + if(cur && op->_onConn) { + auto& conn = op->chan->conn; + Connected evt(conn->peerName, conn->connTime, conn->cred); + op->_onConn(evt); + } else if(!cur && op->_onDis) { op->_onDis(); + } op->chan->connectors.push_back(op.get()); }); @@ -380,7 +386,7 @@ std::shared_ptr Channel::build(const std::shared_ptr& cont } else { // bypass search and connect so a specific server chan->forcedServer = forceServer; - chan->conn = Connection::build(context, forceServer); + chan->conn = Connection::build(context, forceServer, false, false); // TODO: how to TLS? chan->conn->pending[chan->cid] = chan; chan->state = Connecting; @@ -410,6 +416,45 @@ Context::Context(const Config& conf) Context::~Context() {} +void Context::reconfigure(const Config& newconf) +{ + if(!pvt) + throw std::logic_error("NULL Context"); + +#ifdef PVXS_ENABLE_OPENSSL + + ossl::SSLContext new_context; + if(!newconf.tls_keychain_file.empty() + || !newconf.tls_authority_files.empty() + || !newconf.tls_authority_dirs.empty()) + { + new_context = ossl::SSLContext::for_client(newconf); + } + + pvt->impl->manager.loop().call([this, &new_context](){ + + log_debug_printf(setup, "Client reconfigure%s", "\n"); + + auto conns(std::move(pvt->impl->connByAddr)); + + for(auto& pair : conns) { + auto conn = pair.second.lock(); + if(!conn) + continue; + + conn->cleanup(); + } + + conns.clear(); + + pvt->impl->tls_context = new_context; + }); + +#else + pvt->impl->manager.loop().sync(); +#endif +} + const Config& Context::config() const { if(!pvt) @@ -542,6 +587,15 @@ ContextImpl::ContextImpl(const Config& conf, const evbase& tcp_loop) ,nsChecker(__FILE__, __LINE__, event_new(tcp_loop.base, -1, EV_TIMEOUT|EV_PERSIST, &ContextImpl::onNSCheckS, this)) { +#ifdef PVXS_ENABLE_OPENSSL + if(!effective.tls_keychain_file.empty() + || !effective.tls_authority_files.empty() + || !effective.tls_authority_dirs.empty()) + { + tls_context = ossl::SSLContext::for_client(effective); + } +#endif + searchBuckets.resize(nBuckets); std::set bcasts; @@ -654,9 +708,10 @@ void ContextImpl::startNS() // start connections to name servers for(auto& ns : nameServers) { const auto& serv = ns.first; - ns.second = Connection::build(shared_from_this(), serv); + ns.second = Connection::build(shared_from_this(), serv, false, false); // TODO: how to TLS? ns.second->nameserver = true; - log_debug_printf(io, "Connecting to nameserver %s\n", ns.second->peerName.c_str()); + log_debug_printf(io, "Connecting to nameserver %s%s\n", + ns.second->peerName.c_str(), ns.second->isTLS ? " TLS" : ""); } if(event_add(nsChecker.get(), &tcpNSCheckInterval)) @@ -873,7 +928,16 @@ void procSearchReply(ContextImpl& self, const SockAddr& src, uint8_t peerVersion self.onBeacon(fakebeacon); } - if(!found || proto!="tcp") + bool isTCP = proto=="tcp"; + +#ifdef PVXS_ENABLE_OPENSSL + bool isTLS = proto=="tls"; + if(!self.tls_context && isTLS) + return; +#else + const bool isTLS = false; +#endif + if(!found || !(isTCP || isTLS)) return; for(auto n : range(nSearch)) { @@ -901,7 +965,7 @@ void procSearchReply(ContextImpl& self, const SockAddr& src, uint8_t peerVersion chan->guid = guid; chan->replyAddr = serv; - chan->conn = Connection::build(self.shared_from_this(), serv); + chan->conn = Connection::build(self.shared_from_this(), serv, false, isTLS); chan->conn->pending[chan->cid] = chan; chan->state = Channel::Connecting; @@ -1061,6 +1125,13 @@ void ContextImpl::tickSearch(SearchKind kind, bool poked) if(kind == SearchKind::discover) { to_wire(M, uint8_t(0u)); +#ifdef PVXS_ENABLE_OPENSSL + } else if(tls_context) { + to_wire(M, uint8_t(2u)); + to_wire(M, "tls"); + to_wire(M, "tcp"); +#endif + } else { to_wire(M, uint8_t(1u)); to_wire(M, "tcp"); @@ -1286,7 +1357,7 @@ void ContextImpl::onNSCheck() if(ns.second && ns.second->state != ConnBase::Disconnected) // hold-off, connecting, or connected continue; - ns.second = Connection::build(shared_from_this(), ns.first); + ns.second = Connection::build(shared_from_this(), ns.first, false, false); // How to TLS? ns.second->nameserver = true; log_debug_printf(io, "Reconnecting nameserver %s\n", ns.second->peerName.c_str()); } diff --git a/src/clientconn.cpp b/src/clientconn.cpp index aaf9fb7c..8c68f276 100644 --- a/src/clientconn.cpp +++ b/src/clientconn.cpp @@ -18,11 +18,13 @@ DEFINE_LOGGER(remote, "pvxs.remote.log"); Connection::Connection(const std::shared_ptr& context, const SockAddr& peerAddr, - bool reconn) + bool reconn, + bool isTLS) :ConnBase (true, context->effective.sendBE(), nullptr, peerAddr) ,context(context) + ,isTLS(isTLS) ,echoTimer(__FILE__, __LINE__, event_new(context->tcp_loop.base, -1, EV_TIMEOUT|EV_PERSIST, &tickEchoS, this)) { @@ -45,15 +47,16 @@ Connection::~Connection() } std::shared_ptr Connection::build(const std::shared_ptr& context, - const SockAddr& serv, bool reconn) + const SockAddr& serv, bool reconn, bool tls) { if(context->state!=ContextImpl::Running) throw std::logic_error("Context close()d"); + auto pair(std::make_pair(serv, tls)); std::shared_ptr ret; - auto it = context->connByAddr.find(serv); + auto it = context->connByAddr.find(pair); if(it==context->connByAddr.end() || !(ret = it->second.lock())) { - context->connByAddr[serv] = ret = std::make_shared(context, serv, reconn); + context->connByAddr[pair] = ret = std::make_shared(context, serv, reconn, tls); } return ret; } @@ -62,19 +65,46 @@ void Connection::startConnecting() { assert(!this->bev); - auto bev(bufferevent_socket_new(context->tcp_loop.base, -1, BEV_OPT_CLOSE_ON_FREE|BEV_OPT_DEFER_CALLBACKS)); + evbufferevent bev; - bufferevent_setcb(bev, &bevReadS, nullptr, &bevEventS, this); +#ifdef PVXS_ENABLE_OPENSSL + if(isTLS) { + auto ctx(SSL_new(context->tls_context.ctx)); + if(!ctx) + throw ossl::SSLError("SSL_new"); + + // w/ BEV_OPT_CLOSE_ON_FREE calls SSL_free() on error + bev.reset(bufferevent_openssl_socket_new(context->tcp_loop.base, + -1, + ctx, + BUFFEREVENT_SSL_CONNECTING, + BEV_OPT_CLOSE_ON_FREE|BEV_OPT_DEFER_CALLBACKS)); + + // added with libevent 2.2.1-alpha + //(void)bufferevent_ssl_set_flags(bev.get(), BUFFEREVENT_SSL_DIRTY_SHUTDOWN); + // deprecated, but not yet removed + bufferevent_openssl_set_allow_dirty_shutdown(bev.get(), 1); + + } else +#endif + { + bev.reset(bufferevent_socket_new(context->tcp_loop.base, + -1, + BEV_OPT_CLOSE_ON_FREE|BEV_OPT_DEFER_CALLBACKS)); + } + + bufferevent_setcb(bev.get(), &bevReadS, nullptr, &bevEventS, this); timeval tmo(totv(context->effective.tcpTimeout)); - bufferevent_set_timeouts(bev, &tmo, &tmo); + bufferevent_set_timeouts(bev.get(), &tmo, &tmo); - if(bufferevent_socket_connect(bev, const_cast(&peerAddr->sa), peerAddr.size())) + if(bufferevent_socket_connect(bev.get(), const_cast(&peerAddr->sa), peerAddr.size())) throw std::runtime_error("Unable to begin connecting"); - connect(bev); + connect(std::move(bev)); - log_debug_printf(io, "Connecting to %s, RX readahead %zu\n", peerName.c_str(), readahead); + log_debug_printf(io, "Connecting to %s, RX readahead %zu%s\n", + peerName.c_str(), readahead, isTLS ? " TLS" : ""); } void Connection::createChannels() @@ -128,11 +158,47 @@ void Connection::sendDestroyRequest(uint32_t sid, uint32_t ioid) void Connection::bevEvent(short events) { +#ifdef PVXS_ENABLE_OPENSSL + if((events & BEV_EVENT_ERROR) && isTLS && bev) { + while(auto err = bufferevent_get_openssl_error(bev.get())) { + log_err_printf(io, "TLS Error (0x%lx) %s\n", + err, ERR_reason_error_string(err)); + } + } +#endif ConnBase::bevEvent(events); // called Connection::cleanup() if(bev && (events&BEV_EVENT_CONNECTED)) { log_debug_printf(io, "Connected to %s\n", peerName.c_str()); + connTime = epicsTime::getCurrent(); + + auto peerCred(std::make_shared()); + peerCred->peer = peerName; + peerCred->isTLS = isTLS; + +#ifdef PVXS_ENABLE_OPENSSL + if(isTLS) { + auto ctx = bufferevent_openssl_get_ssl(bev.get()); + assert(ctx); + auto cert = SSL_get0_peer_certificate(ctx); + // SSL_get0_verified_chain() + if(cert) { + auto subj = X509_get_subject_name(cert); + char name[64]; + if(subj && X509_NAME_get_text_by_NID(subj, NID_commonName, name, sizeof(name)-1)) { + name[sizeof(name)-1] = '\0'; + log_debug_printf(io, "Peer CN=%s\n", name); + peerCred->method = "x509"; + peerCred->account = name; + } + } + } else +#endif + { + peerCred->method = "anonymous"; + } + cred = std::move(peerCred); if(bufferevent_enable(bev.get(), EV_READ|EV_WRITE)) throw std::logic_error("Unable to enable BEV"); @@ -157,7 +223,7 @@ void Connection::cleanup() { ready = false; - context->connByAddr.erase(peerAddr); + context->connByAddr.erase(std::make_pair(peerAddr, isTLS)); if(bev) bev.reset(); @@ -220,6 +286,10 @@ void Connection::handle_CONNECTION_VALIDATION() if(method=="ca" || (method=="anonymous" && selected!="ca")) selected = method; +#ifdef PVXS_ENABLE_OPENSSL + else if(isTLS && method=="x509" && context->tls_context.have_certificate()) + selected = method; +#endif } if(!M.good()) { @@ -383,9 +453,10 @@ void Connection::handle_CREATE_CHANNEL() auto conns(chan->connectors); // copy list + struct Connected connEvt(peerName, connTime, cred); for(auto& conn : conns) { if(!conn->_connected.exchange(true, std::memory_order_relaxed) && conn->_onConn) - conn->_onConn(); + conn->_onConn(connEvt); } } } diff --git a/src/clientimpl.h b/src/clientimpl.h index d9101919..cd13e630 100644 --- a/src/clientimpl.h +++ b/src/clientimpl.h @@ -83,6 +83,7 @@ struct RequestInfo { struct Connection final : public ConnBase, public std::enable_shared_from_this { const std::shared_ptr context; + const bool isTLS; // While HoldOff, the time until re-connection // While Connected, periodic Echo @@ -102,17 +103,21 @@ struct Connection final : public ConnBase, public std::enable_shared_from_this cred; + INST_COUNTER(Connection); Connection(const std::shared_ptr& context, const SockAddr &peerAddr, - bool reconn); + bool reconn, bool isTLS); virtual ~Connection(); static std::shared_ptr build(const std::shared_ptr& context, const SockAddr& serv, - bool reconn=false); + bool reconn, + bool isTLS); private: void startConnecting(); @@ -157,7 +162,7 @@ struct ConnectImpl final : public Connect std::shared_ptr chan; const std::string _name; std::atomic _connected; - std::function _onConn; + std::function _onConn; std::function _onDis; ConnectImpl(const evbase& loop, const std::string& name) @@ -304,7 +309,8 @@ struct ContextImpl : public std::enable_shared_from_this // chanByName key'd by (pv, forceServer) std::map, std::shared_ptr> chanByName; - std::map> connByAddr; + // pair (addr, useTLS) + std::map, std::weak_ptr> connByAddr; std::vector>> nameServers; @@ -323,6 +329,10 @@ struct ContextImpl : public std::enable_shared_from_this const evevent cacheCleaner; const evevent nsChecker; +#ifdef PVXS_ENABLE_OPENSSL + ossl::SSLContext tls_context; +#endif + INST_COUNTER(ClientContextImpl); ContextImpl(const Config& conf, const evbase &tcp_loop); diff --git a/src/clientmon.cpp b/src/clientmon.cpp index 8bf249da..9fed1ca5 100644 --- a/src/clientmon.cpp +++ b/src/clientmon.cpp @@ -359,7 +359,9 @@ struct SubscriptionImpl final : public OperationBase, public Subscription if(!maskConn) { notify = queue.empty() && wantToNotify(); - queue.emplace_back(std::make_exception_ptr(Connected(conn->peerName))); + queue.emplace_back(std::make_exception_ptr(Connected(conn->peerName, + conn->connTime, + conn->cred))); log_debug_printf(io, "Server %s channel %s monitor PUSH Connected\n", chan->conn ? chan->conn->peerName.c_str() : "", diff --git a/src/config.cpp b/src/config.cpp index 80ce6bee..b81a2128 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -29,6 +29,10 @@ DEFINE_LOGGER(config, "pvxs.config"); namespace pvxs { +namespace impl { +ConfigCommon::~ConfigCommon() {} +} // namespace impl + SockEndpoint::SockEndpoint(const char* ep, uint16_t defport) { // @@ -148,38 +152,79 @@ namespace { */ constexpr double tmoScale = 4.0/3.0; // 40 second idle timeout / 30 configured -void split_addr_into(const char* name, std::vector& out, const std::string& inp, - uint16_t defaultPort, bool required=false) +// remove duplicates while preserving order of first appearance +template +void removeDups(std::vector& addrs) +{ + std::sort(addrs.begin(), addrs.end()); + addrs.erase(std::unique(addrs.begin(), addrs.end()), + addrs.end()); +} + +// special handling for SockEndpoint where duplication is based on +// address,interface. Duplicates are combined with the longest TTL. +template<> +void removeDups(std::vector& addrs) +{ + std::map, size_t> seen; + for(size_t i=0; isecond]; + + if(ep.ttl > orig.ttl) { // w/ longer TTL + orig.ttl = ep.ttl; + } + + addrs.erase(addrs.begin()+i); + // 'ep' and 'orig' are invalidated + } + } +} + +void split_into(std::vector& out, const std::string& inp) { size_t pos=0u; - // parse, resolve host names, then re-print. - // Catch syntax errors early, and normalize prior to removing duplicates while(pos& out, const std::string& inp, + uint16_t defaultPort, bool required=false) +{ + std::vector raw; + split_into(raw, inp); + + // parse, resolve host names, then re-print. + // Catch syntax errors early, and normalize prior to removing duplicates + for(auto& temp : raw) { + try { + SockEndpoint ep(temp); + if(ep.addr.port()==0) + ep.addr.setPort(defaultPort); + out.push_back(SB()<& in) @@ -334,41 +379,6 @@ void addGroups(std::vector& ifaces, } } -// remove duplicates while preserving order of first appearance -template -void removeDups(std::vector& addrs) -{ - std::sort(addrs.begin(), addrs.end()); - addrs.erase(std::unique(addrs.begin(), addrs.end()), - addrs.end()); -} - -// special handling for SockEndpoint where duplication is based on -// address,interface. Duplicates are combined with the longest TTL. -template<> -void removeDups(std::vector& addrs) -{ - std::map, size_t> seen; - for(size_t i=0; isecond]; - - if(ep.ttl > orig.ttl) { // w/ longer TTL - orig.ttl = ep.ttl; - } - - addrs.erase(addrs.begin()+i); - // 'ep' and 'orig' are invalidated - } - } -} - void enforceTimeout(double& tmo) { /* Inactivity timeouts with PVA have a long (and growing) history. @@ -388,7 +398,6 @@ void enforceTimeout(double& tmo) else if(tmo < 2.0) tmo = 2.0; } - } // namespace namespace server { @@ -398,6 +407,18 @@ void _fromDefs(Config& self, const std::map& defs, boo { PickOne pickone{defs, useenv}; + if(pickone({"EPICS_PVAS_TLS_KEYCHAIN", "EPICS_PVA_TLS_KEYCHAIN"})) { + self.tls_keychain_file = pickone.val; + } + + if(pickone({"EPICS_PVAS_CA_FILES", "EPICS_PVA_CA_FILES"})) { + split_into(self.tls_authority_files, pickone.val); + } + + if(pickone({"EPICS_PVAS_CA_DIRS", "EPICS_PVA_CA_DIRS"})) { + split_into(self.tls_authority_dirs, pickone.val); + } + if(pickone({"EPICS_PVAS_SERVER_PORT", "EPICS_PVA_SERVER_PORT"})) { try { self.tcp_port = parseTo(pickone.val); @@ -406,6 +427,14 @@ void _fromDefs(Config& self, const std::map& defs, boo } } + if(pickone({"EPICS_PVAS_TLS_PORT", "EPICS_PVA_TLS_PORT"})) { + try { + self.tls_port = parseTo(pickone.val); + }catch(std::exception& e) { + log_err_printf(serversetup, "%s invalid integer : %s", pickone.name.c_str(), e.what()); + } + } + if(pickone({"EPICS_PVAS_BROADCAST_PORT", "EPICS_PVA_BROADCAST_PORT"})) { try { self.udp_port = parseTo(pickone.val); @@ -472,6 +501,9 @@ Config& Config::applyDefs(const std::map& defs) void Config::updateDefs(defs_t& defs) const { + defs["EPICS_PVAS_TLS_KEYCHAIN"] = defs["EPICS_PVA_TLS_KEYCHAIN"] = SB()<& defs, boo { PickOne pickone{defs, useenv}; + if(pickone({"EPICS_PVA_TLS_KEYCHAIN"})) { + self.tls_keychain_file = pickone.val; + } + + if(pickone({"EPICS_PVA_CA_FILES"})) { + split_into(self.tls_authority_files, pickone.val); + } + + if(pickone({"EPICS_PVA_CA_DIRS"})) { + split_into(self.tls_authority_dirs, pickone.val); + } + if(pickone({"EPICS_PVA_BROADCAST_PORT"})) { try { self.udp_port = parseTo(pickone.val); @@ -611,6 +655,9 @@ Config& Config::applyDefs(const std::map& defs) void Config::updateDefs(defs_t& defs) const { + defs["EPICS_PVA_TLS_KEYCHAIN"] = SB()<bev && state==Holdoff); - this->bev.reset(bev); + this->bev = std::move(bev); - readahead = evsocket::get_buffer_size(bufferevent_getfd(bev), false); + readahead = evsocket::get_buffer_size(bufferevent_getfd(this->bev.get()), false); #if LIBEVENT_VERSION_NUMBER >= 0x02010000 // allow to drain OS socket buffer in a single read - (void)bufferevent_set_max_single_read(bev, readahead); + (void)bufferevent_set_max_single_read(this->bev.get(), readahead); #endif readahead *= tcp_readahead_mult; #if LIBEVENT_VERSION_NUMBER >= 0x02010000 // allow attempt to write as much as is available - (void)bufferevent_set_max_single_write(bev, EV_SSIZE_MAX); + (void)bufferevent_set_max_single_write(this->bev.get(), EV_SSIZE_MAX); #endif state = isClient ? Connecting : Connected; diff --git a/src/conn.h b/src/conn.h index b4ba7159..f2d9c0f4 100644 --- a/src/conn.h +++ b/src/conn.h @@ -50,7 +50,7 @@ struct ConnBase Disconnected, } state; - ConnBase(bool isClient, bool sendBE, bufferevent* bev, const SockAddr& peerAddr); + ConnBase(bool isClient, bool sendBE, evbufferevent &&bev, const SockAddr& peerAddr); ConnBase(const ConnBase&) = delete; ConnBase& operator=(const ConnBase&) = delete; virtual ~ConnBase(); @@ -61,7 +61,7 @@ struct ConnBase bufferevent* connection() { return bev.get(); } - void connect(bufferevent* bev); + void connect(evbufferevent &&bev); void disconnect(); protected: diff --git a/src/describe.cpp b/src/describe.cpp index 66bd96ce..980a837b 100644 --- a/src/describe.cpp +++ b/src/describe.cpp @@ -137,6 +137,11 @@ std::ostream& version_information(std::ostream& strm) strm< #include +#ifdef PVXS_ENABLE_OPENSSL +# include +#endif + #include #include #include #include "pvaproto.h" +#ifdef PVXS_ENABLE_OPENSSL +# include "ossl.h" +#endif // hooks for std::unique_ptr namespace std { @@ -61,10 +68,27 @@ template struct owned_ptr : public std::unique_ptr { constexpr owned_ptr() {} + constexpr owned_ptr(std::nullptr_t np) : std::unique_ptr(np) {} explicit owned_ptr(const char* file, int line, T* ptr) : std::unique_ptr(ptr) { if(!*this) throw loc_bad_alloc(file, line); } + + // for functions which return a pointer in an argument + // int some(T** presult); // store *presult = output + // use like + // owned_ptr x; + // some(x.acquire()); + struct acquisition { + owned_ptr* o; + T* ptr = nullptr; + operator T** () { return &ptr; } + constexpr acquisition(owned_ptr* o) :o(o) {} + ~acquisition() { + o->reset(ptr); + } + }; + acquisition acquire() { return acquisition{this}; } }; /* It seems that std::function(Fn&&) from gcc (circa 8.3) and clang (circa 7.0) diff --git a/src/ossl.cpp b/src/ossl.cpp new file mode 100644 index 00000000..4c2d08cf --- /dev/null +++ b/src/ossl.cpp @@ -0,0 +1,433 @@ +/** + * Copyright - See the COPYRIGHT that is included with this distribution. + * pvxs is distributed subject to a Software License Agreement found + * in file LICENSE that is included with this distribution. + */ + +#include +#include + +#include "ossl.h" +#include +#include +#include +#include + +#include +#include "evhelper.h" +#include "utilpvt.h" + +#include + +#ifndef TLS1_3_VERSION +# error TLS 1.3 support required. Upgrade to openssl >= 1.1.0 +#endif + +DEFINE_LOGGER(_setup, "pvxs.ossl.setup"); +DEFINE_LOGGER(_io, "pvxs.ossl.io"); + +namespace std { +template<> +struct default_delete { + inline void operator()(OSSL_LIB_CTX* fp) { if(fp) OSSL_LIB_CTX_free(fp); } +}; +template<> +struct default_delete { + inline void operator()(BIO* fp) { if(fp) BIO_free(fp); } +}; +template<> +struct default_delete { + inline void operator()(PKCS12* fp) { if(fp) PKCS12_free(fp); } +}; +template<> +struct default_delete { + inline void operator()(EVP_PKEY* fp) { if(fp) EVP_PKEY_free(fp); } +}; +template<> +struct default_delete { + inline void operator()(X509* fp) { if(fp) X509_free(fp); } +}; +template<> +struct default_delete { + inline void operator()(STACK_OF(X509)* fp) { if(fp) sk_X509_free(fp); } +}; +} // namespace std + +namespace pvxs { +namespace ossl { + +namespace { + +constexpr int ossl_verify_depth = 5; + +// see NOTE in "man SSL_CTX_set_alpn_protos" +const unsigned char pva_alpn[] = "\x05pva/1"; + +struct OSSLGbl { + owned_ptr libctx; + int SSL_CTX_ex_idx; +#ifdef PVXS_ENABLE_SSLKEYLOGFILE + std::ofstream keylog; + epicsMutex keylock; +#endif +} *ossl_gbl; + +epicsThreadOnceId OSSLGbl_once = EPICS_THREAD_ONCE_INIT; + +#ifdef PVXS_ENABLE_SSLKEYLOGFILE +void sslkeylogfile_exit(void*) noexcept +{ + auto gbl = ossl_gbl; + try { + epicsGuard G(gbl->keylock); + if(gbl->keylog.is_open()) { + gbl->keylog.flush(); + gbl->keylog.close(); + } + }catch(std::exception& e){ + static bool once = false; + if(!once) { + fprintf(stderr, "Error while writing to SSLKEYLOGFILE\n"); + once = true; + } + } +} + +void sslkeylogfile_log(const SSL*, const char *line) noexcept +{ + auto gbl = ossl_gbl; + try { + epicsGuard G(gbl->keylock); + if(gbl->keylog.is_open()) { + gbl->keylog << line << '\n'; + gbl->keylog.flush(); + } + }catch(std::exception& e){ + static bool once = false; + if(!once) { + fprintf(stderr, "Error while writing to SSLKEYLOGFILE\n"); + once = true; + } + } +} +#endif // PVXS_ENABLE_SSLKEYLOGFILE + +struct SSL_CTX_sidecar { + std::unique_ptr cert; +}; + +void free_SSL_CTX_sidecar(void *parent, void *ptr, CRYPTO_EX_DATA *ad, + int idx, long argl, void *argp) noexcept +{ + auto car = static_cast(ptr); + delete car; +} + +void OSSLGbl_init(void*) +{ + owned_ptr ctx(__FILE__, __LINE__, OSSL_LIB_CTX_new()); + // read $OPENSSL_CONF or eg. /usr/lib/ssl/openssl.cnf + (void)CONF_modules_load_file_ex(ctx.get(), NULL, "pvxs", + CONF_MFLAGS_IGNORE_MISSING_FILE + |CONF_MFLAGS_IGNORE_RETURN_CODES); + std::unique_ptr gbl{new OSSLGbl}; + gbl->SSL_CTX_ex_idx = SSL_CTX_get_ex_new_index(0, nullptr, + nullptr, + nullptr, + free_SSL_CTX_sidecar); +#ifdef PVXS_ENABLE_SSLKEYLOGFILE + if(auto env = getenv("SSLKEYLOGFILE")) { + epicsGuard G(gbl->keylock); + gbl->keylog.open(env); + if(gbl->keylog.is_open()) { + epicsAtExit(sslkeylogfile_exit, nullptr); + fprintf(stderr, "NOTICE: debug logging TLS SECRETS to SSLKEYLOGFILE=%s\n", env); + } else { + fprintf(stderr, "Warning: Unable to open. SSLKEYLOGFILE disabled : %s\n", env); + } + } +#endif // PVXS_ENABLE_SSLKEYLOGFILE + ossl_gbl = gbl.release(); +} + +int ossl_verify(int preverify_ok, X509_STORE_CTX *x509_ctx) +{ + // note: no context pointer passed directly. If needed see: man SSL_CTX_set_verify + if(!preverify_ok) { +// X509_STORE_CTX_print_verify_cb(preverify_ok, x509_ctx); + auto err = X509_STORE_CTX_get_error(x509_ctx); + auto cert = X509_STORE_CTX_get_current_cert(x509_ctx); + log_err_printf(_io, "Unable to verify peer cert %s : %s\n", + std::string(SB()<libctx.get(), NULL, method); + if(!ctx.ctx) + throw SSLError("Unable to allocate SSL_CTX"); + + { + std::unique_ptr car{new SSL_CTX_sidecar}; + if(!SSL_CTX_set_ex_data(ctx.ctx, ossl_gbl->SSL_CTX_ex_idx, car.get())) + throw SSLError("SSL_CTX_set_ex_data"); + car.release(); // SSL_CTX_free() now responsible + } + +#ifdef PVXS_ENABLE_SSLKEYLOGFILE +// assert(!SSL_CTX_get_keylog_callback(ctx.ctx)); + (void)SSL_CTX_set_keylog_callback(ctx.ctx, &sslkeylogfile_log); +#endif + + // TODO: SSL_CTX_set_options(), SSL_CTX_set_mode() ? + + // we mandate TLS >= 1.3 + (void)SSL_CTX_set_min_proto_version(ctx.ctx, TLS1_3_VERSION); + (void)SSL_CTX_set_max_proto_version(ctx.ctx, 0); // up to max. + + // populate SSL_CTX::cert_store + for(auto& file : conf.tls_authority_files) { + log_debug_printf(_setup, "Read TLS CAs from %s\n", file.c_str()); + if(1!=SSL_CTX_load_verify_locations(ctx.ctx, file.c_str(), nullptr)) + throw SSLError("SSL_CTX_load_verify_locations"); + } + for(auto& dir : conf.tls_authority_dirs) { + log_debug_printf(_setup, "Read TLS CAs in %s\n", dir.c_str()); + if(1!=SSL_CTX_load_verify_locations(ctx.ctx, nullptr, dir.c_str())) + throw SSLError("SSL_CTX_load_verify_locations"); + } + + if(!conf.tls_keychain_file.empty()) { + log_debug_printf(_setup, "Read keychain (PKCS12) %s\n", conf.tls_keychain_file.c_str()); + + std::unique_ptr fp(fopen(conf.tls_keychain_file.c_str(), "rb")); + if(!fp) { + auto err = errno; + throw std::runtime_error(SB()<<"Unable to open \""< p12; + { + if(!d2i_PKCS12_fp(fp.get(), p12.acquire())) + throw SSLError(SB()<<"Unable to read \""< key; + impl::owned_ptr cert; + impl::owned_ptr CAs(__FILE__, __LINE__, sk_X509_new_null()); + + if(!PKCS12_parse(p12.get(), "", key.acquire(), cert.acquire(), CAs.acquire())) + throw SSLError(SB()<<"Unable to process \""<(SSL_CTX_get_ex_data(ctx.ctx, ossl_gbl->SSL_CTX_ex_idx)); + car->cert = std::move(cert); + + if(!SSL_CTX_build_cert_chain(ctx.ctx, SSL_BUILD_CHAIN_FLAG_UNTRUSTED)) // SSL_BUILD_CHAIN_FLAG_CHECK + throw SSLError("invalid cert chain"); + } + } + + /* wrt. SSL_VERIFY_CLIENT_ONCE + * TLS 1.3 does not support for session renegotiation. + * Does allow server to re-request client cert. via CertificateRequest. + * However, no way for client to re-request server cert. + * So we don't bother with this, and instead for connection reset + * when new certs. loaded. + */ + SSL_CTX_set_verify(ctx.ctx, SSL_VERIFY_PEER|SSL_VERIFY_CLIENT_ONCE, + &ossl_verify); + SSL_CTX_set_verify_depth(ctx.ctx, ossl_verify_depth); + + return ctx; +} + +} // namespace + +bool SSLContext::have_certificate() const +{ + if(!ctx) + throw std::invalid_argument("NULL"); + + auto car = static_cast(SSL_CTX_get_ex_data(ctx, ossl_gbl->SSL_CTX_ex_idx)); + return car->cert.operator bool(); +} + +const X509* SSLContext::certificate0() const +{ + if(!ctx) + throw std::invalid_argument("NULL"); + + auto car = static_cast(SSL_CTX_get_ex_data(ctx, ossl_gbl->SSL_CTX_ex_idx)); + return car->cert.get(); +} + +SSLContext SSLContext::for_client(const impl::ConfigCommon &conf) +{ + auto ctx(ossl_setup_common(TLS_client_method(), true, conf)); + + if(0!=SSL_CTX_set_alpn_protos(ctx.ctx, pva_alpn, sizeof(pva_alpn)-1)) + throw SSLError("oops"); + + return ctx; +} + +SSLContext SSLContext::for_server(const impl::ConfigCommon &conf) +{ + auto ctx(ossl_setup_common(TLS_server_method(), false, conf)); + + SSL_CTX_set_alpn_select_cb(ctx.ctx, &ossl_alpn_select, nullptr); + + return ctx; +} + +SSLError::SSLError(const std::string &msg) + :std::runtime_error([&msg]() -> std::string { + std::ostringstream strm; + const char *file = nullptr; + int line = 0; + const char *data = nullptr; + int flags = 0; + while(auto err = ERR_get_error_all(&file, &line, nullptr, &data, &flags)) { + strm< io(__FILE__, __LINE__, BIO_new(BIO_s_mem())); + (void)BIO_printf(io.get(), "subject:"); + (void)X509_NAME_print(io.get(), name, 1024); + (void)BIO_printf(io.get(), " issuer:"); + (void)X509_NAME_print(io.get(), issuer, 1024); + { + char *str = nullptr; + if(auto len = BIO_get_mem_data(io.get(), &str)) { + strm.write(str, len); + } + } + return strm; +} + +} // namespace ossl +} // namespace pvxs diff --git a/src/ossl.h b/src/ossl.h new file mode 100644 index 00000000..43099e40 --- /dev/null +++ b/src/ossl.h @@ -0,0 +1,97 @@ +/** + * Copyright - See the COPYRIGHT that is included with this distribution. + * pvxs is distributed subject to a Software License Agreement found + * in file LICENSE that is included with this distribution. + */ + +#ifndef OSSL_H +#define OSSL_H + +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include "pvxs/client.h" + +namespace std { +template<> +struct default_delete { + inline void operator()(SSL* fp) { if(fp) SSL_free(fp); } +}; +} // namespace std + +namespace pvxs { +namespace ossl { + +struct SSLError : public std::runtime_error { + explicit + SSLError(const std::string& msg); + virtual ~SSLError(); +}; + +struct ShowX509 { const X509* cert; }; +std::ostream& operator<<(std::ostream& strm, const ShowX509& cert); + +struct SSLContext { + SSL_CTX *ctx = nullptr; + + PVXS_API + static + SSLContext for_client(const impl::ConfigCommon& conf); + PVXS_API + static + SSLContext for_server(const impl::ConfigCommon &conf); + + SSLContext() =default; + inline + SSLContext(const SSLContext& o) + :ctx(o.ctx) + { + auto ret(SSL_CTX_up_ref(ctx)); + assert(ret==1); // can up_ref actually fail? + } + inline + SSLContext(SSLContext& o) noexcept + :ctx(o.ctx) + { + o.ctx = nullptr; + } + inline + ~SSLContext() { + SSL_CTX_free(ctx); // If ctx is NULL nothing is done. + } + inline + SSLContext& operator=(const SSLContext& o) + { + auto ret(SSL_CTX_up_ref(o.ctx)); + assert(ret==1); // can up_ref actually fail? + SSL_CTX_free(ctx); + ctx = o.ctx; + return *this; + } + inline + SSLContext& operator=(SSLContext&& o) + { + SSL_CTX_free(ctx); + ctx = o.ctx; + o.ctx = nullptr; + return *this; + } + + explicit operator bool() const { return ctx; } + + bool have_certificate() const; + const X509* certificate0() const; +}; + +} // namespace ossl +} // namespace pvxs + +#endif // OSSL_H diff --git a/src/pvxs/client.h b/src/pvxs/client.h index 2a5383ef..07731eb0 100644 --- a/src/pvxs/client.h +++ b/src/pvxs/client.h @@ -28,6 +28,7 @@ namespace client { class Context; struct Config; +struct ServerCredentials; //! Operation failed because of connection to server was lost struct PVXS_API Disconnect : public std::runtime_error @@ -54,14 +55,25 @@ struct PVXS_API Finished : public Disconnect virtual ~Finished(); }; -//! For monitor only. Subscription has (re)connected. +//! Indication of connection to a server struct PVXS_API Connected : public std::runtime_error { - Connected(const std::string& peerName); + Connected(const std::string& peerName, + const epicsTime& time, + const std::shared_ptr& cred); + Connected(const std::string& peerName, + const std::shared_ptr& cred) + :Connected(peerName, epicsTime::getCurrent(), cred) // legacy + {} virtual ~Connected(); + //! Server IP address const std::string peerName; + //! Local time of connection const epicsTime time; + //! Identity of server. + //! @since UNRELEASED + const std::shared_ptr cred; }; struct PVXS_API Interrupted : public std::runtime_error @@ -309,7 +321,17 @@ class PVXS_API Context { static Context fromEnv(); + /** Apply (in part) updated configuration + * + * Currently, only updates TLS configuration. Causes all in-progress + * Operations to be disconnected. + * + * @since UNRELEASED + */ + void reconfigure(const Config&); + //! effective config of running client + //! @since UNRELEASED Reference invalidated by a call to reconfigure() const Config& config() const; /** Force close the client. @@ -600,6 +622,22 @@ class PVXS_API Context { std::shared_ptr pvt; }; +//! Identity of a server +//! @since UNRELEASED +struct ServerCredentials { + //! Peer address (eg. numeric IPv4) + std::string peer; + //! The local interface address (eg. numeric IPv4) through which this client is connected. + //! May be a wildcard address (eg. 0.0.0.0) if the receiving socket is so bound. + std::string iface; + //! How account was authenticated. ("anonymous" or "x509") + std::string method; + //! Remote user account name. Meaning depends upon method. + std::string account; + //! Operation over secure transport + bool isTLS = false; +}; + namespace detail { struct PVRParser; @@ -920,7 +958,7 @@ class ConnectBuilder std::shared_ptr ctx; std::string _pvname; std::string _server; - std::function _onConn; + std::function _onConn; std::function _onDis; bool _syncCancel = true; public: @@ -931,7 +969,16 @@ class ConnectBuilder {} //! Handler to be invoked when channel becomes connected. - ConnectBuilder& onConnect(std::function&& cb) { _onConn = std::move(cb); return *this; } + //! @since UNRELEASED + ConnectBuilder& onConnect(std::function&& cb) + { _onConn = std::move(cb); return *this; } + //! Handler to be invoked when channel becomes connected. + //! @since UNRELEASED Prefer void(const Connected&) in new code. + ConnectBuilder& onConnect(std::function&& cb) + { + _onConn = [cb](const Connected&) { cb(); }; + return *this; + } //! Handler to be invoked when channel becomes disconnected. ConnectBuilder& onDisconnect(std::function&& cb) { _onDis = std::move(cb); return *this; } @@ -1002,7 +1049,7 @@ class DiscoverBuilder }; DiscoverBuilder Context::discover(std::function && fn) { return DiscoverBuilder(pvt, std::move(fn)); } -struct PVXS_API Config { +struct PVXS_API Config : public impl::ConfigCommon { /** List of unicast, multicast, and broadcast addresses to which search requests will be sent. * * Entries may take the forms: diff --git a/src/pvxs/netcommon.h b/src/pvxs/netcommon.h index 4492af23..6ec99f1f 100644 --- a/src/pvxs/netcommon.h +++ b/src/pvxs/netcommon.h @@ -8,6 +8,7 @@ #include #include #include +#include #include @@ -69,7 +70,16 @@ struct PVXS_API ReportInfo { virtual ~ReportInfo(); }; -#endif +#endif // PVXS_EXPERT_API_ENABLED + +struct PVXS_API ConfigCommon { + virtual ~ConfigCommon() =0; + + std::string tls_keychain_file; + + std::vector tls_authority_files; + std::vector tls_authority_dirs; +}; } // namespace impl } // namespace pvxs diff --git a/src/pvxs/server.h b/src/pvxs/server.h index c113b706..380c8bbd 100644 --- a/src/pvxs/server.h +++ b/src/pvxs/server.h @@ -87,7 +87,17 @@ class PVXS_API Server //! Queue a request to break run() Server& interrupt(); + /** Apply (in part) updated configuration + * + * Currently, only updates TLS configuration. Causes all in-progress + * Operations to be disconnected. + * + * @since UNRELEASED + */ + void reconfigure(const Config&); + //! effective config + //! @since UNRELEASED Reference invalidated by a call to reconfigure() const Config& config() const; //! Create a client configuration which can communicate with this Server. @@ -146,7 +156,7 @@ PVXS_API std::ostream& operator<<(std::ostream& strm, const Server& serv); //! Configuration for a Server -struct PVXS_API Config { +struct PVXS_API Config : public impl::ConfigCommon { //! List of network interface addresses (**not** host names) to which this server will bind. //! interfaces.empty() treated as an alias for "0.0.0.0", which may also be given explicitly. //! Port numbers are optional and unused (parsed and ignored) @@ -162,6 +172,9 @@ struct PVXS_API Config { std::vector beaconDestinations; //! TCP port to bind. Default is 5075. May be zero. unsigned short tcp_port = 5075; + //! TCP port to bind for TLS traffic. Default is 5076 + //! @since UNRELEASED + unsigned short tls_port = 5076; //! UDP port to bind. Default is 5076. May be zero, cf. Server::config() to find allocated port. unsigned short udp_port = 5076; //! Whether to populate the beacon address list automatically. (recommended) diff --git a/src/pvxs/srvcommon.h b/src/pvxs/srvcommon.h index 0458828e..cfac7451 100644 --- a/src/pvxs/srvcommon.h +++ b/src/pvxs/srvcommon.h @@ -33,7 +33,7 @@ struct PVXS_API ClientCredentials { //! The local interface address (eg. numeric IPv4) through which this client is connected. //! May be a wildcard address (eg. 0.0.0.0) if the receiving socket is so bound. std::string iface; - //! Authentication "method" + //! How account was authenticated. ("anonymous", "ca", or "x509") std::string method; //! Remote user account name. Meaning depends upon method. std::string account; @@ -47,6 +47,10 @@ struct PVXS_API ClientCredentials { * On other targets, an empty list is returned. */ std::set roles() const; + /** Operation over secure transport + * @since UNRELEASED + */ + bool isTLS = false; }; PVXS_API diff --git a/src/server.cpp b/src/server.cpp index 1ac9f25f..10d2bb2a 100644 --- a/src/server.cpp +++ b/src/server.cpp @@ -148,6 +148,39 @@ std::vector > Server::listSource() return names; } +void Server::reconfigure(const Config& newconf) +{ + if(!pvt) + throw std::logic_error("NULL Server"); + +#ifdef PVXS_ENABLE_OPENSSL + + ossl::SSLContext new_context; + if(!newconf.tls_keychain_file.empty() + || !newconf.tls_authority_files.empty() + || !newconf.tls_authority_dirs.empty()) + { + new_context = ossl::SSLContext::for_server(newconf); + } + + pvt->acceptor_loop.call([this, &new_context](){ + + log_debug_printf(serversetup, "Server reconfigure%s", "\n"); + + auto conns = std::move(pvt->connections); + for(auto& pair : conns) { + pair.second->disconnect(); + pair.second->cleanup(); + } + + pvt->tls_context = new_context; + }); + +#else + pvt->acceptor_loop.sync(); +#endif +} + const Config& Server::config() const { if(!pvt) @@ -162,6 +195,9 @@ client::Config Server::clientConfig() const throw std::logic_error("NULL Server"); client::Config ret; + ret.tls_authority_dirs = pvt->effective.tls_authority_dirs; + ret.tls_authority_files = pvt->effective.tls_authority_files; + // do not copy tls_keychain_file ret.udp_port = pvt->effective.udp_port; ret.tcp_port = pvt->effective.tcp_port; ret.interfaces = pvt->effective.interfaces; @@ -319,6 +355,18 @@ std::ostream& operator<<(std::ostream& strm, const Server& serv) } strm<<"\n"; +#ifdef PVXS_ENABLE_OPENSSL + if(serv.pvt->tls_context) { + auto cert(serv.pvt->tls_context.certificate0()); + assert(cert); + strm<connections) { @@ -327,15 +375,25 @@ std::ostream& operator<<(std::ostream& strm, const Server& serv) strm<peerName <<" backlog="<backlog.size() <<" TX="<statTx<<" RX="<statRx - <<" auth="<cred->method<<"\n"; - if(detail>2) - strm<<*conn->cred; + <<" auth="<cred->method + <<(conn->iface->isTLS ? " TLS" : "") + <<"\n"; if(detail<=2) continue; Indented I(strm); + strm<cred<<"\n"; +#ifdef PVXS_ENABLE_OPENSSL + if(conn->iface->isTLS && conn->connection()) { + auto ctx = bufferevent_openssl_get_ssl(conn->connection()); + assert(ctx); + if(auto cert = SSL_get0_peer_certificate(ctx)) + strm<chanBySID) { auto& chan = pair.second; strm<name<<" TX="<statTx<<" RX="<statRx<<' '; @@ -388,6 +446,15 @@ Server::Pvt::Pvt(const Config &conf) { effective.expand(); +#ifdef PVXS_ENABLE_OPENSSL + if(!effective.tls_keychain_file.empty() + || !effective.tls_authority_files.empty() + || !effective.tls_authority_dirs.empty()) + { + tls_context = ossl::SSLContext::for_server(effective); + } +#endif + beaconSender4.set_broadcast(true); auto manager = UDPManager::instance(effective.shareUDP()); @@ -488,18 +555,38 @@ Server::Pvt::Pvt(const Config &conf) acceptor_loop.call([this, &tcpifaces](){ // from accepter worker +#ifdef PVXS_ENABLE_OPENSSL + decltype(tcpifaces) tlsifaces(tcpifaces); // copy before any setPort() +#endif + bool firstiface = true; for(auto& addr : tcpifaces) { if(addr.port()==0) addr.setPort(effective.tcp_port); - interfaces.emplace_back(addr, this, firstiface); + interfaces.emplace_back(addr, this, firstiface, false); if(firstiface || effective.tcp_port==0) effective.tcp_port = interfaces.back().bind_addr.port(); firstiface = false; } +#ifdef PVXS_ENABLE_OPENSSL + if(tls_context) { + firstiface = true; + for(auto& addr : tlsifaces) { + // unconditionally set port to avoid clash with plain TCP listener + addr.setPort(effective.tls_port); + + interfaces.emplace_back(addr, this, firstiface, true); + + if(firstiface || effective.tls_port==0) + effective.tls_port = interfaces.back().bind_addr.port(); + firstiface = false; + } + } +#endif + for(const auto& addr : effective.beaconDestinations) { beaconDest.emplace_back(addr.c_str(), effective.udp_port); log_debug_printf(serversetup, "Will send beacons to %s\n", @@ -585,7 +672,8 @@ void Server::Pvt::start() if(evconnlistener_enable(iface.listener.get())) { log_err_printf(serversetup, "Error enabling listener on %s\n", iface.name.c_str()); } - log_debug_printf(serversetup, "Server enabled listener on %s\n", iface.name.c_str()); + log_debug_printf(serversetup, "Server enabled%s listener on %s\n", + iface.isTLS ? " TLS" : "", iface.name.c_str()); } }); if(prev_state!=Stopped) @@ -712,7 +800,7 @@ void Server::Pvt::onSearch(const UDPManager::Search& msg) } // "pvlist" breaks unless we honor mustReply flag - if(nreply==0 && !msg.mustReply) + if(nreply==0 && !msg.mustReply && (msg.protoTCP || msg.protoTLS)) return; VectorOutBuf M(true, searchReply); @@ -722,8 +810,16 @@ void Server::Pvt::onSearch(const UDPManager::Search& msg) _to_wire<12>(M, effective.guid.data(), false, __FILE__, __LINE__); to_wire(M, msg.searchID); to_wire(M, SockAddr::any(AF_INET)); - to_wire(M, uint16_t(effective.tcp_port)); - to_wire(M, "tcp"); +#ifdef PVXS_ENABLE_OPENSSL + if(msg.protoTLS && tls_context && effective.tls_port) { + to_wire(M, uint16_t(effective.tls_port)); + to_wire(M, "tls"); + } else +#endif + { // protoTCP + to_wire(M, uint16_t(effective.tcp_port)); + to_wire(M, "tcp"); + } // "found" flag to_wire(M, uint8_t(nreply!=0 ? 1 : 0)); diff --git a/src/serverchan.cpp b/src/serverchan.cpp index 36a08656..9c9be98e 100644 --- a/src/serverchan.cpp +++ b/src/serverchan.cpp @@ -183,12 +183,16 @@ void ServerConn::handle_SEARCH() M.skip(3 + 16 + 2, __FILE__, __LINE__); // unused and replyAddr (we always and only reply to TCP peer) bool foundtcp = false; + bool foundtls = false; Size nproto{0}; from_wire(M, nproto); for(size_t i=0; ibind_addr.port()); - to_wire(R, "tcp"); + if(foundtls) { + to_wire(R, "tls"); // prefer TLS + + } else if(foundtcp) { + to_wire(R, "tcp"); + } // "found" flag to_wire(R, uint8_t(nreply!=0 ? 1 : 0)); diff --git a/src/serverconn.cpp b/src/serverconn.cpp index fee0bfe7..b02080d2 100644 --- a/src/serverconn.cpp +++ b/src/serverconn.cpp @@ -55,14 +55,38 @@ DEFINE_LOGGER(remote, "pvxs.remote.log"); ServerConn::ServerConn(ServIface* iface, evutil_socket_t sock, struct sockaddr *peer, int socklen) :ConnBase(false, iface->server->effective.sendBE(), - bufferevent_socket_new(iface->server->acceptor_loop.base, sock, BEV_OPT_CLOSE_ON_FREE|BEV_OPT_DEFER_CALLBACKS), + evbufferevent(__FILE__, __LINE__, + bufferevent_socket_new(iface->server->acceptor_loop.base, sock, BEV_OPT_CLOSE_ON_FREE|BEV_OPT_DEFER_CALLBACKS) + ), SockAddr(peer)) ,iface(iface) ,tcp_tx_limit(evsocket::get_buffer_size(sock, true) * tcp_tx_limit_mult) { - log_debug_printf(connio, "Client %s connects, RX readahead %zu TX limit %zu\n", - peerName.c_str(), readahead, tcp_tx_limit); - + log_debug_printf(connio, "Client %s connects%s, RX readahead %zu TX limit %zu\n", + peerName.c_str(), iface->isTLS ? " TLS" : "", readahead, tcp_tx_limit); + +#ifdef PVXS_ENABLE_OPENSSL + if(iface->isTLS) { + assert(iface->server->tls_context); + auto ctx(SSL_new(iface->server->tls_context.ctx)); + if(!ctx) + throw ossl::SSLError("SSL_new()"); + auto rawconn = bev.release(); + // BEV_OPT_CLOSE_ON_FREE will free on error + evbufferevent tlsconn(__FILE__, __LINE__, + bufferevent_openssl_filter_new(iface->server->acceptor_loop.base, + rawconn, + ctx, + BUFFEREVENT_SSL_ACCEPTING, + BEV_OPT_CLOSE_ON_FREE|BEV_OPT_DEFER_CALLBACKS)); + bev = std::move(tlsconn); + + // added with libevent 2.2.1-alpha + //(void)bufferevent_ssl_set_flags(bev.get(), BUFFEREVENT_SSL_DIRTY_SHUTDOWN); + // deprecated, but not yet removed + bufferevent_openssl_set_allow_dirty_shutdown(bev.get(), 1); + } +#endif { auto cred(std::make_shared()); cred->peer = peerName; @@ -99,9 +123,11 @@ ServerConn::ServerConn(ServIface* iface, evutil_socket_t sock, struct sockaddr * * Old pvAccess* was missing a "break" when looping, * so it took the last known plugin. */ - to_wire(M, Size{2}); + to_wire(M, Size{iface->isTLS ? 3u : 2u}); to_wire(M, "anonymous"); to_wire(M, "ca"); + if(iface->isTLS) + to_wire(M, "x509"); auto bend = M.save(); FixedBuf H(sendBE, save, 8); @@ -192,6 +218,7 @@ void ServerConn::handle_CONNECTION_VALIDATION() std::string(SB()<(*cred)); + C->isTLS = iface->isTLS; if(selected=="ca") { auth["user"].as([&C, &selected](const std::string& user) { @@ -199,6 +226,24 @@ void ServerConn::handle_CONNECTION_VALIDATION() C->account = user; }); } +#ifdef PVXS_ENABLE_OPENSSL + else if(iface->isTLS && selected=="x509" && bev) { + auto ctx = bufferevent_openssl_get_ssl(bev.get()); + assert(ctx); + auto cert = SSL_get0_peer_certificate(ctx); + // SSL_get0_verified_chain() + if(cert) { + auto subj = X509_get_subject_name(cert); + char name[64]; + if(subj && X509_NAME_get_text_by_NID(subj, NID_commonName, name, sizeof(name)-1)) { + name[sizeof(name)-1] = '\0'; + log_debug_printf(connio, "Peer CN=%s\n", name); + C->method = selected; + C->account = name; + } + } + } +#endif if(C->method.empty()) { C->account = C->method = "anonymous"; } @@ -208,13 +253,14 @@ void ServerConn::handle_CONNECTION_VALIDATION() } } - if(selected!="ca" && selected!="anonymous") { + if(selected!="ca" && selected!="anonymous" && selected!="x509") { log_debug_printf(connsetup, "Client %s selects unadvertised auth \"%s\"", peerName.c_str(), selected.c_str()); auth_complete(this, Status{Status::Error, "Client selects unadvertised auth"}); return; } else { - log_debug_printf(connsetup, "Client %s selects auth \"%s\"\n", peerName.c_str(), selected.c_str()); + log_debug_printf(connsetup, "Client %s selects auth \"%s\" as \"%s\"\n", + peerName.c_str(), cred->method.c_str(), cred->account.c_str()); } // remainder of segBuf is payload w/ credentials @@ -354,6 +400,19 @@ void ServerConn::cleanup() } } +void ServerConn::bevEvent(short events) +{ +#ifdef PVXS_ENABLE_OPENSSL + if((events & BEV_EVENT_ERROR) && iface->isTLS && bev) { + while(auto err = bufferevent_get_openssl_error(bev.get())) { + log_err_printf(connio, "TLS Error (0x%lx) %s\n", + err, ERR_reason_error_string(err)); + } + } +#endif + ConnBase::bevEvent(events); +} + void ServerConn::bevRead() { ConnBase::bevRead(); @@ -394,8 +453,9 @@ void ServerConn::bevWrite() } -ServIface::ServIface(const SockAddr &addr, server::Server::Pvt *server, bool fallback) +ServIface::ServIface(const SockAddr &addr, server::Server::Pvt *server, bool fallback, bool isTLS) :server(server) + ,isTLS(isTLS) ,bind_addr(addr) { server->acceptor_loop.assertInLoop(); diff --git a/src/serverconn.h b/src/serverconn.h index a068bb19..b5a205b1 100644 --- a/src/serverconn.h +++ b/src/serverconn.h @@ -158,7 +158,7 @@ struct ServerConn final : public ConnBase, public std::enable_shared_from_this #include +#include + #include #include @@ -47,6 +49,14 @@ #include +// hooks for std::unique_ptr +namespace std { +template<> +struct default_delete { + inline void operator()(FILE* fp) { if(fp) fclose(fp); } +}; +} + namespace pvxs {namespace impl { template diff --git a/test/Makefile b/test/Makefile index 66debbdc..230023bb 100644 --- a/test/Makefile +++ b/test/Makefile @@ -114,6 +114,13 @@ TESTPROD_HOST += testudpfwd testudpfwd_SRCS += testudpfwd.cpp TESTS += testudpfwd +ifeq (YES,$(EVENT2_HAS_OPENSSL)) +TESTPROD_HOST += testtls +testtls_SRCS += testtls.cpp +TESTS += testtls +TESTFILES += $(COMMON_DIR)/ca.pem $(wildcard $(COMMON_DIR)/*.p12) +endif + ifdef BASE_7_0 TESTPROD_HOST += benchdata @@ -187,4 +194,17 @@ include $(TOP)/configure/RULES ifdef BASE_3_15 rtemsTestData.c : $(TESTFILES) $(TOOLS)/epicsMakeMemFs.pl $(PERL) $(TOOLS)/epicsMakeMemFs.pl $@ epicsRtemsFSImage $(TESTFILES) + +ifeq (YES,$(EVENT2_HAS_OPENSSL)) +testioc_registerRecordDeviceDriver.cpp: $(COMMON_DIR)/ca.pem + +# generate test certs only with EPICS_HOST_ARCH +ifdef T_A +ifeq ($(T_A),$(EPICS_HOST_ARCH)) +$(COMMON_DIR)/ca.pem : ../gen_test_certs.py + $(PYTHON) ../gen_test_certs.py --outdir $(COMMON_DIR) +endif # EPICS_HOST_ARCH +endif # T_A +endif # EVENT2_HAS_OPENSSL + endif diff --git a/test/gen_test_certs.py b/test/gen_test_certs.py new file mode 100755 index 00000000..cb6e3a90 --- /dev/null +++ b/test/gen_test_certs.py @@ -0,0 +1,214 @@ +#!/usr/bin/env python3 +"""Generate a set of certificates and keys for use by unit tests. + +Create a root CA, and an intermediate CA. +Intermediate CA will issue some client and server certificates. +""" + +from pathlib import Path +from typing import List, Tuple, Optional + +from OpenSSL import crypto + +# certs only for testing. no need to waste RNG +nBits = 2048 + +hashalg = 'sha256' + +def getargs(): + from argparse import ArgumentParser + P = ArgumentParser() + P.add_argument('-O', '--outdir', metavar='DIR', + type=Path, default=Path(__file__).parent, + help='Write files to this directory') + return P + +def main(args): + outdir = args.outdir + expire = 10*365*24*60*60 # 10 years + + rootCA = create_cert( + subject=[('CN', 'rootCA')], + SN = 0, + notAfter= expire, + isCA = True, + ) + write_cert(outdir / 'ca.pem', rootCA[0]) + write_p12(outdir / 'ca.p12', None, [rootCA[0]]) + # don't save the root CA key, this would be kept offline anyway, + # and these certs are after all only for testing... + + # special case w/o intermediate CA + superserver1 = create_cert( + subject=[('CN', 'superserver1')], + issuer=rootCA, + SN = 1, + notAfter= expire, + isServer= True, + ) + write_p12(outdir / 'superserver1.p12', superserver1, [rootCA[0]]) + write_cert(outdir / 'superserver1.pem', superserver1[0]) + write_key(outdir / 'superserver1.key', superserver1[1]) + + intermediateCA = create_cert( + subject=[('CN', 'intermediateCA')], + issuer=rootCA, + SN = 2, + notAfter= expire, + isCA = True, + ) + # intermediate CA may be used for OCSP or CRL signing + write_cert(outdir / 'intermediateCA.pem', intermediateCA[0]) + write_p12(outdir / 'intermediateCA.p12', intermediateCA, [rootCA[0]]) + + full_chain = [intermediateCA[0], rootCA[0]] + incomplete_chain = [rootCA[0]] + + # IOC is both client and server + ioc1 = create_cert( + subject=[('CN', 'ioc1')], + issuer=intermediateCA, + SN = 3, + notAfter= expire, + isServer= True, + isClient= True, + ) + write_p12(outdir / 'ioc1.p12', ioc1, full_chain) + write_p12(outdir / 'ioc1-incomplete.p12', ioc1, incomplete_chain) + + server1 = create_cert( + subject=[('CN', 'server1')], + issuer=intermediateCA, + SN = 4, + notAfter= expire, + isServer = True, + ) + write_p12(outdir / 'server1.p12', server1, full_chain) + + server2 = create_cert( + subject=[('CN', 'server2')], + issuer=intermediateCA, + SN = 5, + notBefore= -1000, # will be expired + isServer = True, + ) + write_p12(outdir / 'server2-expired.p12', server2, full_chain) + + client1 = create_cert( + subject=[('CN', 'client1')], + issuer=intermediateCA, + SN = 6, + notAfter= expire, + isClient= True, + ) + write_p12(outdir / 'client1.p12', client1, full_chain) + + client2 = create_cert( + subject=[('CN', 'client2')], + issuer=intermediateCA, + SN = 7, + notAfter= expire, + isClient= True, + ) + write_p12(outdir / 'client2.p12', client2, full_chain) + +def create_cert(subject: List[Tuple[str,str]], + issuer : Optional[Tuple[crypto.X509, crypto.PKey]] = None, + SN : Optional[int]=None, + notBefore = 0, + notAfter = 0, + isCA = False, + isServer = False, + isClient = False, + ) -> Tuple[crypto.X509, crypto.PKey]: + key = crypto.PKey() + key.generate_key(crypto.TYPE_RSA, nBits) + + cert = crypto.X509() + cert.set_version(2) + cert.set_pubkey(key) + + subj = cert.get_subject() + for comp, val in subject: + setattr(subj, comp, val) + + if issuer is None: + issuer, ikey = cert, key # self-signed + else: + issuer, ikey = issuer + + cert.set_issuer(issuer.get_subject()) + + cert.gmtime_adj_notBefore(notBefore) + cert.gmtime_adj_notAfter(notAfter) + + if SN is not None: + cert.set_serial_number(SN) + + # nsCertType = [] # deprecated + keyUsage = [] + extendedKeyUsage = [] + + if isCA: + #nsCertType += [b'sslCA'] + if cert is not issuer: + keyUsage += [b'digitalSignature'] + extendedKeyUsage += [b'serverAuth', b'clientAuth', b'OCSPSigning'] + else: + pass # root CA doesn't need extendedKeyUsage + keyUsage += [b'cRLSign', b'keyCertSign', ] + + if isServer or isClient: + keyUsage += [b'digitalSignature', b'keyEncipherment'] + + if isServer: + #nsCertType += [b'server'] + extendedKeyUsage += [b'serverAuth'] + + if isClient: + #nsCertType += [b'client', b'email', b'objsign'] + extendedKeyUsage += [b'clientAuth'] + + cert.add_extensions([ + crypto.X509Extension(b'subjectKeyIdentifier', False, b"hash", subject=cert), + ]) + # for self-signed, must set subjectKeyIdentifier before authorityKeyIdentifier. + # for others, makes no difference. + cert.add_extensions([ + crypto.X509Extension(b'authorityKeyIdentifier', False, b"keyid:always,issuer:always", issuer=issuer), + crypto.X509Extension(b'basicConstraints', True, b"CA:TRUE" if isCA else b"CA:FALSE"), + #crypto.X509Extension(b'nsCertType', False, b', '.join(nsCertType)), + crypto.X509Extension(b'keyUsage', False, b', '.join(keyUsage)), + ]) + if extendedKeyUsage: + cert.add_extensions([ + crypto.X509Extension(b'extendedKeyUsage', False, b', '.join(extendedKeyUsage)), + ]) + + cert.sign(ikey, hashalg) + + return cert, key + +def write_p12(out : Path, + pair : Optional[Tuple[crypto.X509, crypto.PKey]] = None, + CAs : List[crypto.X509] = [], + ): + P = crypto.PKCS12() + + if pair is not None: + cert, key = pair + P.set_certificate(cert) + P.set_privatekey(key) + + P.set_ca_certificates(CAs) + + out.write_bytes(P.export(passphrase=b'')) + +def write_cert(out : Path, cert : crypto.X509): + out.write_bytes(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) + +def write_key(out : Path, key : crypto.PKey): + out.write_bytes(crypto.dump_privatekey(crypto.FILETYPE_PEM, key)) + +if __name__=='__main__': + main(getargs().parse_args()) diff --git a/test/testtls.cpp b/test/testtls.cpp new file mode 100644 index 00000000..60d3537c --- /dev/null +++ b/test/testtls.cpp @@ -0,0 +1,269 @@ +/** + * Copyright - See the COPYRIGHT that is included with this distribution. + * pvxs is distributed subject to a Software License Agreement found + * in file LICENSE that is included with this distribution. + */ +#define PVXS_ENABLE_EXPERT_API + +#include + +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace pvxs; + +namespace { + +void testGetSuper() { + testShow()<<__func__; + + auto initial(nt::NTScalar{TypeCode::Int32}.create()); + auto mbox(server::SharedPV::buildReadonly()); + + auto serv_conf(server::Config::isolated()); + serv_conf.tls_keychain_file = "../O.Common/superserver1.p12"; + + auto serv(serv_conf.build() + .addPV("mailbox", mbox)); + + auto cli_conf(serv.clientConfig()); + cli_conf.tls_authority_files.push_back("../O.Common/ca.pem"); + + auto cli(cli_conf.build()); + + mbox.open(initial.update("value", 42)); + serv.start(); + + auto reply(cli.get("mailbox").exec()->wait(5.0)); + testEq(reply["value"].as(), 42); +} + +void testGetIntermediate() { + testShow()<<__func__; + + auto initial(nt::NTScalar{TypeCode::Int32}.create()); + auto mbox(server::SharedPV::buildReadonly()); + + auto serv_conf(server::Config::isolated()); + serv_conf.tls_keychain_file = "../O.Common/server1.p12"; + + auto serv(serv_conf.build() + .addPV("mailbox", mbox)); + + auto cli_conf(serv.clientConfig()); + cli_conf.tls_authority_files.push_back("../O.Common/ca.pem"); + + auto cli(cli_conf.build()); + + mbox.open(initial.update("value", 42)); + serv.start(); + + auto reply(cli.get("mailbox").exec()->wait(5.0)); + testEq(reply["value"].as(), 42); +} + +struct WhoAmI final : public server::Source { + const Value resultType; + + WhoAmI() + :resultType(nt::NTScalar(TypeCode::String).create()) + {} + + virtual void onSearch(Search &op) override final { + for(auto& pv : op) { + if(strcmp(pv.name(), "whoami")==0) + pv.claim(); + } + } + + virtual void onCreate(std::unique_ptr &&op) override final { + if(op->name()!="whoami") + return; + + op->onOp([this](std::unique_ptr&& cop) { + + cop->onGet([this](std::unique_ptr&& eop) { + auto cred(eop->credentials()); + std::ostringstream strm; + strm<method<<'/'<account; + + eop->reply(resultType.cloneEmpty() + .update("value", strm.str())); + }); + + cop->connect(resultType); + }); + + std::shared_ptr sub; + op->onSubscribe([this, sub](std::unique_ptr&& sop) mutable { + sub = sop->connect(resultType); + auto cred(sub->credentials()); + std::ostringstream strm; + strm<method<<'/'<account; + + sub->post(resultType.cloneEmpty() + .update("value", strm.str())); + }); + } +}; + +Value pop(const std::shared_ptr& sub, epicsEvent& evt) +{ + while(true) { + if(auto ret = sub->pop()) { + return ret; + + } else if (!evt.wait(5.0)) { + testFail("timeout waiting for event"); + return Value(); + } + } +} + +void testClientReconfig() { + testShow()<<__func__; + + auto serv_conf(server::Config::isolated()); + serv_conf.tls_keychain_file = "../O.Common/ioc1.p12"; + + auto serv(serv_conf.build() + .addSource("whoami", std::make_shared())); + + auto cli_conf(serv.clientConfig()); + cli_conf.tls_keychain_file = "../O.Common/client1.p12"; + + auto cli(cli_conf.build()); + + serv.start(); + + epicsEvent evt; + auto sub(cli.monitor("whoami") + .maskConnected(false) + .maskDisconnected(false) + .event([&evt](client::Subscription&) { + evt.signal(); + }).exec()); + Value update; + + try { + pop(sub, evt); + testFail("Unexpected success"); + testSkip(2, "oops"); + } catch(client::Connected& e) { + testTrue(e.cred->isTLS); + testEq(e.cred->method, "x509"); + testEq(e.cred->account, "ioc1"); + } + testDiag("Connect"); + + update = pop(sub, evt); + testEq(update["value"].as(), "x509/client1"); + + cli_conf = cli.config(); + cli_conf.tls_keychain_file = "../O.Common/client2.p12"; + testDiag("cli.reconfigure()"); + cli.reconfigure(cli_conf); + + testThrows([&sub, &evt]{ + pop(sub, evt); + }); + testDiag("Disconnect"); + + testThrows([&sub, &evt]{ + pop(sub, evt); + }); + testDiag("Reconnect"); + + update = pop(sub, evt); + testEq(update["value"].as(), "x509/client2"); +} + +void testServerReconfig() { + testShow()<<__func__; + + auto serv_conf(server::Config::isolated()); + serv_conf.tls_keychain_file = "../O.Common/server1.p12"; + + auto serv(serv_conf.build() + .addSource("whoami", std::make_shared())); + + auto cli_conf(serv.clientConfig()); + cli_conf.tls_keychain_file = "../O.Common/ioc1.p12"; + + auto cli(cli_conf.build()); + + serv.start(); + + epicsEvent evt; + auto sub(cli.monitor("whoami") + .maskConnected(false) + .maskDisconnected(false) + .event([&evt](client::Subscription&) { + evt.signal(); + }).exec()); + Value update; + + try { + pop(sub, evt); + testFail("Unexpected success"); + testSkip(2, "oops"); + } catch(client::Connected& e) { + testTrue(e.cred->isTLS); + testEq(e.cred->method, "x509"); + testEq(e.cred->account, "server1"); + } + testDiag("Connect"); + + update = pop(sub, evt); + testEq(update["value"].as(), "x509/ioc1"); + + serv_conf = serv.config(); + serv_conf.tls_keychain_file = "../O.Common/ioc1.p12"; + testDiag("serv.reconfigure()"); + serv.reconfigure(serv_conf); + + testThrows([&sub, &evt]{ + pop(sub, evt); + }); + testDiag("Disconnect"); + + try { + pop(sub, evt); + testFail("Unexpected success"); + testSkip(2, "oops"); + } catch(client::Connected& e) { + testTrue(e.cred->isTLS); + testEq(e.cred->method, "x509"); + testEq(e.cred->account, "ioc1"); + } + testDiag("Reconnect"); + + update = pop(sub, evt); + testEq(update["value"].as(), "x509/ioc1"); +} + +} // namespace + +MAIN(testtls) +{ + testPlan(18); + testSetup(); + logger_config_env(); + testGetSuper(); + testGetIntermediate(); + testClientReconfig(); + testServerReconfig(); + cleanup_for_valgrind(); + return testDone(); +} diff --git a/tools/mshim.cpp b/tools/mshim.cpp index 41a070a2..270a5b48 100644 --- a/tools/mshim.cpp +++ b/tools/mshim.cpp @@ -110,8 +110,12 @@ struct App { size_t nproto = msg.otherproto.size(); if(msg.protoTCP) nproto++; + if(msg.protoTLS) + nproto++; to_wire(buf, Size{nproto}); + if(msg.protoTLS) + to_wire(buf, "tls"); if(msg.protoTCP) to_wire(buf, "tcp"); for(auto& prot : msg.otherproto) {