Files
pvxs/tools/pvxvct.cpp
T
Michael Davidsaver 466044d6a5 initial
2019-10-19 20:17:39 -07:00

372 lines
12 KiB
C++

/**
* Copyright - See the COPYRIGHT that is included with this distribution.
* pvAccessCPP is distributed subject to a Software License Agreement found
* in file LICENSE that is included with this distribution.
*/
#include <event2/event.h>
#include <cstring>
#include <stdexcept>
#include <iostream>
#include <sstream>
#include <vector>
#include <set>
#include <tuple>
#include <regex>
#if !defined(_WIN32)
#include <signal.h>
#define USE_SIGNAL
#endif
#include <epicsEvent.h>
#include <epicsGetopt.h>
#include <osiSock.h>
#include <pvxs/log.h>
#include <udp_collector.h>
#include <pvaproto.h>
namespace pva = pvxsimpl;
namespace {
DEFINE_LOGGER(out, "pvxvct");
epicsEvent done;
#ifdef USE_SIGNAL
void alldone(int num)
{
(void)num;
done.signal();
}
#endif
// parse hostname, IP, or IP+netmask
std::tuple<uint32_t, uint32_t>
parsePeer(const char *optarg)
{
// nameorip
// nameorip/##
// nameorip/###.###.###.###
// static is safe as we only use from main() thread
static std::regex netname("([^/:]*)(?:/([0-9.]+))?");
std::cmatch match;
if(!std::regex_match(optarg, match, netname)) {
throw std::runtime_error(pva::SB()<<"Expected host name or IP range. not "<<optarg);
}
in_addr addr, mask;
std::cerr<<"XXX '"<<match[0]<<"' '"<<match[1]<<"' '"<<match[2]<<"' "<<match.size()<<"\n";
if(hostToIPAddr(match[1].str().c_str(), &addr)) {
throw std::runtime_error(pva::SB()<<"Expected a host name or IP. not "<<match[1].str());
}
mask.s_addr = INADDR_BROADCAST;
if(match[2].length()) {
auto smask = match[2].str();
if(smask.find_first_of('.')!=smask.npos) {
if(!evutil_inet_pton(AF_INET, smask.c_str(), &mask)) {
throw std::runtime_error(pva::SB()<<"Expected netmask. not "<<smask);
}
} else {
// only # of bits. eg. "/24"
std::istringstream strm(match[2].str());
unsigned nbit=0;
if((strm>>nbit).good()) {
throw std::runtime_error(pva::SB()<<"Expected number of bits. not "<<match[2]);
}
mask.s_addr = htonl(0xffffffff<<(32-nbit));
}
}
// 1.2.3.4/24 === 1.2.3.0/24
addr.s_addr &= mask.s_addr;
return std::make_tuple(addr.s_addr, mask.s_addr);
}
void usage(const char *name)
{
std::cerr<<"Usage: "<<name<<" [-C|-S] [-B hostip[:port]] [-H hostip]\n"
"\n"
"PV Access Virtual Cable Tester\n"
"\n"
"Assist in troubleshooting network (mis)configuration by listening\n"
"for (some) PVA client/server UDP traffic.\n"
"\n"
" -h Print this message\n"
" -C Show only client Searches\n"
" -S Show only server Beacons\n"
" -B hostip[:port] Listen on the given interface(s). May be repeated.\n"
" -H host Show only message sent from this peer. May be repeated.\n"
" -P pvname Show only searches for this PV name. May be repeated.\n"
<<std::endl;
}
} // namespace
int main(int argc, char *argv[])
{
try {
// group options used from callback
struct {
bool client = false, server = false;
// IP, netmask, port
// stored in network byte order
std::vector<std::tuple<uint32_t, uint32_t>> peers;
std::set<std::string> pvnames;
} opts;
std::vector<pva::evsockaddr> bindaddrs;
{
int opt;
while ((opt = getopt(argc, argv, "hCSH:B:P:")) != -1) {
switch(opt) {
case 'h':
usage(argv[0]);
return 0;
default:
usage(argv[0]);
std::cerr<<"\nUnknown argument: "<<char(opt)<<std::endl;
return 1;
case 'C':
opts.client = true;
break;
case 'S':
opts.server = true;
break;
case 'B': {
pva::evsockaddr addr;
int slen = sizeof(addr->ss);
if(evutil_parse_sockaddr_port(optarg, &addr->sa, &slen)) {
throw std::runtime_error(pva::SB()<<"Expected address[:port] to bind. Not "<<optarg);
}
if(addr.port()==0)
addr.setPort(5076);
bindaddrs.push_back(addr);
}
break;
case 'P':
opts.pvnames.insert(optarg);
break;
case 'H':
opts.peers.push_back(parsePeer(optarg));
break;
}
}
}
// apply defaults
if(!opts.client && !opts.server) {
opts.client = opts.server = true;
}
if(bindaddrs.empty()) {
bindaddrs.emplace_back(pva::evsockaddr::any(AF_INET, 5076));
}
pva::logger_level_set("pvxvct", PLVL_INFO);
pva::logger_config_env(); // from $PVXS_LOG
log_printf(out, PLVL_DEBUG, "Show Search: %s\nShow Beacon: %s\n", opts.client?"yes":"no", opts.server?"yes":"no");
if(opts.client && opts.pvnames.empty()) {
log_printf(out, PLVL_DEBUG, "Show all PV names\n");
} else {
for(const auto& name : opts.pvnames) {
log_printf(out, PLVL_DEBUG, "Show PV: %s\n", name.c_str());
}
}
if(opts.peers.empty()) {
log_printf(out, PLVL_DEBUG, "No peer filter\n");
} else if(pva::log_test(out, PLVL_DEBUG)) {
for(const auto& tup : opts.peers) {
in_addr addr, netmask;
std::tie(addr.s_addr, netmask.s_addr) = tup;
char abuf[16];
char nbuf[16];
evutil_inet_ntop(AF_INET, &addr, abuf, sizeof(abuf));
evutil_inet_ntop(AF_INET, &netmask, nbuf, sizeof(nbuf));
log_printf(out, PLVL_DEBUG, "Show from %s/%s\n", abuf, nbuf);
}
}
auto cb = [&opts](const pva::UDPMsg& msg)
{
// later, from worker thread
// filter by sender
if(!opts.peers.empty()) {
if(msg.src.family()!=AF_INET)
return;
bool match = false;
for(auto& tup : opts.peers) {
uint32_t addr, mask;
std::tie(addr, mask) = tup;
if((msg.src->in.sin_addr.s_addr&mask)==addr) {
match = true;
break;
}
}
if(!match)
return;
}
bool showpeer=false;
auto lazypeer = [&showpeer, &msg]() {
if(!showpeer)
log_printf(out, PLVL_INFO, "From %s\n", msg.src.tostring().c_str());
showpeer = true;
};
// allow that one UDP packet may contain several PVA messages
for(unsigned i=0; !msg.msgs[i].empty(); i++)
{
auto M = msg.msgs[i];
auto be = M[2]&pva::pva_flags::MSB;
auto cmd = M[3];
M+=4; // skip header
uint32_t blen;
pva::from_wire(M, blen, be);
switch(cmd) {
case pva::pva_app_msg::OriginTag:
log_printf(out, PLVL_WARN, "Peer sends ORIGIN_TAG by unicast/broadcast.\n");
break;
case pva::pva_app_msg::Search: {
uint32_t id;
uint8_t flags;
pva::evsockaddr replyAddr;
pva::from_wire(M, id, be);
pva::from_wire(M, flags, be);
M += 3; // unused/reserved
pva::from_wire(M, replyAddr, be);
uint16_t port = 0;
pva::from_wire(M, port, be);
replyAddr.setPort(port);
// so far, only "tcp" transport has ever been seen.
// however, we will consider and ignore any others which might appear
bool foundtcp = false;
size_t nproto=0;
pva::from_wire(M, pva::Size<size_t>(nproto), be);
for(size_t i=0; i<nproto && !M.err; i++) {
size_t nchar=0;
pva::from_wire(M, pva::Size<size_t>(nchar), be);
if(M.size()>=3 && nchar==3 && M[0]=='t' && M[1]=='c' && M[2]=='p') {
foundtcp = true;
M += 3;
break;
}
}
if(!foundtcp && !M.err) {
// so far, not something which should actually happen
log_printf(out, PLVL_DEBUG, " Search w/o proto \"tcp\"\n");
continue;
}
// one Search message can include many PV names.
uint16_t nchan=0;
pva::from_wire(M, nchan, be);
for(size_t i=0; i<nchan && !M.err; i++) {
uint32_t id=0xffffffff; // poison
size_t chlen;
pva::from_wire(M, id, be);
pva::from_wire(M, pva::Size<size_t>(chlen), be);
if(opts.client && chlen<=M.size() && !M.err) {
std::string pvname(reinterpret_cast<const char*>(M.pos), chlen);
if(opts.pvnames.empty() || opts.pvnames.find(pvname)!=opts.pvnames.end()) {
lazypeer();
log_printf(out, PLVL_INFO, " Search 0x%08x '%s' (rsvp %s)\n",
unsigned(id), pvname.c_str(), replyAddr.tostring().c_str());
}
}
M += chlen;
}
break;
}
case pva::pva_app_msg::Beacon: {
uint8_t guid[12] = {};
uint8_t seq =0;
pva::evsockaddr addr;
uint16_t port = 0;
if(M.size()>=12) {
std::memcpy(guid, M.pos, 12);
}
M += 12;
M += 1; // flags. unused
pva::from_wire(M, seq, be);
M += 1; // "change" count. unused
pva::from_wire(M, addr, be);
pva::from_wire(M, port, be);
addr.setPort(port);
size_t protolen=0;
pva::from_wire(M, pva::Size<size_t>(protolen), be);
M += protolen; // ignore string
// ignore remaining "server status" blob
if(opts.server && !M.err) {
lazypeer();
log_printf(out, PLVL_INFO, " Beacon %02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x %s seq %u\n",
guid[0], guid[1], guid[2], guid[3], guid[4], guid[5], guid[6], guid[7], guid[8], guid[9], guid[10], guid[11],
addr.tostring().c_str(), seq);
}
}
break;
default:
log_printf(out, PLVL_WARN, "unknown command 0x%02x\n", cmd);
}
if(M.err) {
log_printf(out, PLVL_ERR, " Error while decoding\n");
}
}
};
std::vector<std::unique_ptr<pva::UDPListener>> listeners;
listeners.reserve(bindaddrs.size());
for(auto& baddr : bindaddrs) {
listeners.push_back(pva::UDPManager::instance()
.subscribe(baddr, cb));
log_printf(out, PLVL_DEBUG, "Bind: %s\n", baddr.tostring().c_str());
}
#ifdef USE_SIGNAL
signal(SIGINT, alldone);
signal(SIGTERM, alldone);
signal(SIGQUIT, alldone);
#endif
done.wait();
log_printf(out, PLVL_INFO, "Done\n");
errlogFlush();
return 0;
}catch(std::runtime_error& e) {
errlogFlush();
std::cerr<<"Error: "<<e.what()<<std::endl;
return 1;
}
}