Files
pvxs/src/config.cpp
T
Michael Davidsaver 266ee52704 server: bind both wildcard for UDP
When possible, bind both ipv4 and ipv6 wildcard addresses
for UDP.  Even when only binding ipv6 address for TCP (Linux)
2023-04-10 08:44:37 -07:00

692 lines
20 KiB
C++

/**
* 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 <algorithm>
#include <vector>
#include <string>
#include <sstream>
#include <limits>
#include <cmath>
#include <dbDefs.h>
#include <osiSock.h>
#include <epicsMath.h>
#include <epicsStdlib.h>
#include <epicsString.h>
#include <pvxs/log.h>
#include "serverconn.h"
#include "clientimpl.h"
#include "utilpvt.h"
#include "evhelper.h"
DEFINE_LOGGER(serversetup, "pvxs.server.setup");
DEFINE_LOGGER(clientsetup, "pvxs.client.setup");
DEFINE_LOGGER(config, "pvxs.config");
namespace pvxs {
SockEndpoint::SockEndpoint(const char* ep, uint16_t defport)
{
// <IP46>
// <IP46>,<ttl#>
// <IP46>@ifacename
// <IP46>,<ttl#>@ifacename
auto comma = strchr(ep, ',');
auto at = strchr(ep, '@');
if(comma && at && comma > at) {
throw std::runtime_error(SB()<<'"'<<escape(ep)<<"\" comma expected before @");
}
if(!comma && !at) {
addr.setAddress(ep, defport);
} else { // comma || at
auto firstsep = comma ? comma : at;
addr.setAddress(std::string(ep, firstsep-ep), defport);
if(comma && !at) {
ttl = parseTo<int64_t>(comma+1);
} else if(comma) {
ttl = parseTo<int64_t>(std::string(comma+1, at-comma-1));
}
if(at)
iface = at+1;
}
auto& ifmap = IfaceMap::instance();
if(addr.family()==AF_INET6) {
if(iface.empty() && addr->in6.sin6_scope_id) {
// interface index provide with IPv6 address
// we map back to symbolic name for storage
iface = ifmap.name_of(addr->in6.sin6_scope_id);
}
addr->in6.sin6_scope_id = 0;
} else if(addr.family()==AF_INET && addr.isMCast() && !iface.empty()) {
SockAddr ifaddr(AF_INET);
if(evutil_inet_pton(AF_INET, iface.c_str(), &ifaddr->in.sin_addr.s_addr)==1) {
// map interface address to symbolic name
iface = ifmap.name_of(ifaddr);
}
}
if(!iface.empty() && !ifmap.index_of(iface)) {
log_warn_printf(config, "Invalid interface address or name: \"%s\"\n", iface.c_str());
}
}
MCastMembership SockEndpoint::resolve() const
{
if(!addr.isMCast())
throw std::logic_error("not mcast");
auto& ifmap = IfaceMap::instance();
MCastMembership m;
m.af = addr.family();
if(m.af==AF_INET) {
auto& req = m.req.in;
req.imr_multiaddr.s_addr = addr->in.sin_addr.s_addr;
if(!iface.empty()) {
auto iface = ifmap.address_of(this->iface);
if(iface.family()==AF_INET) {
req.imr_interface.s_addr = iface->in.sin_addr.s_addr;
}
}
} else if(m.af==AF_INET6) {
auto& req = m.req.in6;
req.ipv6mr_multiaddr = addr->in6.sin6_addr;
if(!iface.empty()) {
req.ipv6mr_interface = ifmap.index_of(this->iface);
if(!req.ipv6mr_interface) {
log_warn_printf(config, "Unable to resolve interface '%s'\n", iface.c_str());
}
}
} else {
throw std::logic_error("Unsupported address family");
}
return m;
}
std::ostream& operator<<(std::ostream& strm, const SockEndpoint& addr)
{
strm<<addr.addr;
if(addr.addr.isMCast()) {
if(addr.ttl)
strm<<','<<addr.ttl;
if(!addr.iface.empty())
strm<<'@'<<addr.iface;
}
return strm;
}
bool operator==(const SockEndpoint& lhs, const SockEndpoint& rhs)
{
return lhs.addr==rhs.addr && lhs.ttl==rhs.ttl && lhs.iface==rhs.iface;
}
namespace {
/* Historically pvAccessCPP used $EPICS_PVA_CONN_TMO as the period
* between sending CMD_ECHO. *::Config::tcpTimeout is the actual
* inactivity timeout period. Apply a scaling factor to add a
* go from one to the other.
*/
constexpr double tmoScale = 4.0/3.0; // 40 second idle timeout / 30 configured
void split_addr_into(const char* name, std::vector<std::string>& out, const std::string& inp,
uint16_t defaultPort, bool required=false)
{
size_t pos=0u;
// parse, resolve host names, then re-print.
// Catch syntax errors early, and normalize prior to removing duplicates
while(pos<inp.size()) {
auto start = inp.find_first_not_of(" \t\r\n", pos);
auto end = inp.find_first_of(" \t\r\n", start);
pos = end;
if(start<end) {
auto temp(inp.substr(start, end==std::string::npos ? end : end-start));
try {
SockEndpoint ep(temp);
if(ep.addr.port()==0)
ep.addr.setPort(defaultPort);
out.push_back(SB()<<ep);
} catch(std::exception& e){
if(required)
throw std::runtime_error(SB()<<"invalid endpoint \""<<temp<<"\" "<<e.what());
log_err_printf(config, "%s ignoring invalid '%s' : %s\n", name, temp.c_str(), e.what());
}
}
}
// remove any duplicates
std::sort(out.begin(), out.end());
out.erase(std::unique(out.begin(), out.end()),
out.end());
}
std::string join_addr(const std::vector<std::string>& in)
{
std::ostringstream strm;
bool first=true;
for(auto& addr : in) {
if(first)
first = false;
else
strm<<' ';
strm<<addr;
}
return strm.str();
}
void parse_bool(bool& dest, const std::string& name, const std::string& val)
{
if(epicsStrCaseCmp(val.c_str(), "YES")==0 || val=="1") {
dest = true;
} else if(epicsStrCaseCmp(val.c_str(), "NO")==0 || val=="0") {
dest = false;
} else {
log_err_printf(config, "%s invalid bool value (YES/NO) : '%s'\n",
name.c_str(), val.c_str());
}
}
void parse_timeout(double& dest, const std::string& name, const std::string& val)
{
double temp;
try {
temp = parseTo<double>(val);
if(!std::isfinite(temp)
|| temp<0.0
|| temp>double(std::numeric_limits<time_t>::max()))
throw std::out_of_range("Out of range");
dest = temp*tmoScale;
} catch(std::exception& e) {
log_err_printf(serversetup, "%s invalid double value : '%s'\n",
name.c_str(), val.c_str());
}
}
struct PickOne {
const std::map<std::string, std::string>& defs;
bool useenv;
std::string name, val;
bool operator()(std::initializer_list<const char*> names) {
for(auto candidate : names) {
if(useenv) {
if(auto eval = getenv(candidate)) {
name = candidate;
val = eval;
return true;
}
} else {
auto it = defs.find(candidate);
if(it!=defs.end()) {
name = candidate;
val = it->second;
return true;
}
}
}
return false;
}
};
std::vector<SockEndpoint> parseAddresses(const std::vector<std::string>& addrs, uint16_t defport=0)
{
std::vector<SockEndpoint> ret;
for(const auto& addr : addrs) {
try {
ret.emplace_back(addr, defport);
}catch(std::runtime_error& e){
log_warn_printf(config, "Ignoring %s : %s\n", addr.c_str(), e.what());
continue;
}
}
return ret;
}
void printAddresses(std::vector<std::string>& out, const std::vector<SockEndpoint>& inp)
{
std::vector<std::string> temp;
temp.reserve(inp.size());
for(auto& addr : inp) {
temp.emplace_back(SB()<<addr);
}
out = std::move(temp);
}
// Fill out address list by appending broadcast addresses
// of any and all local interface addresses already included
void expandAddrList(const std::vector<SockEndpoint>& ifaces,
std::vector<SockEndpoint>& addrs)
{
SockAttach attach;
evsocket dummy(AF_INET, SOCK_DGRAM, 0);
for(auto& saddr : ifaces) {
auto matchAddr = &saddr.addr;
if(evsocket::ipstack==evsocket::Linsock && saddr.addr.family()==AF_INET6 && saddr.addr.isAny()) {
// special case handling to match "promote" in server::Config::expand()
// treat [::] as 0.0.0.0
matchAddr = nullptr;
} else if(saddr.addr.family()!=AF_INET) {
continue;
}
for(auto& addr : dummy.broadcasts(matchAddr)) {
addr.setPort(0u);
addrs.emplace_back(addr);
}
}
}
void addGroups(std::vector<SockEndpoint>& ifaces,
const std::vector<SockEndpoint>& addrs)
{
auto& ifmap = IfaceMap::instance();
std::set<std::string> allifaces;
for(const auto& addr : addrs) {
if(!addr.addr.isMCast())
continue;
if(!addr.iface.empty()) {
// interface already specified
ifaces.push_back(addr);
} else {
// no interface specified, treat as wildcard
if(allifaces.empty())
allifaces = ifmap.all_external();
for(auto& iface : allifaces) {
auto ifaceaddr(addr);
ifaceaddr.iface = iface;
ifaces.push_back(ifaceaddr);
}
}
}
}
// remove duplicates while preserving order of first appearance
template<typename A>
void removeDups(std::vector<A>& 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<SockEndpoint>& addrs)
{
std::map<std::pair<SockAddr, std::string>, size_t> seen;
for(size_t i=0; i<addrs.size(); ) {
auto& ep = addrs[i];
auto key = std::make_pair(ep.addr, ep.iface);
auto it = seen.find(key);
if(it==seen.end()) { // first sighting
seen[key] = i++;
} else { // duplicate
auto& orig = addrs[it->second];
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.
*
* - Originally pvAccessCPP clients didn't send CMD_ECHO, and servers would never timeout.
* - Since module version 7.0.0 (in Base 7.0.3) clients send echo every 15 seconds, and
* either peer will timeout after 30 seconds of inactivity.
* - pvAccessJava clients send CMD_ECHO every 30 seconds, and timeout after 60 seconds.
*
* So this was a bug, with c++ server timeout racing with Java client echo.
*
* - As a compromise, continue to send echo at least every 15 seconds,
* and increase default timeout to 40.
*/
if(!std::isfinite(tmo) || tmo <= 0.0 || tmo >= double(std::numeric_limits<time_t>::max()))
tmo = 40.0;
else if(tmo < 2.0)
tmo = 2.0;
}
} // namespace
namespace server {
static
void _fromDefs(Config& self, const std::map<std::string, std::string>& defs, bool useenv)
{
PickOne pickone{defs, useenv};
if(pickone({"EPICS_PVAS_SERVER_PORT", "EPICS_PVA_SERVER_PORT"})) {
try {
self.tcp_port = parseTo<uint64_t>(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<uint64_t>(pickone.val);
}catch(std::exception& e) {
log_err_printf(serversetup, "%s invalid integer : %s", pickone.name.c_str(), e.what());
}
}
if(pickone({"EPICS_PVAS_INTF_ADDR_LIST"})) {
split_addr_into(pickone.name.c_str(), self.interfaces, pickone.val, self.tcp_port, true);
}
if(pickone({"EPICS_PVAS_IGNORE_ADDR_LIST"})) {
split_addr_into(pickone.name.c_str(), self.ignoreAddrs, pickone.val, 0, true);
}
if(pickone({"EPICS_PVAS_BEACON_ADDR_LIST", "EPICS_PVA_ADDR_LIST"})) {
split_addr_into(pickone.name.c_str(), self.beaconDestinations, pickone.val, self.udp_port);
}
if(pickone({"EPICS_PVAS_AUTO_BEACON_ADDR_LIST", "EPICS_PVA_AUTO_ADDR_LIST"})) {
parse_bool(self.auto_beacon, pickone.name, pickone.val);
}
if(pickone({"EPICS_PVA_CONN_TMO"})) {
parse_timeout(self.tcpTimeout, pickone.name, pickone.val);
}
}
Config& Config::applyEnv()
{
_fromDefs(*this, std::map<std::string, std::string>(), true);
return *this;
}
Config Config::isolated(int family)
{
Config ret;
ret.udp_port = 0u;
ret.tcp_port = 0u;
ret.auto_beacon = false;
switch(family) {
case AF_INET:
ret.interfaces.emplace_back("127.0.0.1");
ret.beaconDestinations.emplace_back("127.0.0.1");
break;
case AF_INET6:
ret.interfaces.emplace_back("::1");
ret.beaconDestinations.emplace_back("::1");
break;
default:
throw std::logic_error(SB()<<"Unsupported address family "<<family);
}
return ret;
}
Config& Config::applyDefs(const std::map<std::string, std::string>& defs)
{
_fromDefs(*this, defs, false);
return *this;
}
void Config::updateDefs(defs_t& defs) const
{
defs["EPICS_PVA_BROADCAST_PORT"] = defs["EPICS_PVAS_BROADCAST_PORT"] = SB()<<udp_port;
defs["EPICS_PVA_SERVER_PORT"] = defs["EPICS_PVAS_SERVER_PORT"] = SB()<<tcp_port;
defs["EPICS_PVA_AUTO_ADDR_LIST"] = defs["EPICS_PVAS_AUTO_BEACON_ADDR_LIST"] = auto_beacon ? "YES" : "NO";
defs["EPICS_PVA_ADDR_LIST"] = defs["EPICS_PVAS_BEACON_ADDR_LIST"] = join_addr(beaconDestinations);
defs["EPICS_PVA_INTF_ADDR_LIST"] = defs["EPICS_PVAS_INTF_ADDR_LIST"] = join_addr(interfaces);
defs["EPICS_PVAS_IGNORE_ADDR_LIST"] = join_addr(ignoreAddrs);
defs["EPICS_PVA_CONN_TMO"] = SB()<<tcpTimeout/tmoScale;
}
void Config::expand()
{
auto ifaces(parseAddresses(interfaces));
auto bdest(parseAddresses(beaconDestinations));
// empty interface address list implies the wildcard
// (because no addresses isn't interesting...)
if(ifaces.empty()) {
ifaces.emplace_back(SockAddr::any(AF_INET));
}
auto& ifmap = IfaceMap::instance();
for(size_t i=0; i<ifaces.size(); i++) {
auto& ep = ifaces[i];
if(!ep.addr.isMCast()) {
// no-op
} else if(!ep.iface.empty()) {
ifaces.emplace_back(ifmap.address_of(ep.iface));
} else {
ifaces.emplace_back(SockAddr::any(ep.addr.family()));
}
// ep invalidated by emplace()
}
if(auto_beacon) {
// use interface list add ipv4 broadcast addresses to beaconDestinations.
// 0.0.0.0 -> adds all bcasts
// otherwise add bcast for each iface address
expandAddrList(ifaces, bdest);
addGroups(ifaces, bdest);
auto_beacon = false;
}
removeDups(ifaces);
printAddresses(interfaces, ifaces);
removeDups(bdest);
printAddresses(beaconDestinations, bdest);
removeDups(ignoreAddrs);
enforceTimeout(tcpTimeout);
}
std::ostream& operator<<(std::ostream& strm, const Config& conf)
{
auto showAddrs = [&strm](const char* var, const std::vector<std::string>& addrs) {
strm<<indent{}<<var<<"=\"";
bool first = true;
for(auto& iface : addrs) {
if(first)
first = false;
else
strm<<' ';
strm<<iface;
}
strm<<"\"\n";
};
showAddrs("EPICS_PVAS_INTF_ADDR_LIST", conf.interfaces);
showAddrs("EPICS_PVAS_BEACON_ADDR_LIST", conf.beaconDestinations);
showAddrs("EPICS_PVAS_IGNORE_ADDR_LIST", conf.ignoreAddrs);
strm<<indent{}<<"EPICS_PVAS_AUTO_BEACON_ADDR_LIST="<<(conf.auto_beacon?"YES":"NO")<<'\n';
strm<<indent{}<<"EPICS_PVAS_SERVER_PORT="<<conf.tcp_port<<'\n';
strm<<indent{}<<"EPICS_PVAS_BROADCAST_PORT="<<conf.udp_port<<'\n';
strm<<indent{}<<"EPICS_PVA_CONN_TMO="<<conf.tcpTimeout/tmoScale<<'\n';
return strm;
}
} // namespace server
namespace client {
static
void _fromDefs(Config& self, const std::map<std::string, std::string>& defs, bool useenv)
{
PickOne pickone{defs, useenv};
if(pickone({"EPICS_PVA_BROADCAST_PORT"})) {
try {
self.udp_port = parseTo<uint64_t>(pickone.val);
}catch(std::exception& e) {
log_warn_printf(clientsetup, "%s invalid integer : %s", pickone.name.c_str(), e.what());
}
}
if(self.udp_port==0u) {
log_warn_printf(clientsetup, "ignoring EPICS_PVA_BROADCAST_PORT=%d\n", 0);
self.udp_port = 5076;
}
if(pickone({"EPICS_PVA_SERVER_PORT", "EPICS_PVAS_SERVER_PORT"})) {
try {
self.tcp_port = parseTo<uint64_t>(pickone.val);
}catch(std::exception& e) {
log_warn_printf(clientsetup, "%s invalid integer : %s", pickone.name.c_str(), e.what());
}
}
if(self.tcp_port==0u && !self.nameServers.empty()) {
log_warn_printf(clientsetup, "ignoring EPICS_PVA_SERVER_PORT=%d\n", 0);
self.tcp_port = 5075;
}
if(pickone({"EPICS_PVA_ADDR_LIST"})) {
split_addr_into(pickone.name.c_str(), self.addressList, pickone.val, self.udp_port);
}
if(pickone({"EPICS_PVA_NAME_SERVERS"})) {
split_addr_into(pickone.name.c_str(), self.nameServers, pickone.val, self.tcp_port);
}
if(pickone({"EPICS_PVA_AUTO_ADDR_LIST"})) {
parse_bool(self.autoAddrList, pickone.name, pickone.val);
}
if(pickone({"EPICS_PVA_INTF_ADDR_LIST"})) {
split_addr_into(pickone.name.c_str(), self.interfaces, pickone.val, 0);
}
if(pickone({"EPICS_PVA_CONN_TMO"})) {
parse_timeout(self.tcpTimeout, pickone.name, pickone.val);
}
}
Config& Config::applyEnv()
{
_fromDefs(*this, std::map<std::string, std::string>(), true);
return *this;
}
Config& Config::applyDefs(const std::map<std::string, std::string>& defs)
{
_fromDefs(*this, defs, false);
return *this;
}
void Config::updateDefs(defs_t& defs) const
{
defs["EPICS_PVA_BROADCAST_PORT"] = SB()<<udp_port;
defs["EPICS_PVA_SERVER_PORT"] = SB()<<tcp_port;
defs["EPICS_PVA_AUTO_ADDR_LIST"] = autoAddrList ? "YES" : "NO";
defs["EPICS_PVA_ADDR_LIST"] = join_addr(addressList);
defs["EPICS_PVA_INTF_ADDR_LIST"] = join_addr(interfaces);
defs["EPICS_PVA_CONN_TMO"] = SB()<<tcpTimeout/tmoScale;
}
void Config::expand()
{
if(udp_port==0)
throw std::runtime_error("Client can't use UDP random port");
if(tcp_port==0)
tcp_port = 5075;
auto ifaces(parseAddresses(interfaces));
auto addrs(parseAddresses(addressList));
if(ifaces.empty())
ifaces.emplace_back(SockAddr::any(AF_INET));
if(autoAddrList) {
expandAddrList(ifaces, addrs);
addGroups(ifaces, addrs);
autoAddrList = false;
}
printAddresses(interfaces, ifaces);
removeDups(addrs);
printAddresses(addressList, addrs);
enforceTimeout(tcpTimeout);
}
std::ostream& operator<<(std::ostream& strm, const Config& conf)
{
bool first;
strm<<indent{}<<"EPICS_PVA_ADDR_LIST=\"";
first = true;
for(auto& iface : conf.addressList) {
if(first)
first = false;
else
strm<<' ';
strm<<iface;
}
strm<<"\"\n";
strm<<indent{}<<"EPICS_PVA_AUTO_ADDR_LIST="<<(conf.autoAddrList?"YES":"NO")<<'\n';
strm<<indent{}<<"EPICS_PVA_BROADCAST_PORT="<<conf.udp_port<<'\n';
strm<<indent{}<<"EPICS_PVA_SERVER_PORT="<<conf.tcp_port<<'\n';
strm<<indent{}<<"EPICS_PVA_CONN_TMO="<<conf.tcpTimeout/tmoScale<<'\n';
return strm;
}
} // namespace client
} // namespace pvxs