#include"Boss/Mod/InternetConnectionMonitor.hpp" #include"Boss/Mod/Rpc.hpp" #include"Boss/Mod/Waiter.hpp" #include"Boss/Msg/Init.hpp" #include"Boss/Msg/InternetOnline.hpp" #include"Boss/Msg/ListpeersAnalyzedResult.hpp" #include"Boss/Msg/NeedsConnect.hpp" #include"Boss/Msg/ProvideStatus.hpp" #include"Boss/Msg/SolicitStatus.hpp" #include"Boss/Msg/TaskCompletion.hpp" #include"Boss/concurrent.hpp" #include"Boss/log.hpp" #include"Boss/random_engine.hpp" #include"Ev/Io.hpp" #include"Ev/ThreadPool.hpp" #include"Ev/map.hpp" #include"Jsmn/Object.hpp" #include"Json/Out.hpp" #include"Net/Connector.hpp" #include"Net/SocketFd.hpp" #include"S/Bus.hpp" #include"Util/make_unique.hpp" #include #include #include #include #include namespace { /* How long before we timeout an LN-level ping attempt * with a peer and consider it to have failed? */ auto const ln_ping_timeout = double(30.0); /* List of servers that we try to connect to. * When we think we might get disconnected, we select * one of these by random and try to connect. * If the connection succeeds we consider ourselves * to be connected. * If the connection fails we try *all* of the below * and if at least one connects, we still consider * ourselves to be connected. * * Note that we connect, then disconnect immediately. * We just want to check the Internet, not spam these * servers. * * It would probably be better to "just" IP-level `ping` * the server, but that is suprisingly complicated. * Either we require some kind of `ping` command and * parse its output (including figuring out what * exactly its arguments are, its version, the exact * variant of the `ping` command, etc.), or we need to * open an ICMP socket, which, depending on OS policies, * might not be possible for non-root users (`ping` is * traditionally a setuid program). */ auto const servers = std::vector> { {"www.duckduckgo.com", 443} , {"www.google.com", 443} , {"www.yahoo.com", 443} , {"www.facebook.com", 443} , {"www.amazon.com", 443} , {"www.reddit.com", 443} , {"www.twitter.com", 443} , {"www.alibaba.com", 443} , {"www.netflix.com", 443} , {"www.baidu.com", 443} , {"www.paypal.com", 443} , {"www.microsoft.com", 443} , {"www.mozilla.org", 443} , {"1.1.1.1", 443} , {"www.squareup.com", 443} , {"wikipedia.org", 443} , {"www.bitcoin.org", 443} , {"bitcoinstats.com", 443} , {"www.iana.org", 443} , {"www.example.org", 443} , {"github.com", 443} , {"lightning.network", 443} , {"www.blockstream.com", 443} , {"acinq.co", 443} }; auto dist = std::uniform_int_distribution( 0, servers.size() - 1 ); } namespace Boss { namespace Mod { class InternetConnectionMonitor::Impl { private: S::Bus& bus; Ev::ThreadPool& threadpool; Boss::Mod::Waiter& waiter; Net::Connector *connector; Boss::Mod::Rpc *rpc; bool online; bool checking_connectivity; void start() { bus.subscribe< Msg::Init >([this](Msg::Init const& init) { connector = &init.connector; rpc = &init.rpc; checking_connectivity = true; return Boss::concurrent(server_check()); }); bus.subscribe< Msg::ListpeersAnalyzedResult >([this](Msg::ListpeersAnalyzedResult const& l) { if (!connector || checking_connectivity) return Ev::lift(); checking_connectivity = true; /* Copy connected nodes. */ return periodic_check(l); }); /* If the NeedsConnectSolicitor failed, it might be due to * Internet connection problems. */ bus.subscribe< Msg::TaskCompletion >([this](Msg::TaskCompletion const& comp) { if (!connector || checking_connectivity) return Ev::lift(); if (comp.comment != "NeedsConnectSolicitor") return Ev::lift(); if (!comp.failed) return Ev::lift(); checking_connectivity = true; return server_check(); }); /* Report status. */ bus.subscribe< Msg::SolicitStatus >([this](Msg::SolicitStatus const&) { auto status = Json::Out() .start_object() .field( "connection" , online ? std::string("online") : std::string("offline") ) .field( "checking_connectivity" , checking_connectivity ) .end_object() ; return bus.raise(Msg::ProvideStatus{ "internet", std::move(status) }); }); } Ev::Io server_check() { /* No peers, so query connectivity via server connecting. */ return Ev::lift().then([this]() { return check_server_connectivity() .then([this](bool n_online) { return set_online(n_online); }); }); } Ev::Io check_server_connectivity() { assert(connector); auto i = dist(Boss::random_engine); return check_server(servers[i]).then([this, i](bool res) { if (!res) return Boss::log( bus, Debug , "InternetConnectionMonitor: " "Could not contact %s, " "will contact other servers." , servers[i].first.c_str() ).then([this]() { return check_all_servers(); }); else return Ev::lift(true); }); } Ev::Io check_all_servers() { using std::placeholders::_1; return Ev::map( std::bind(&Impl::check_server, this, _1) , servers ).then([this](std::vector results) { auto bitvector = std::string(""); /* If one of the servers passed, it turns out we * are connected after all. */ auto res = false; for (auto const& r : results) { if (r) { res = true; bitvector += "1"; } else bitvector += "0"; } auto act = Ev::lift(); if (res) act += Boss::log( bus, Debug , "InternetConnectionMonitor: " "Other server contacted, " "we are online after all. " "(%s)" , bitvector.c_str() ); else act += Boss::log( bus, Debug , "InternetConnectionMonitor: " "Other servers also not " "reached. (%s)" , bitvector.c_str() ); return std::move(act).then([res]() { return Ev::lift(res); }); }); } Ev::Io check_server(std::pair const& s) { return threadpool.background([this, s]() { auto fd = connector->connect(s.first, s.second); return !!fd; }); } Ev::Io set_online(bool n_online) { auto was_online = online; online = n_online; auto act = Ev::lift(); act += Boss::log( bus, Info , "InternetConnectionMonitor: %s." , online ? "online" : "offline" ); if (was_online != online) act += bus.raise(Msg::InternetOnline{online}); if (online) { checking_connectivity = false; /* If we moved from offline->online, * raise NeedsConnect to get us back * to gossiping. */ if (!was_online) act += bus.raise(Msg::NeedsConnect()); } else act += offline_quick_loop(); return act; } /* If we are offline, we should keep trying to connect in a * quick 5-second loop. * This is not an attack on the rest of the Internet, because * we are offline and cannot access it anyway. */ Ev::Io offline_quick_loop() { return waiter.wait(5.0).then([this]() { return check_server_connectivity(); }).then([this](bool result) { if (result) return set_online(true); else return offline_quick_loop(); }); } Ev::Io periodic_check(Msg::ListpeersAnalyzedResult const& r) { /* No connected peers? * Query servers directly. */ if ( r.connected_channeled.empty() && r.connected_unchanneled.empty() ) return Boss::concurrent(server_check()); /* Otherwise pick a random peer to ping. */ auto peers = std::vector(); std::copy( r.connected_channeled.begin() , r.connected_channeled.end() , std::back_inserter(peers) ); std::copy( r.connected_unchanneled.begin() , r.connected_unchanneled.end() , std::back_inserter(peers) ); auto dist_peers = std::uniform_int_distribution( 0, peers.size() - 1 ); auto i = dist_peers(Boss::random_engine); return Boss::concurrent(ln_ping_check(peers[i])); } Ev::Io ln_ping_check(Ln::NodeId const& p) { /* Prefer to check our connectivity by pinging a * peer directly. * Use a timeout from Mod::Waiter; on some systems, * even if you are disconnected physically, TCP * connections are still kept alive for some time, * leading to long delays in the LN-level `ping`. */ return Ev::lift().then([this, p]() { return waiter.timed( ln_ping_timeout , do_ln_ping(p) ).catching< Waiter::TimedOut >([](Waiter::TimedOut const& _) { return Ev::lift(false); }); }).then([this, p](bool online) { if (online) return Boss::log( bus, Debug , "InternetConnectionMonitor: " "ping %s success." , std::string(p).c_str() ).then([this]() { return set_online(true); }); return Boss::log( bus, Debug , "InternetConnectionMonitor: " "ping %s failed." , std::string(p).c_str() ).then([this]() { /* Fall back to connecting to a server. */ return server_check(); }); }); } Ev::Io do_ln_ping(Ln::NodeId const& p) { return Ev::lift().then([this, p]() { auto p_s = std::string(p); auto presult = std::make_shared(); return rpc->command( "ping" , Json::Out() .start_object() .field("id", p_s) .end_object() ).then([](Jsmn::Object _) { return Ev::lift(true); }).catching([](RpcError const& e) { return Ev::lift(false); }).then([this, presult](bool result) { *presult = result; return Boss::log( bus, Debug , "InternetConnectionMonitor: " "do_ln_ping: ping %s." , result ? "success" : "failed" ); }).then([presult]() { return Ev::lift(*presult); }); }); } public: Impl() =delete; explicit Impl( S::Bus& bus_ , Ev::ThreadPool& threadpool_ , Boss::Mod::Waiter& waiter_ ) : bus(bus_) , threadpool(threadpool_) , waiter(waiter_) , connector(nullptr) , online(false) , checking_connectivity(false) { start(); } bool is_online() const { return online; } }; InternetConnectionMonitor::InternetConnectionMonitor (InternetConnectionMonitor&& o) : pimpl(std::move(o.pimpl)) { } InternetConnectionMonitor::~InternetConnectionMonitor() { } InternetConnectionMonitor::InternetConnectionMonitor ( S::Bus& bus , Ev::ThreadPool& threadpool , Boss::Mod::Waiter& waiter ) : pimpl(Util::make_unique(bus, threadpool, waiter)) { } bool InternetConnectionMonitor::is_online() const { return pimpl->is_online(); } }}