Merge pull request #1905 from majestrate/connect-out-2022-04-28

connect to routers even if we are decomissioned
This commit is contained in:
majestrate 2022-05-27 13:39:05 -04:00 committed by GitHub
commit 0331db494e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 407 additions and 98 deletions

View File

@ -126,32 +126,23 @@ namespace llarp
"provided the public-port option must also be specified.",
},
[this](std::string arg) {
if (not arg.empty())
{
llarp::LogInfo("public ip ", arg, " size ", arg.size());
if (arg.empty())
return;
nuint32_t addr{};
if (not addr.FromString(arg))
throw std::invalid_argument{stringify(arg, " is not a valid IPv4 address")};
if (arg.size() > 15)
throw std::invalid_argument(stringify("Not a valid IPv4 addr: ", arg));
if (IsIPv4Bogon(addr))
throw std::invalid_argument{
stringify(addr, " looks like it is not a publicly routable ip address")};
m_publicAddress.setAddress(arg);
}
m_PublicIP = addr;
});
conf.defineOption<std::string>("router", "public-address", Hidden, [this](std::string arg) {
if (not arg.empty())
{
llarp::LogWarn(
"*** WARNING: The config option [router]:public-address=",
arg,
" is deprecated, use public-ip=",
arg,
" instead to avoid this warning and avoid future configuration problems.");
if (arg.size() > 15)
throw std::invalid_argument(stringify("Not a valid IPv4 addr: ", arg));
m_publicAddress.setAddress(arg);
}
conf.defineOption<std::string>("router", "public-address", Hidden, [](std::string) {
throw std::invalid_argument{
"[router]:public-address option no longer supported, use [router]:public-ip and "
"[router]:public-port instead"};
});
conf.defineOption<int>(
@ -166,8 +157,7 @@ namespace llarp
[this](int arg) {
if (arg <= 0 || arg > std::numeric_limits<uint16_t>::max())
throw std::invalid_argument("public-port must be >= 0 and <= 65536");
m_publicAddress.setPort(arg);
m_PublicPort = ToNet(huint16_t{static_cast<uint16_t>(arg)});
});
conf.defineOption<int>(

View File

@ -54,7 +54,8 @@ namespace llarp
bool m_blockBogons = false;
IpAddress m_publicAddress;
std::optional<nuint32_t> m_PublicIP;
nuint16_t m_PublicPort;
int m_workerThreads = -1;
int m_numNetThreads = -1;

View File

@ -8,6 +8,7 @@
#include <utility>
#include <unordered_set>
#include <llarp/router/abstractrouter.hpp>
#include <oxenc/variant.h>
static constexpr auto LINK_LAYER_TICK_INTERVAL = 100ms;
@ -129,7 +130,7 @@ namespace llarp
}
bool
ILinkLayer::Configure(AbstractRouter* router, const std::string& ifname, int af, uint16_t port)
ILinkLayer::Configure(AbstractRouter* router, std::string ifname, int af, uint16_t port)
{
m_Router = router;
m_udp = m_Router->loop()->make_udp(
@ -142,11 +143,42 @@ namespace llarp
if (ifname == "*")
{
if (!AllInterfaces(af, m_ourAddr))
if (router->IsServiceNode())
{
if (auto maybe = router->OurPublicIP())
{
auto addr = var::visit([](auto&& addr) { return SockAddr{addr}; }, *maybe);
// service node outbound link
if (HasInterfaceAddress(addr.getIP()))
{
// we have our ip claimed on a local net interface
m_ourAddr = addr;
}
else if (auto maybe = net::AllInterfaces(addr))
{
// we do not have our claimed ip, nat or something?
m_ourAddr = *maybe;
}
else
return false; // the ultimate failure case
}
else
return false;
}
else if (auto maybe = net::AllInterfaces(SockAddr{"0.0.0.0"}))
{
// client outbound link
m_ourAddr = *maybe;
}
else
return false;
}
else
{
if (ifname == "0.0.0.0" and not GetBestNetIF(ifname))
throw std::invalid_argument{
"0.0.0.0 provided and we cannot find a valid ip to use, please set one "
"explicitly instead in the bind section instead of 0.0.0.0"};
if (const auto maybe = GetInterfaceAddr(ifname, af))
{
m_ourAddr = *maybe;
@ -157,10 +189,11 @@ namespace llarp
{
m_ourAddr = SockAddr{ifname + ":0"};
}
catch (const std::exception& e)
catch (const std::exception& ex)
{
LogError(stringify("Could not use ifname ", ifname, " to configure ILinkLayer"));
throw e;
LogError(
stringify("Could not use ifname ", ifname, " to configure ILinkLayer: ", ex.what()));
throw ex;
}
}
}

View File

@ -108,7 +108,7 @@ namespace llarp
SendTo_LL(const SockAddr& to, const llarp_buffer_t& pkt);
virtual bool
Configure(AbstractRouter* loop, const std::string& ifname, int af, uint16_t port);
Configure(AbstractRouter* loop, std::string ifname, int af, uint16_t port);
virtual std::shared_ptr<ILinkSession>
NewOutboundSession(const RouterContact& rc, const AddressInfo& ai) = 0;

View File

@ -27,6 +27,12 @@ namespace llarp
return lhs.rank < rhs.rank || lhs.ip < rhs.ip || lhs.port < rhs.port;
}
std::variant<nuint32_t, nuint128_t>
AddressInfo::IP() const
{
return SockAddr{ip}.getIP();
}
bool
AddressInfo::DecodeKey(const llarp_buffer_t& key, llarp_buffer_t* buf)
{

View File

@ -9,6 +9,8 @@
#include <string>
#include <vector>
#include <oxenc/variant.h>
/**
* address_info.hpp
*
@ -47,6 +49,10 @@ namespace llarp
void
fromSockAddr(const SockAddr& address);
/// get this as an explicit v4 or explicit v6
std::variant<nuint32_t, nuint128_t>
IP() const;
std::ostream&
print(std::ostream& stream, int level, int spaces) const;
};

View File

@ -576,29 +576,60 @@ namespace llarp
return addr.asIPv6();
}
bool
AllInterfaces(int af, SockAddr& result)
namespace net
{
if (af == AF_INET)
namespace
{
sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(0);
result = SockAddr{addr};
return true;
}
if (af == AF_INET6)
SockAddr
All(int af)
{
if (af == AF_INET)
{
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(0);
return SockAddr{addr};
}
if (af == AF_INET6)
{
sockaddr_in6 addr6{};
addr6.sin6_family = AF_INET6;
addr6.sin6_port = htons(0);
addr6.sin6_addr = IN6ADDR_ANY_INIT;
return SockAddr{addr6};
}
throw llarp::make_exception<std::invalid_argument>(af, " is not a valid address family");
}
} // namespace
std::optional<SockAddr>
AllInterfaces(SockAddr pub)
{
sockaddr_in6 addr6;
addr6.sin6_family = AF_INET6;
addr6.sin6_port = htons(0);
addr6.sin6_addr = IN6ADDR_ANY_INIT;
result = SockAddr{addr6};
return true;
std::optional<SockAddr> found;
IterAllNetworkInterfaces([pub, &found](auto* ifa) {
if (found)
return;
if (auto ifa_addr = ifa->ifa_addr)
{
if (ifa_addr->sa_family != pub.Family())
return;
SockAddr addr{*ifa->ifa_addr};
if (addr == pub)
found = addr;
}
});
// 0.0.0.0 is used in our compat shim as our public ip so we check for that special case
const auto zero = IPRange::FromIPv4(0, 0, 0, 0, 8);
// when we cannot find an address but we are looking for 0.0.0.0 just default to the old style
if (not found and (pub.isIPv4() and zero.Contains(pub.asIPv4())))
found = All(pub.Family());
return found;
}
return false;
}
} // namespace net
#if !defined(TESTNET)
static constexpr std::array bogonRanges_v6 = {
@ -692,5 +723,20 @@ namespace llarp
return false;
}
#endif
bool
HasInterfaceAddress(std::variant<nuint32_t, nuint128_t> ip)
{
bool found{false};
IterAllNetworkInterfaces([ip, &found](const auto* iface) {
if (found or iface == nullptr)
return;
if (auto addr = iface->ifa_addr;
addr and (addr->sa_family == AF_INET or addr->sa_family == AF_INET6))
{
found = SockAddr{*iface->ifa_addr}.getIP() == ip;
}
});
return found;
}
} // namespace llarp

View File

@ -52,6 +52,12 @@ namespace llarp
bool
IsIPv4Bogon(const huint32_t& addr);
inline bool
IsIPv4Bogon(const nuint32_t& addr)
{
return IsIPv4Bogon(ToHost(addr));
}
bool
IsBogon(const in6_addr& addr);
@ -61,8 +67,25 @@ namespace llarp
bool
IsBogonRange(const in6_addr& host, const in6_addr& mask);
bool
AllInterfaces(int af, SockAddr& addr);
/// get a sock addr we can use for all interfaces given our public address
namespace net
{
std::optional<SockAddr>
AllInterfaces(SockAddr pubaddr);
}
/// compat shim
// TODO: remove me
inline bool
AllInterfaces(int af, SockAddr& addr)
{
if (auto maybe = net::AllInterfaces(SockAddr{af == AF_INET ? "0.0.0.0" : "::"}))
{
addr = *maybe;
return true;
}
return false;
}
/// get first network interface with public address
bool
@ -92,4 +115,8 @@ namespace llarp
}
#endif
/// return true if we have a network interface with this ip
bool
HasInterfaceAddress(std::variant<nuint32_t, nuint128_t> ip);
} // namespace llarp

View File

@ -351,6 +351,14 @@ namespace llarp
return a;
}
std::variant<nuint32_t, nuint128_t>
SockAddr::getIP() const
{
if (isIPv4())
return getIPv4();
return getIPv6();
}
void
SockAddr::setIPv4(nuint32_t ip)
{

View File

@ -12,6 +12,7 @@
#include <string_view>
#include <string>
#include "net_int.hpp"
#include <oxenc/variant.h>
namespace llarp
{
@ -81,6 +82,14 @@ namespace llarp
std::string
hostString() const;
inline int
Family() const
{
if (isIPv6())
return AF_INET6;
return AF_INET;
}
/// Returns true if this is an empty SockAddr, defined by having no IP address set. An empty IP
/// address with a valid port is still considered empty.
///
@ -133,6 +142,8 @@ namespace llarp
getIPv6() const;
nuint32_t
getIPv4() const;
std::variant<nuint32_t, nuint128_t>
getIP() const;
/// in host order
huint128_t

View File

@ -220,6 +220,10 @@ namespace llarp
virtual const byte_t*
pubkey() const = 0;
/// get what our real public ip is if we can know it
virtual std::optional<std::variant<nuint32_t, nuint128_t>>
OurPublicIP() const = 0;
/// connect to N random routers
virtual void
ConnectToRandomRouters(int N) = 0;

View File

@ -1,5 +1,7 @@
#pragma once
#include <llarp/router_contact.hpp>
#include <llarp/util/time.hpp>
#include <optional>
namespace llarp
{
@ -15,7 +17,7 @@ namespace llarp
virtual bool
GossipRC(const RouterContact& rc) = 0;
using Time_t = std::chrono::milliseconds;
using Time_t = Duration_t;
virtual void
Decay(Time_t now) = 0;
@ -27,5 +29,17 @@ namespace llarp
/// return true if that rc is owned by us
virtual bool
IsOurRC(const RouterContact& rc) const = 0;
/// forget the replay filter entry given pubkey
virtual void
Forget(const RouterID& router) = 0;
/// returns the time point when we will send our next gossip at
virtual TimePoint_t
NextGossipAt() const = 0;
/// returns the time point when we sent our last gossip at or nullopt if we never did
virtual std::optional<TimePoint_t>
LastGossipAt() const = 0;
};
} // namespace llarp

View File

@ -27,9 +27,7 @@ namespace llarp
bool
RCGossiper::ShouldGossipOurRC(Time_t now) const
{
bool should = now >= (m_LastGossipedOurRC + GossipOurRCInterval);
LogWarn("ShouldGossipOurRC: ", should);
return should;
return now >= (m_LastGossipedOurRC + GossipOurRCInterval);
}
bool
@ -44,6 +42,30 @@ namespace llarp
m_Filter.Decay(now);
}
void
RCGossiper::Forget(const RouterID& pk)
{
m_Filter.Remove(pk);
if (m_OurRouterID == pk)
m_LastGossipedOurRC = 0s;
}
TimePoint_t
RCGossiper::NextGossipAt() const
{
if (auto maybe = LastGossipAt())
return *maybe + GossipOurRCInterval;
return DateClock_t::now();
}
std::optional<TimePoint_t>
RCGossiper::LastGossipAt() const
{
if (m_LastGossipedOurRC == 0s)
return std::nullopt;
return DateClock_t::time_point{m_LastGossipedOurRC};
}
bool
RCGossiper::GossipRC(const RouterContact& rc)
{

View File

@ -29,6 +29,15 @@ namespace llarp
void
Init(ILinkManager*, const RouterID&, AbstractRouter*);
void
Forget(const RouterID& router) override;
TimePoint_t
NextGossipAt() const override;
std::optional<TimePoint_t>
LastGossipAt() const override;
private:
RouterID m_OurRouterID;
Time_t m_LastGossipedOurRC = 0s;

View File

@ -415,9 +415,6 @@ namespace llarp
if (!FromConfig(conf))
throw std::runtime_error("FromConfig() failed");
if (!InitOutboundLinks())
throw std::runtime_error("InitOutboundLinks() failed");
if (not EnsureIdentity())
throw std::runtime_error("EnsureIdentity() failed");
@ -479,6 +476,25 @@ namespace llarp
and _rcLookupHandler.IsGreylisted(pubkey());
}
bool
Router::LooksDeregistered() const
{
return IsServiceNode() and whitelistRouters and _rcLookupHandler.HaveReceivedWhitelist()
and not _rcLookupHandler.SessionIsAllowed(pubkey());
}
bool
Router::ShouldTestOtherRouters() const
{
if (not IsServiceNode())
return false;
if (not whitelistRouters)
return true;
if (not _rcLookupHandler.HaveReceivedWhitelist())
return false;
return _rcLookupHandler.SessionIsAllowed(pubkey());
}
bool
Router::SessionToRouterAllowed(const RouterID& router) const
{
@ -577,8 +593,8 @@ namespace llarp
transport_keyfile = m_keyManager->m_transportKeyPath;
ident_keyfile = m_keyManager->m_idKeyPath;
if (not conf.router.m_publicAddress.isEmpty())
_ourAddress = conf.router.m_publicAddress.createSockAddr();
if (auto maybe = conf.router.m_PublicIP)
_ourAddress = SockAddr{*maybe, conf.router.m_PublicPort};
RouterContact::BlockBogons = conf.router.m_blockBogons;
@ -709,14 +725,15 @@ namespace llarp
if (inboundLinks.empty() and m_isServiceNode)
{
const auto& publicAddr = conf.router.m_publicAddress;
if (publicAddr.isEmpty() or not publicAddr.hasPort())
if (_ourAddress)
{
throw std::runtime_error(
"service node enabled but could not find a public IP to bind to; you need to set the "
"public-ip= and public-port= options");
inboundLinks.push_back(LinksConfig::LinkInfo{
_ourAddress->hostString(), _ourAddress->Family(), _ourAddress->getPort()});
}
inboundLinks.push_back(LinksConfig::LinkInfo{"0.0.0.0", AF_INET, *publicAddr.getPort()});
else
throw std::runtime_error{
"service node enabled but could not find a public IP to bind to; you need to set the "
"public-ip= and public-port= options"};
}
// create inbound links, if we are a service node
@ -850,7 +867,17 @@ namespace llarp
ss << " snode | known/svc/clients: " << nodedb()->NumLoaded() << "/"
<< NumberOfConnectedRouters() << "/" << NumberOfConnectedClients() << " | "
<< pathContext().CurrentTransitPaths() << " active paths | "
<< "block " << (m_lokidRpcClient ? m_lokidRpcClient->BlockHeight() : 0);
<< "block " << (m_lokidRpcClient ? m_lokidRpcClient->BlockHeight() : 0) << " | gossip: "
<< "(next/last) " << time_delta<std::chrono::seconds>{_rcGossiper.NextGossipAt()}
<< " / ";
if (auto maybe = _rcGossiper.LastGossipAt())
{
ss << time_delta<std::chrono::seconds>{*maybe};
}
else
{
ss << "never";
}
}
else
{
@ -889,15 +916,24 @@ namespace llarp
const bool gotWhitelist = _rcLookupHandler.HaveReceivedWhitelist();
const bool isSvcNode = IsServiceNode();
const bool decom = LooksDecommissioned();
bool shouldGossip = isSvcNode and whitelistRouters and gotWhitelist
and _rcLookupHandler.SessionIsAllowed(pubkey());
if (_rc.ExpiresSoon(now, std::chrono::milliseconds(randint() % 10000))
|| (now - _rc.last_updated) > rcRegenInterval)
if (isSvcNode
and (_rc.ExpiresSoon(now, std::chrono::milliseconds(randint() % 10000)) or (now - _rc.last_updated) > rcRegenInterval))
{
LogInfo("regenerating RC");
if (!UpdateOurRC(false))
LogError("Failed to update our RC");
if (UpdateOurRC())
{
// our rc changed so we should gossip it
shouldGossip = true;
// remove our replay entry so it goes out
_rcGossiper.Forget(pubkey());
}
else
LogError("failed to update our RC");
}
else if (whitelistRouters and gotWhitelist and _rcLookupHandler.SessionIsAllowed(pubkey()))
if (shouldGossip)
{
// if we have the whitelist enabled, we have fetched the list and we are in either
// the white or grey list, we want to gossip our RC
@ -982,17 +1018,19 @@ namespace llarp
connectToNum = strictConnect;
}
if (decom)
if (auto dereg = LooksDeregistered(); (dereg or decom) and now >= m_NextDecommissionWarn)
{
// complain about being deregistered
if (now >= m_NextDecommissionWarn)
{
constexpr auto DecommissionWarnInterval = 30s;
LogError("We are running as a service node but we seem to be decommissioned");
m_NextDecommissionWarn = now + DecommissionWarnInterval;
}
constexpr auto DecommissionWarnInterval = 30s;
LogError(
"We are running as a service node but we seem to be ",
dereg ? "deregistered" : "decommissioned");
m_NextDecommissionWarn = now + DecommissionWarnInterval;
}
else if (connected < connectToNum)
// if we need more sessions to routers and we are not a service node kicked from the network
// we shall connect out to others
if (connected < connectToNum and not LooksDeregistered())
{
size_t dlt = connectToNum - connected;
LogDebug("connecting to ", dlt, " random routers to keep alive");
@ -1013,7 +1051,8 @@ namespace llarp
if (m_peerDb)
{
// TODO: throttle this?
// TODO: need to capture session stats when session terminates / is removed from link manager
// TODO: need to capture session stats when session terminates / is removed from link
// manager
_linkManager.updatePeerDb(m_peerDb);
if (m_peerDb->shouldFlush(now))
@ -1212,6 +1251,12 @@ namespace llarp
return false;
}
if (not InitOutboundLinks())
{
LogError("failed to init outbound links");
return false;
}
if (IsServiceNode())
{
if (!SaveRC())
@ -1293,8 +1338,10 @@ namespace llarp
// dont run tests if we are not running or we are stopping
if (not _running)
return;
// dont run tests if we are decommissioned
if (LooksDecommissioned())
// dont run tests if we think we should not test other routers
// this occurs when we are deregistered or do not have the service node list
// yet when we expect to have one.
if (not ShouldTestOtherRouters())
return;
auto tests = m_routerTesting.get_failing();
if (auto maybe = m_routerTesting.next_random(this))
@ -1520,6 +1567,22 @@ namespace llarp
return ep and ep->HasExit();
}
std::optional<std::variant<nuint32_t, nuint128_t>>
Router::OurPublicIP() const
{
if (_ourAddress)
return _ourAddress->getIP();
std::optional<std::variant<nuint32_t, nuint128_t>> found;
_linkManager.ForEachInboundLink([&found](const auto& link) {
if (found)
return;
AddressInfo ai;
if (link->GetOurAddressInfo(ai))
found = ai.IP();
});
return found;
}
bool
Router::InitOutboundLinks()
{

View File

@ -102,6 +102,9 @@ namespace llarp
return _dht;
}
std::optional<std::variant<nuint32_t, nuint128_t>>
OurPublicIP() const override;
util::StatusObject
ExtractStatus() const override;
@ -197,6 +200,14 @@ namespace llarp
bool
LooksDecommissioned() const;
/// return true if we look like we are a deregistered service node
bool
LooksDeregistered() const;
/// return true if we look like we are allowed and able to test other routers
bool
ShouldTestOtherRouters() const;
std::optional<SockAddr> _ourAddress;
EventLoop_ptr _loop;

View File

@ -72,6 +72,12 @@ namespace llarp
m_CacheInterval = interval;
}
void
Remove(const Val_t& val)
{
m_Values.erase(val);
}
private:
template <typename Predicate_t>
void

View File

@ -35,6 +35,17 @@ namespace llarp
return o.str();
}
/// util for constructing an exception with a message constructed from a set of whatever passed
/// into stringify
/// E must be derived from std::exception here
template <typename E, typename... T>
E
make_exception(T&&... stuff)
{
static_assert(std::is_base_of_v<std::exception, E>);
return E{stringify(std::forward<T>(stuff)...)};
}
using namespace std::literals;
/// Returns true if the first string is equal to the second string, compared case-insensitively.

View File

@ -1,20 +1,24 @@
#include "time.hpp"
#include <chrono>
#include <iomanip>
namespace llarp
{
using Clock_t = std::chrono::system_clock;
template <typename Res, typename Clock>
static Duration_t
time_since_epoch(std::chrono::time_point<Clock> point)
namespace
{
return std::chrono::duration_cast<Res>(point.time_since_epoch());
}
using Clock_t = std::chrono::system_clock;
const static auto started_at_system = Clock_t::now();
template <typename Res, typename Clock>
static Duration_t
time_since_epoch(std::chrono::time_point<Clock> point)
{
return std::chrono::duration_cast<Res>(point.time_since_epoch());
}
const static auto started_at_steady = std::chrono::steady_clock::now();
const static auto started_at_system = Clock_t::now();
const static auto started_at_steady = std::chrono::steady_clock::now();
} // namespace
uint64_t
ToMS(Duration_t ms)
@ -74,4 +78,11 @@ namespace llarp
out.fill(old_fill);
return out << "s";
}
std::ostream&
operator<<(std::ostream& out, const TimePoint_t& tp)
{
auto t = TimePoint_t::clock::to_time_t(tp);
return out << std::put_time(std::localtime(&t), "%c %Z");
}
} // namespace llarp

View File

@ -26,4 +26,36 @@ namespace llarp
nlohmann::json
to_json(const Duration_t& t);
std::ostream&
operator<<(std::ostream& out, const TimePoint_t& t);
template <typename Time_Duration>
struct time_delta
{
const TimePoint_t at;
std::ostream&
operator()(std::ostream& out) const
{
const auto dlt = std::chrono::duration_cast<Time_Duration>(TimePoint_t::clock::now() - at);
if (dlt > 0s)
return out << std::chrono::duration_cast<Duration_t>(dlt) << " ago ";
else if (dlt < 0s)
return out << "in " << std::chrono::duration_cast<Duration_t>(-dlt);
else
return out << "now";
}
};
inline std::ostream&
operator<<(std::ostream& out, const time_delta<std::chrono::seconds>& td)
{
return td(out);
}
inline std::ostream&
operator<<(std::ostream& out, const time_delta<std::chrono::milliseconds>& td)
{
return td(out);
}
} // namespace llarp

View File

@ -14,6 +14,9 @@ namespace llarp
/// convert to milliseconds
uint64_t
ToMS(Duration_t duration);
using DateClock_t = std::chrono::system_clock;
using TimePoint_t = DateClock_t::time_point;
} // namespace llarp
using llarp_time_t = llarp::Duration_t;

View File

@ -39,11 +39,6 @@ namespace llarp
[](RouterConfig& self) { return self.m_dataDir.c_str(); },
[](RouterConfig& self, std::string dir) { self.m_dataDir = dir; })
.def_readwrite("blockBogons", &RouterConfig::m_blockBogons)
.def(
"overrideAddress",
[](RouterConfig& self, std::string addr) {
self.m_publicAddress = llarp::IpAddress(addr);
})
.def_readwrite("workerThreads", &RouterConfig::m_workerThreads)
.def_readwrite("numNetThreads", &RouterConfig::m_numNetThreads)
.def_readwrite("JobQueueSize", &RouterConfig::m_JobQueueSize);