// Copyright (c) 2011 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "net/dns/async_host_resolver.h" #include "base/bind.h" #include "base/scoped_ptr.h" #include "net/base/host_cache.h" #include "net/base/net_log.h" #include "net/base/rand_callback.h" #include "net/base/sys_addrinfo.h" #include "net/base/test_host_resolver_observer.h" #include "net/dns/dns_test_util.h" #include "net/socket/socket_test_util.h" #include "testing/gtest/include/gtest/gtest.h" namespace net { namespace { void VerifyAddressList(const std::vector& ip_addresses, int port, const AddressList& addrlist) { ASSERT_LT(0u, ip_addresses.size()); ASSERT_NE(static_cast(NULL), addrlist.head()); IPAddressNumber ip_number; const struct addrinfo* ainfo = addrlist.head(); for (std::vector::const_iterator i = ip_addresses.begin(); i != ip_addresses.end(); ++i, ainfo = ainfo->ai_next) { ASSERT_NE(static_cast(NULL), ainfo); EXPECT_EQ(sizeof(struct sockaddr_in), ainfo->ai_addrlen); const struct sockaddr* sa = ainfo->ai_addr; const struct sockaddr_in* sa_in = (const struct sockaddr_in*) sa; EXPECT_TRUE(htons(port) == sa_in->sin_port); EXPECT_STREQ(*i, NetAddressToString(sa, ainfo->ai_addrlen).c_str()); } ASSERT_EQ(static_cast(NULL), ainfo); } } // namespace static const int kPortNum = 80; static const size_t kMaxTransactions = 2; static const size_t kMaxPendingRequests = 1; static int transaction_ids[] = {0, 1, 2, 3}; // The following fixture sets up an environment for four different lookups // with their data defined in dns_test_util.h. All tests make use of these // predefined variables instead of each defining their own, to avoid // boilerplate code in every test. Assuming every coming query is for a // distinct hostname, as |kMaxTransactions| is set to 2 and // |kMaxPendingRequests| is set to 1, first two queries start immediately // and the next one is sent to pending queue; as a result, the next query // should either fail itself or cause the pending query to fail depending // on its priority. class AsyncHostResolverTest : public testing::Test { public: AsyncHostResolverTest() : info0_(HostPortPair(kT0HostName, kPortNum)), info1_(HostPortPair(kT1HostName, kPortNum)), info2_(HostPortPair(kT2HostName, kPortNum)), info3_(HostPortPair(kT3HostName, kPortNum)), ip_addresses0_(kT0IpAddresses, kT0IpAddresses + arraysize(kT0IpAddresses)), ip_addresses1_(kT1IpAddresses, kT1IpAddresses + arraysize(kT1IpAddresses)), ip_addresses2_(kT2IpAddresses, kT2IpAddresses + arraysize(kT2IpAddresses)), ip_addresses3_(kT3IpAddresses, kT3IpAddresses + arraysize(kT3IpAddresses)), test_prng_(std::deque( transaction_ids, transaction_ids + arraysize(transaction_ids))) { rand_int_cb_ = base::Bind(&TestPrng::GetNext, base::Unretained(&test_prng_)); // AF_INET only for now. info0_.set_address_family(ADDRESS_FAMILY_IPV4); info1_.set_address_family(ADDRESS_FAMILY_IPV4); info2_.set_address_family(ADDRESS_FAMILY_IPV4); info3_.set_address_family(ADDRESS_FAMILY_IPV4); // Setup socket read/writes for transaction 0. writes0_.push_back( MockWrite(true, reinterpret_cast(kT0QueryDatagram), arraysize(kT0QueryDatagram))); reads0_.push_back( MockRead(true, reinterpret_cast(kT0ResponseDatagram), arraysize(kT0ResponseDatagram))); data0_.reset(new StaticSocketDataProvider(&reads0_[0], reads0_.size(), &writes0_[0], writes0_.size())); // Setup socket read/writes for transaction 1. writes1_.push_back( MockWrite(true, reinterpret_cast(kT1QueryDatagram), arraysize(kT1QueryDatagram))); reads1_.push_back( MockRead(true, reinterpret_cast(kT1ResponseDatagram), arraysize(kT1ResponseDatagram))); data1_.reset(new StaticSocketDataProvider(&reads1_[0], reads1_.size(), &writes1_[0], writes1_.size())); // Setup socket read/writes for transaction 2. writes2_.push_back( MockWrite(true, reinterpret_cast(kT2QueryDatagram), arraysize(kT2QueryDatagram))); reads2_.push_back( MockRead(true, reinterpret_cast(kT2ResponseDatagram), arraysize(kT2ResponseDatagram))); data2_.reset(new StaticSocketDataProvider(&reads2_[0], reads2_.size(), &writes2_[0], writes2_.size())); // Setup socket read/writes for transaction 3. writes3_.push_back( MockWrite(true, reinterpret_cast(kT3QueryDatagram), arraysize(kT3QueryDatagram))); reads3_.push_back( MockRead(true, reinterpret_cast(kT3ResponseDatagram), arraysize(kT3ResponseDatagram))); data3_.reset(new StaticSocketDataProvider(&reads3_[0], reads3_.size(), &writes3_[0], writes3_.size())); factory_.AddSocketDataProvider(data0_.get()); factory_.AddSocketDataProvider(data1_.get()); factory_.AddSocketDataProvider(data2_.get()); factory_.AddSocketDataProvider(data3_.get()); IPEndPoint dns_server; bool rv0 = CreateDnsAddress(kDnsIp, kDnsPort, &dns_server); DCHECK(rv0); resolver_.reset( new AsyncHostResolver( dns_server, kMaxTransactions, kMaxPendingRequests, rand_int_cb_, HostCache::CreateDefaultCache(), &factory_, NULL)); } protected: AddressList addrlist0_, addrlist1_, addrlist2_, addrlist3_; HostResolver::RequestInfo info0_, info1_, info2_, info3_; std::vector writes0_, writes1_, writes2_, writes3_; std::vector reads0_, reads1_, reads2_, reads3_; scoped_ptr data0_, data1_, data2_, data3_; std::vector ip_addresses0_, ip_addresses1_, ip_addresses2_, ip_addresses3_; MockClientSocketFactory factory_; TestPrng test_prng_; RandIntCallback rand_int_cb_; scoped_ptr resolver_; TestCompletionCallback callback0_, callback1_, callback2_, callback3_; }; TEST_F(AsyncHostResolverTest, EmptyHostLookup) { info0_.set_host_port_pair(HostPortPair("", kPortNum)); int rv = resolver_->Resolve(info0_, &addrlist0_, &callback0_, NULL, BoundNetLog()); EXPECT_EQ(ERR_NAME_NOT_RESOLVED, rv); } TEST_F(AsyncHostResolverTest, IPv4LiteralLookup) { const char* kIPLiteral = "192.168.1.2"; info0_.set_host_port_pair(HostPortPair(kIPLiteral, kPortNum)); info0_.set_host_resolver_flags(HOST_RESOLVER_CANONNAME); int rv = resolver_->Resolve(info0_, &addrlist0_, &callback0_, NULL, BoundNetLog()); EXPECT_EQ(OK, rv); std::vector ip_addresses(1, kIPLiteral); VerifyAddressList(ip_addresses, kPortNum, addrlist0_); EXPECT_STREQ(kIPLiteral, addrlist0_.head()->ai_canonname); } TEST_F(AsyncHostResolverTest, IPv6LiteralLookup) { info0_.set_host_port_pair(HostPortPair("2001:db8:0::42", kPortNum)); int rv = resolver_->Resolve(info0_, &addrlist0_, &callback0_, NULL, BoundNetLog()); // When support for IPv6 is added, this should succeed. EXPECT_EQ(ERR_NAME_NOT_RESOLVED, rv); } TEST_F(AsyncHostResolverTest, CachedLookup) { int rv = resolver_->ResolveFromCache(info0_, &addrlist0_, BoundNetLog()); EXPECT_EQ(ERR_DNS_CACHE_MISS, rv); // Cache the result of |info0_| lookup. rv = resolver_->Resolve(info0_, &addrlist0_, &callback0_, NULL, BoundNetLog()); EXPECT_EQ(ERR_IO_PENDING, rv); rv = callback0_.WaitForResult(); EXPECT_EQ(OK, rv); VerifyAddressList(ip_addresses0_, kPortNum, addrlist0_); // Now lookup |info0_| from cache only, store results in |addrlist1_|, // should succeed synchronously. rv = resolver_->ResolveFromCache(info0_, &addrlist1_, BoundNetLog()); EXPECT_EQ(OK, rv); VerifyAddressList(ip_addresses0_, kPortNum, addrlist1_); } TEST_F(AsyncHostResolverTest, InvalidHostNameLookup) { const std::string kHostName1(64, 'a'); info0_.set_host_port_pair(HostPortPair(kHostName1, kPortNum)); int rv = resolver_->Resolve(info0_, &addrlist0_, &callback0_, NULL, BoundNetLog()); EXPECT_EQ(ERR_NAME_NOT_RESOLVED, rv); const std::string kHostName2(4097, 'b'); info0_.set_host_port_pair(HostPortPair(kHostName2, kPortNum)); rv = resolver_->Resolve(info0_, &addrlist0_, &callback0_, NULL, BoundNetLog()); EXPECT_EQ(ERR_NAME_NOT_RESOLVED, rv); } TEST_F(AsyncHostResolverTest, Lookup) { int rv = resolver_->Resolve(info0_, &addrlist0_, &callback0_, NULL, BoundNetLog()); EXPECT_EQ(ERR_IO_PENDING, rv); rv = callback0_.WaitForResult(); EXPECT_EQ(OK, rv); VerifyAddressList(ip_addresses0_, kPortNum, addrlist0_); } TEST_F(AsyncHostResolverTest, ConcurrentLookup) { int rv0 = resolver_->Resolve(info0_, &addrlist0_, &callback0_, NULL, BoundNetLog()); int rv1 = resolver_->Resolve(info1_, &addrlist1_, &callback1_, NULL, BoundNetLog()); int rv2 = resolver_->Resolve(info2_, &addrlist2_, &callback2_, NULL, BoundNetLog()); EXPECT_EQ(ERR_IO_PENDING, rv0); EXPECT_EQ(ERR_IO_PENDING, rv1); EXPECT_EQ(ERR_IO_PENDING, rv2); rv0 = callback0_.WaitForResult(); EXPECT_EQ(OK, rv0); VerifyAddressList(ip_addresses0_, kPortNum, addrlist0_); rv1 = callback1_.WaitForResult(); EXPECT_EQ(OK, rv1); VerifyAddressList(ip_addresses1_, kPortNum, addrlist1_); rv2 = callback2_.WaitForResult(); EXPECT_EQ(OK, rv2); VerifyAddressList(ip_addresses2_, kPortNum, addrlist2_); EXPECT_EQ(3u, factory_.udp_client_sockets().size()); } TEST_F(AsyncHostResolverTest, SameHostLookupsConsumeSingleTransaction) { // We pass the info0_ to all requests. int rv0 = resolver_->Resolve(info0_, &addrlist0_, &callback0_, NULL, BoundNetLog()); int rv1 = resolver_->Resolve(info0_, &addrlist1_, &callback1_, NULL, BoundNetLog()); int rv2 = resolver_->Resolve(info0_, &addrlist2_, &callback2_, NULL, BoundNetLog()); EXPECT_EQ(ERR_IO_PENDING, rv0); EXPECT_EQ(ERR_IO_PENDING, rv1); EXPECT_EQ(ERR_IO_PENDING, rv2); rv0 = callback0_.WaitForResult(); EXPECT_EQ(OK, rv0); VerifyAddressList(ip_addresses0_, kPortNum, addrlist0_); rv1 = callback1_.WaitForResult(); EXPECT_EQ(OK, rv1); VerifyAddressList(ip_addresses0_, kPortNum, addrlist1_); rv2 = callback2_.WaitForResult(); EXPECT_EQ(OK, rv2); VerifyAddressList(ip_addresses0_, kPortNum, addrlist2_); // Although we have three lookups, a single UDP socket was used. EXPECT_EQ(1u, factory_.udp_client_sockets().size()); } TEST_F(AsyncHostResolverTest, CancelLookup) { HostResolver::RequestHandle req0 = NULL, req2 = NULL; int rv0 = resolver_->Resolve(info0_, &addrlist0_, &callback0_, &req0, BoundNetLog()); int rv1 = resolver_->Resolve(info1_, &addrlist1_, &callback1_, NULL, BoundNetLog()); int rv2 = resolver_->Resolve(info2_, &addrlist2_, &callback2_, &req2, BoundNetLog()); EXPECT_EQ(ERR_IO_PENDING, rv0); EXPECT_EQ(ERR_IO_PENDING, rv1); EXPECT_EQ(ERR_IO_PENDING, rv2); resolver_->CancelRequest(req0); resolver_->CancelRequest(req2); MessageLoop::current()->RunAllPending(); EXPECT_FALSE(callback0_.have_result()); EXPECT_FALSE(callback2_.have_result()); rv1 = callback1_.WaitForResult(); EXPECT_EQ(OK, rv1); VerifyAddressList(ip_addresses1_, kPortNum, addrlist1_); } // Tests the following scenario: start two resolutions for the same host, // cancel one of them, make sure that the other one completes. TEST_F(AsyncHostResolverTest, CancelSameHostLookup) { HostResolver::RequestHandle req0 = NULL; // Pass the info0_ to both requests. int rv0 = resolver_->Resolve(info0_, &addrlist0_, &callback0_, &req0, BoundNetLog()); int rv1 = resolver_->Resolve(info0_, &addrlist1_, &callback1_, NULL, BoundNetLog()); EXPECT_EQ(ERR_IO_PENDING, rv0); EXPECT_EQ(ERR_IO_PENDING, rv1); resolver_->CancelRequest(req0); MessageLoop::current()->RunAllPending(); EXPECT_FALSE(callback0_.have_result()); rv1 = callback1_.WaitForResult(); EXPECT_EQ(OK, rv1); VerifyAddressList(ip_addresses0_, kPortNum, addrlist1_); EXPECT_EQ(1u, factory_.udp_client_sockets().size()); } // Test that a queued lookup completes. TEST_F(AsyncHostResolverTest, QueuedLookup) { // kMaxTransactions is 2, thus the following requests consume all // available transactions. int rv0 = resolver_->Resolve(info0_, &addrlist0_, &callback0_, NULL, BoundNetLog()); int rv1 = resolver_->Resolve(info1_, &addrlist1_, &callback1_, NULL, BoundNetLog()); EXPECT_EQ(ERR_IO_PENDING, rv0); EXPECT_EQ(ERR_IO_PENDING, rv1); // The following request will end up in queue. int rv2 = resolver_->Resolve(info2_, &addrlist2_, &callback2_, NULL, BoundNetLog()); EXPECT_EQ(ERR_IO_PENDING, rv2); EXPECT_EQ(1u, static_cast(resolver_.get())->GetNumPending()); // Make sure all requests complete. rv0 = callback0_.WaitForResult(); EXPECT_EQ(OK, rv0); VerifyAddressList(ip_addresses0_, kPortNum, addrlist0_); rv1 = callback1_.WaitForResult(); EXPECT_EQ(OK, rv1); VerifyAddressList(ip_addresses1_, kPortNum, addrlist1_); rv2 = callback2_.WaitForResult(); EXPECT_EQ(OK, rv2); VerifyAddressList(ip_addresses2_, kPortNum, addrlist2_); } // Test that cancelling a queued lookup works. TEST_F(AsyncHostResolverTest, CancelPendingLookup) { // kMaxTransactions is 2, thus the following requests consume all // available transactions. int rv0 = resolver_->Resolve(info0_, &addrlist0_, &callback0_, NULL, BoundNetLog()); int rv1 = resolver_->Resolve(info1_, &addrlist1_, &callback1_, NULL, BoundNetLog()); EXPECT_EQ(ERR_IO_PENDING, rv0); EXPECT_EQ(ERR_IO_PENDING, rv1); // The following request will end up in queue. HostResolver::RequestHandle req2 = NULL; int rv2 = resolver_->Resolve(info2_, &addrlist2_, &callback2_, &req2, BoundNetLog()); EXPECT_EQ(ERR_IO_PENDING, rv2); EXPECT_EQ(1u, static_cast(resolver_.get())->GetNumPending()); resolver_->CancelRequest(req2); // Make sure first two requests complete while the cancelled one doesn't. MessageLoop::current()->RunAllPending(); EXPECT_FALSE(callback2_.have_result()); rv0 = callback0_.WaitForResult(); EXPECT_EQ(OK, rv0); VerifyAddressList(ip_addresses0_, kPortNum, addrlist0_); rv1 = callback1_.WaitForResult(); EXPECT_EQ(OK, rv1); VerifyAddressList(ip_addresses1_, kPortNum, addrlist1_); } TEST_F(AsyncHostResolverTest, ResolverDestructionCancelsLookups) { int rv0 = resolver_->Resolve(info0_, &addrlist0_, &callback0_, NULL, BoundNetLog()); int rv1 = resolver_->Resolve(info1_, &addrlist1_, &callback1_, NULL, BoundNetLog()); // This one is queued. int rv2 = resolver_->Resolve(info2_, &addrlist2_, &callback2_, NULL, BoundNetLog()); EXPECT_EQ(1u, static_cast(resolver_.get())->GetNumPending()); EXPECT_EQ(ERR_IO_PENDING, rv0); EXPECT_EQ(ERR_IO_PENDING, rv1); EXPECT_EQ(ERR_IO_PENDING, rv2); resolver_.reset(); MessageLoop::current()->RunAllPending(); EXPECT_FALSE(callback0_.have_result()); EXPECT_FALSE(callback1_.have_result()); EXPECT_FALSE(callback2_.have_result()); } // Test that when the number of pending lookups is at max, a new lookup // with a priority lower than all of those in the queue fails. TEST_F(AsyncHostResolverTest, OverflowQueueWithLowPriorityLookup) { int rv0 = resolver_->Resolve(info0_, &addrlist0_, &callback0_, NULL, BoundNetLog()); int rv1 = resolver_->Resolve(info1_, &addrlist1_, &callback1_, NULL, BoundNetLog()); // This one is queued and fills up the queue since its size is 1. int rv2 = resolver_->Resolve(info2_, &addrlist2_, &callback2_, NULL, BoundNetLog()); EXPECT_EQ(1u, static_cast(resolver_.get())->GetNumPending()); EXPECT_EQ(ERR_IO_PENDING, rv0); EXPECT_EQ(ERR_IO_PENDING, rv1); EXPECT_EQ(ERR_IO_PENDING, rv2); // This one fails. info3_.set_priority(LOWEST); int rv3 = resolver_->Resolve(info3_, &addrlist3_, &callback3_, NULL, BoundNetLog()); EXPECT_EQ(ERR_HOST_RESOLVER_QUEUE_TOO_LARGE, rv3); MessageLoop::current()->RunAllPending(); EXPECT_FALSE(callback3_.have_result()); } // Test that when the number of pending lookups is at max, a new lookup // with a priority higher than any of those in the queue succeeds and // causes the lowest priority lookup in the queue to fail. TEST_F(AsyncHostResolverTest, OverflowQueueWithHighPriorityLookup) { int rv0 = resolver_->Resolve(info0_, &addrlist0_, &callback0_, NULL, BoundNetLog()); int rv1 = resolver_->Resolve(info1_, &addrlist1_, &callback1_, NULL, BoundNetLog()); // Next lookup is queued. Since this will be ejected from the queue and // will not consume a socket from our factory, we are not passing it // predefined members. HostResolver::RequestInfo info(HostPortPair("cnn.com", 80)); info.set_address_family(ADDRESS_FAMILY_IPV4); AddressList addrlist_fail; TestCompletionCallback callback_fail; int rv_fail = resolver_->Resolve(info, &addrlist_fail, &callback_fail, NULL, BoundNetLog()); EXPECT_EQ(1u, static_cast(resolver_.get())->GetNumPending()); EXPECT_EQ(ERR_IO_PENDING, rv0); EXPECT_EQ(ERR_IO_PENDING, rv1); EXPECT_EQ(ERR_IO_PENDING, rv_fail); // Lookup 2 causes the above to fail, but itself should succeed. info2_.set_priority(HIGHEST); int rv2 = resolver_->Resolve(info2_, &addrlist2_, &callback2_, NULL, BoundNetLog()); rv0 = callback0_.WaitForResult(); EXPECT_EQ(OK, rv0); VerifyAddressList(ip_addresses0_, kPortNum, addrlist0_); rv1 = callback1_.WaitForResult(); EXPECT_EQ(OK, rv1); VerifyAddressList(ip_addresses1_, kPortNum, addrlist1_); rv_fail = callback_fail.WaitForResult(); EXPECT_EQ(ERR_HOST_RESOLVER_QUEUE_TOO_LARGE, rv_fail); EXPECT_EQ(static_cast(NULL), addrlist_fail.head()); rv2 = callback2_.WaitForResult(); EXPECT_EQ(OK, rv2); VerifyAddressList(ip_addresses2_, kPortNum, addrlist2_); } // Test that registering, unregistering, and notifying of observers of // resolution start, completion and cancellation (both due to CancelRequest // and resolver destruction) work. TEST_F(AsyncHostResolverTest, Observers) { TestHostResolverObserver observer; resolver_->AddObserver(&observer); int rv0 = resolver_->Resolve(info0_, &addrlist0_, &callback0_, NULL, BoundNetLog()); int rv1 = resolver_->Resolve(info1_, &addrlist1_, &callback1_, NULL, BoundNetLog()); // We will cancel this one. HostResolver::RequestHandle req2 = NULL; int rv2 = resolver_->Resolve(info2_, &addrlist2_, &callback2_, &req2, BoundNetLog()); EXPECT_EQ(ERR_IO_PENDING, rv0); EXPECT_EQ(ERR_IO_PENDING, rv1); EXPECT_EQ(ERR_IO_PENDING, rv2); // Cancel lookup 2. resolver_->CancelRequest(req2); // Lookup 0 and 1 should succeed. rv0 = callback0_.WaitForResult(); EXPECT_EQ(OK, rv0); VerifyAddressList(ip_addresses0_, kPortNum, addrlist0_); rv1 = callback1_.WaitForResult(); EXPECT_EQ(OK, rv1); VerifyAddressList(ip_addresses1_, kPortNum, addrlist1_); // Next lookup should not have finished. MessageLoop::current()->RunAllPending(); EXPECT_FALSE(callback2_.have_result()); // Verify observer calls. EXPECT_EQ(3u, observer.start_log.size()); EXPECT_EQ(2u, observer.finish_log.size()); EXPECT_EQ(1u, observer.cancel_log.size()); // Lookup 0 started and finished. EXPECT_TRUE(observer.start_log[0] == TestHostResolverObserver::StartOrCancelEntry(0, info0_)); EXPECT_TRUE(observer.finish_log[0] == TestHostResolverObserver::FinishEntry(0, true, info0_)); // Ditto for lookup 1. EXPECT_TRUE(observer.start_log[1] == TestHostResolverObserver::StartOrCancelEntry(1, info1_)); EXPECT_TRUE(observer.finish_log[1] == TestHostResolverObserver::FinishEntry(1, true, info1_)); // Lookup 2 was cancelled, hence, failed to finish. EXPECT_TRUE(observer.start_log[2] == TestHostResolverObserver::StartOrCancelEntry(2, info2_)); EXPECT_TRUE(observer.cancel_log[0] == TestHostResolverObserver::StartOrCancelEntry(2, info2_)); // Unregister observer. resolver_->RemoveObserver(&observer); // We will do lookup 2 again but will not cancel it this time. rv2 = resolver_->Resolve(info2_, &addrlist2_, &callback2_, NULL, BoundNetLog()); EXPECT_EQ(ERR_IO_PENDING, rv2); // Run lookup 2 to completion. rv2 = callback2_.WaitForResult(); EXPECT_EQ(OK, rv2); VerifyAddressList(ip_addresses2_, kPortNum, addrlist2_); // Observer log should stay the same. EXPECT_EQ(3u, observer.start_log.size()); EXPECT_EQ(2u, observer.finish_log.size()); EXPECT_EQ(1u, observer.cancel_log.size()); // Re-register observer. resolver_->AddObserver(&observer); // Start lookup 3. int rv3 = resolver_->Resolve(info3_, &addrlist3_, &callback3_, NULL, BoundNetLog()); EXPECT_EQ(ERR_IO_PENDING, rv3); // Destroy the resolver and make sure that observer was notified of just // the resolution start. resolver_.reset(); EXPECT_EQ(4u, observer.start_log.size()); // Was incremented by 1. EXPECT_EQ(2u, observer.finish_log.size()); EXPECT_EQ(2u, observer.cancel_log.size()); EXPECT_TRUE(observer.start_log[3] == TestHostResolverObserver::StartOrCancelEntry(4, info3_)); } } // namespace net