diff --git a/contrib/seeds/generate-seeds.py b/contrib/seeds/generate-seeds.py index 75cb6fad6e..c35827ed5d 100755 --- a/contrib/seeds/generate-seeds.py +++ b/contrib/seeds/generate-seeds.py @@ -54,7 +54,7 @@ def name_to_bip155(addr): raise ValueError('Invalid onion %s' % vchAddr) elif '.' in addr: # IPv4 return (BIP155Network.IPV4, bytes((int(x) for x in addr.split('.')))) - elif ':' in addr: # IPv6 + elif ':' in addr: # IPv6 or CJDNS sub = [[], []] # prefix, suffix x = 0 addr = addr.split(':') @@ -70,7 +70,14 @@ def name_to_bip155(addr): sub[x].append(val & 0xff) nullbytes = 16 - len(sub[0]) - len(sub[1]) assert((x == 0 and nullbytes == 0) or (x == 1 and nullbytes > 0)) - return (BIP155Network.IPV6, bytes(sub[0] + ([0] * nullbytes) + sub[1])) + addr_bytes = bytes(sub[0] + ([0] * nullbytes) + sub[1]) + if addr_bytes[0] == 0xfc: + # Assume that seeds with fc00::/8 addresses belong to CJDNS, + # not to the publicly unroutable "Unique Local Unicast" network, see + # RFC4193: https://datatracker.ietf.org/doc/html/rfc4193#section-8 + return (BIP155Network.CJDNS, addr_bytes) + else: + return (BIP155Network.IPV6, addr_bytes) else: raise ValueError('Could not parse address %s' % addr) diff --git a/src/init.cpp b/src/init.cpp index d600e54a85..e0a12d117f 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -566,6 +566,7 @@ void SetupServerArgs(NodeContext& node) argsman.AddArg("-allowprivatenet", strprintf("Allow RFC1918 addresses to be relayed and connected to (default: %u)", DEFAULT_ALLOWPRIVATENET), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-bantime=", strprintf("Default duration (in seconds) of manually configured bans (default: %u)", DEFAULT_MISBEHAVING_BANTIME), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-bind=[:][=onion]", strprintf("Bind to given address and always listen on it (default: 0.0.0.0). Use [host]:port notation for IPv6. Append =onion to tag any incoming connections to that address and port as incoming Tor connections (default: 127.0.0.1:%u=onion, testnet: 127.0.0.1:%u=onion, regtest: 127.0.0.1:%u=onion)", defaultBaseParams->OnionServiceTargetPort(), testnetBaseParams->OnionServiceTargetPort(), regtestBaseParams->OnionServiceTargetPort()), ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION); + argsman.AddArg("-cjdnsreachable", "If set then this host is configured for CJDNS (connecting to fc00::/8 addresses would lead us to the CJDNS network) (default: 0)", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-connect=", "Connect only to the specified node; -noconnect disables automatic connections (the rules for this peer are the same as for -addnode). This option can be specified multiple times to connect to multiple nodes.", ArgsManager::ALLOW_ANY | ArgsManager::NETWORK_ONLY, OptionsCategory::CONNECTION); argsman.AddArg("-discover", "Discover own IP addresses (default: 1 when listening and no -externalip or -proxy)", ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); argsman.AddArg("-dns", strprintf("Allow DNS lookups for -addnode, -seednode and -connect (default: %u)", DEFAULT_NAME_LOOKUP), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION); @@ -1785,6 +1786,14 @@ bool AppInitMain(const CoreContext& context, NodeContext& node, interfaces::Bloc } } + if (!args.IsArgSet("-cjdnsreachable")) { + SetReachable(NET_CJDNS, false); + } + // Now IsReachable(NET_CJDNS) is true if: + // 1. -cjdnsreachable is given and + // 2.1. -onlynet is not given or + // 2.2. -onlynet=cjdns is given + // Check for host lookup allowed before parsing any network related parameters fNameLookup = args.GetBoolArg("-dns", DEFAULT_NAME_LOOKUP); @@ -1806,6 +1815,7 @@ bool AppInitMain(const CoreContext& context, NodeContext& node, interfaces::Bloc SetProxy(NET_IPV4, addrProxy); SetProxy(NET_IPV6, addrProxy); SetProxy(NET_ONION, addrProxy); + SetProxy(NET_CJDNS, addrProxy); SetNameProxy(addrProxy); SetReachable(NET_ONION, true); // by default, -proxy sets onion as reachable, unless -noonion later } diff --git a/src/net.cpp b/src/net.cpp index 2e5c297cac..baeef8781c 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -255,9 +255,27 @@ std::optional GetLocalAddrForPeer(CNode *pnode) return std::nullopt; } -// learn a new local address -bool AddLocal(const CService& addr, int nScore) +/** + * If an IPv6 address belongs to the address range used by the CJDNS network and + * the CJDNS network is reachable (-cjdnsreachable config is set), then change + * the type from NET_IPV6 to NET_CJDNS. + * @param[in] service Address to potentially convert. + * @return a copy of `service` either unmodified or changed to CJDNS. + */ +CService MaybeFlipIPv6toCJDNS(const CService& service) { + CService ret{service}; + if (ret.m_net == NET_IPV6 && ret.m_addr[0] == 0xfc && IsReachable(NET_CJDNS)) { + ret.m_net = NET_CJDNS; + } + return ret; +} + +// learn a new local address +bool AddLocal(const CService& addr_, int nScore) +{ + CService addr{MaybeFlipIPv6toCJDNS(addr_)}; + if (!addr.IsRoutable() && Params().RequireRoutableExternalIP()) return false; @@ -454,7 +472,8 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo if (pszDest) { std::vector resolved; if (Lookup(pszDest, resolved, default_port, fNameLookup && !HaveNameProxy(), 256) && !resolved.empty()) { - addrConnect = CAddress(resolved[GetRand(resolved.size())], NODE_NONE); + const CService rnd{resolved[GetRand(resolved.size())]}; + addrConnect = CAddress{MaybeFlipIPv6toCJDNS(rnd), NODE_NONE}; if (!addrConnect.IsValid()) { LogPrint(BCLog::NET, "Resolver returned invalid address %s for %s\n", addrConnect.ToString(), pszDest); return nullptr; @@ -1210,9 +1229,11 @@ void CConnman::AcceptConnection(const ListenSocket& hListenSocket, CMasternodeSy if (!addr.SetSockAddr((const struct sockaddr*)&sockaddr)) { LogPrintf("Warning: Unknown socket family\n"); + } else { + addr = CAddress{MaybeFlipIPv6toCJDNS(addr), NODE_NONE}; } - const CAddress addr_bind = GetBindAddress(hSocket); + const CAddress addr_bind{MaybeFlipIPv6toCJDNS(GetBindAddress(hSocket)), NODE_NONE}; NetPermissionFlags permissionFlags = NetPermissionFlags::None; hListenSocket.AddSocketPermissionFlags(permissionFlags); @@ -3303,7 +3324,10 @@ NodeId CConnman::GetNewNodeId() } -bool CConnman::Bind(const CService &addr, unsigned int flags, NetPermissionFlags permissions) { +bool CConnman::Bind(const CService& addr_, unsigned int flags, NetPermissionFlags permissions) +{ + const CService addr{MaybeFlipIPv6toCJDNS(addr_)}; + if (!(flags & BF_EXPLICIT) && !IsReachable(addr)) { return false; } diff --git a/src/netaddress.cpp b/src/netaddress.cpp index f09fefba7d..aaa036f9e2 100644 --- a/src/netaddress.cpp +++ b/src/netaddress.cpp @@ -676,7 +676,7 @@ bool CNetAddr::GetInAddr(struct in_addr* pipv4Addr) const */ bool CNetAddr::GetIn6Addr(struct in6_addr* pipv6Addr) const { - if (!IsIPv6()) { + if (!IsIPv6() && !IsCJDNS()) { return false; } assert(sizeof(*pipv6Addr) == m_addr.size()); @@ -796,8 +796,14 @@ std::vector CNetAddr::GetGroup(const std::vector &asmap) co vchRet.push_back((ipv4 >> 24) & 0xFF); vchRet.push_back((ipv4 >> 16) & 0xFF); return vchRet; - } else if (IsTor() || IsI2P() || IsCJDNS()) { + } else if (IsTor() || IsI2P()) { nBits = 4; + } else if (IsCJDNS()) { + // Treat in the same way as Tor and I2P because the address in all of + // them is "random" bytes (derived from a public key). However in CJDNS + // the first byte is a constant 0xfc, so the random bytes come after it. + // Thus skip the constant 8 bits at the start. + nBits = 12; } else if (IsHeNet()) { // for he.net, use /36 groups nBits = 36; @@ -894,6 +900,11 @@ int CNetAddr::GetReachabilityFrom(const CNetAddr *paddrPartner) const case NET_I2P: return REACH_PRIVATE; default: return REACH_DEFAULT; } + case NET_CJDNS: + switch (ourNet) { + case NET_CJDNS: return REACH_PRIVATE; + default: return REACH_DEFAULT; + } case NET_TEREDO: switch(ourNet) { default: return REACH_DEFAULT; @@ -995,7 +1006,7 @@ bool CService::GetSockAddr(struct sockaddr* paddr, socklen_t *addrlen) const paddrin->sin_port = htons(port); return true; } - if (IsIPv6()) { + if (IsIPv6() || IsCJDNS()) { if (*addrlen < (socklen_t)sizeof(struct sockaddr_in6)) return false; *addrlen = sizeof(struct sockaddr_in6); diff --git a/src/netaddress.h b/src/netaddress.h index 7ca6e57304..00cf1e2147 100644 --- a/src/netaddress.h +++ b/src/netaddress.h @@ -232,7 +232,7 @@ public: */ bool IsRelayable() const { - return IsIPv4() || IsIPv6() || IsTor() || IsI2P(); + return IsIPv4() || IsIPv6() || IsTor() || IsI2P() || IsCJDNS(); } /** @@ -581,6 +581,8 @@ public: READWRITEAS(CNetAddr, obj); READWRITE(Using>(obj.port)); } + + friend CService MaybeFlipIPv6toCJDNS(const CService& service); }; bool SanityCheckASMap(const std::vector& asmap); diff --git a/src/netbase.cpp b/src/netbase.cpp index 473c8a5a75..74ab7b6318 100644 --- a/src/netbase.cpp +++ b/src/netbase.cpp @@ -95,6 +95,9 @@ enum Network ParseNetwork(const std::string& net_in) { if (net == "i2p") { return NET_I2P; } + if (net == "cjdns") { + return NET_CJDNS; + } return NET_UNROUTABLE; } @@ -119,7 +122,7 @@ std::vector GetNetworkNames(bool append_unroutable) std::vector names; for (int n = 0; n < NET_MAX; ++n) { const enum Network network{static_cast(n)}; - if (network == NET_UNROUTABLE || network == NET_CJDNS || network == NET_INTERNAL) continue; + if (network == NET_UNROUTABLE || network == NET_INTERNAL) continue; names.emplace_back(GetNetworkName(network)); } if (append_unroutable) { diff --git a/src/rpc/net.cpp b/src/rpc/net.cpp index e9f6d46f48..a1a1d40035 100644 --- a/src/rpc/net.cpp +++ b/src/rpc/net.cpp @@ -585,7 +585,7 @@ static UniValue GetNetworksInfo() UniValue networks(UniValue::VARR); for (int n = 0; n < NET_MAX; ++n) { enum Network network = static_cast(n); - if (network == NET_UNROUTABLE || network == NET_CJDNS || network == NET_INTERNAL) continue; + if (network == NET_UNROUTABLE || network == NET_INTERNAL) continue; proxyType proxy; UniValue obj(UniValue::VOBJ); GetProxy(network, proxy); diff --git a/src/test/netbase_tests.cpp b/src/test/netbase_tests.cpp index cae1e3c067..98aae4744d 100644 --- a/src/test/netbase_tests.cpp +++ b/src/test/netbase_tests.cpp @@ -441,11 +441,13 @@ BOOST_AUTO_TEST_CASE(netbase_parsenetwork) BOOST_CHECK_EQUAL(ParseNetwork("ipv6"), NET_IPV6); BOOST_CHECK_EQUAL(ParseNetwork("onion"), NET_ONION); BOOST_CHECK_EQUAL(ParseNetwork("tor"), NET_ONION); + BOOST_CHECK_EQUAL(ParseNetwork("cjdns"), NET_CJDNS); BOOST_CHECK_EQUAL(ParseNetwork("IPv4"), NET_IPV4); BOOST_CHECK_EQUAL(ParseNetwork("IPv6"), NET_IPV6); BOOST_CHECK_EQUAL(ParseNetwork("ONION"), NET_ONION); BOOST_CHECK_EQUAL(ParseNetwork("TOR"), NET_ONION); + BOOST_CHECK_EQUAL(ParseNetwork("CJDNS"), NET_CJDNS); BOOST_CHECK_EQUAL(ParseNetwork(":)"), NET_UNROUTABLE); BOOST_CHECK_EQUAL(ParseNetwork("tÖr"), NET_UNROUTABLE); diff --git a/test/functional/feature_proxy.py b/test/functional/feature_proxy.py index a04a836c0e..5276b8fd36 100755 --- a/test/functional/feature_proxy.py +++ b/test/functional/feature_proxy.py @@ -12,6 +12,7 @@ Test plan: - `-proxy` (proxy everything) - `-onion` (proxy just onions) - `-proxyrandomize` Circuit randomization + - `-cjdnsreachable` - Proxy configurations to test on proxy side, - support no authentication (other proxy) - support no authentication + user/pass authentication (Tor) @@ -26,6 +27,7 @@ addnode connect to IPv4 addnode connect to IPv6 addnode connect to onion addnode connect to generic DNS name +addnode connect to a CJDNS address - Test getnetworkinfo for each node """ @@ -50,14 +52,15 @@ NET_IPV4 = "ipv4" NET_IPV6 = "ipv6" NET_ONION = "onion" NET_I2P = "i2p" +NET_CJDNS = "cjdns" # Networks returned by RPC getnetworkinfo, defined in src/rpc/net.cpp::GetNetworksInfo() -NETWORKS = frozenset({NET_IPV4, NET_IPV6, NET_ONION, NET_I2P}) +NETWORKS = frozenset({NET_IPV4, NET_IPV6, NET_ONION, NET_I2P, NET_CJDNS}) class ProxyTest(BitcoinTestFramework): def set_test_params(self): - self.num_nodes = 4 + self.num_nodes = 5 self.setup_clean_chain = True def setup_nodes(self): @@ -101,7 +104,9 @@ class ProxyTest(BitcoinTestFramework): ['-listen', f'-proxy={self.conf1.addr[0]}:{self.conf1.addr[1]}',f'-onion={self.conf2.addr[0]}:{self.conf2.addr[1]}', f'-i2psam={self.i2p_sam[0]}:{self.i2p_sam[1]}', '-i2pacceptincoming=0', '-proxyrandomize=0'], ['-listen', f'-proxy={self.conf2.addr[0]}:{self.conf2.addr[1]}','-proxyrandomize=1'], - [] + [], + ['-listen', f'-proxy={self.conf1.addr[0]}:{self.conf1.addr[1]}','-proxyrandomize=1', + '-cjdnsreachable'] ] if self.have_ipv6: args[3] = ['-listen', f'-proxy=[{self.conf3.addr[0]}]:{self.conf3.addr[1]}','-proxyrandomize=0', '-noonion'] @@ -113,7 +118,7 @@ class ProxyTest(BitcoinTestFramework): if peer["addr"] == addr: assert_equal(peer["network"], network) - def node_test(self, node, proxies, auth, test_onion=True): + def node_test(self, node, *, proxies, auth, test_onion, test_cjdns): rv = [] addr = "15.61.23.23:1234" self.log.debug(f"Test: outgoing IPv4 connection through node for address {addr}") @@ -161,6 +166,21 @@ class ProxyTest(BitcoinTestFramework): rv.append(cmd) self.network_test(node, addr, network=NET_ONION) + if test_cjdns: + addr = "[fc00:1:2:3:4:5:6:7]:8888" + self.log.debug(f"Test: outgoing CJDNS connection through node for address {addr}") + node.addnode(addr, "onetry") + cmd = proxies[1].queue.get() + assert isinstance(cmd, Socks5Command) + assert_equal(cmd.atyp, AddressType.DOMAINNAME) + assert_equal(cmd.addr, b"fc00:1:2:3:4:5:6:7") + assert_equal(cmd.port, 8888) + if not auth: + assert_equal(cmd.username, None) + assert_equal(cmd.password, None) + rv.append(cmd) + self.network_test(node, addr, network=NET_CJDNS) + addr = "node.noumenon:8333" self.log.debug(f"Test: outgoing DNS name connection through node for address {addr}") node.addnode(addr, "onetry") @@ -179,20 +199,33 @@ class ProxyTest(BitcoinTestFramework): def run_test(self): # basic -proxy - self.node_test(self.nodes[0], [self.serv1, self.serv1, self.serv1, self.serv1], False) + self.node_test(self.nodes[0], + proxies=[self.serv1, self.serv1, self.serv1, self.serv1], + auth=False, test_onion=True, test_cjdns=False) # -proxy plus -onion - self.node_test(self.nodes[1], [self.serv1, self.serv1, self.serv2, self.serv1], False) + self.node_test(self.nodes[1], + proxies=[self.serv1, self.serv1, self.serv2, self.serv1], + auth=False, test_onion=True, test_cjdns=False) # -proxy plus -onion, -proxyrandomize - rv = self.node_test(self.nodes[2], [self.serv2, self.serv2, self.serv2, self.serv2], True) + rv = self.node_test(self.nodes[2], + proxies=[self.serv2, self.serv2, self.serv2, self.serv2], + auth=True, test_onion=True, test_cjdns=False) # Check that credentials as used for -proxyrandomize connections are unique credentials = set((x.username,x.password) for x in rv) assert_equal(len(credentials), len(rv)) if self.have_ipv6: # proxy on IPv6 localhost - self.node_test(self.nodes[3], [self.serv3, self.serv3, self.serv3, self.serv3], False, False) + self.node_test(self.nodes[3], + proxies=[self.serv3, self.serv3, self.serv3, self.serv3], + auth=False, test_onion=False, test_cjdns=False) + + # -proxy=unauth -proxyrandomize=1 -cjdnsreachable + self.node_test(self.nodes[4], + proxies=[self.serv1, self.serv1, self.serv1, self.serv1], + auth=False, test_onion=True, test_cjdns=True) def networks_dict(d): r = {} @@ -214,6 +247,7 @@ class ProxyTest(BitcoinTestFramework): assert_equal(n0[net]['proxy_randomize_credentials'], expected_randomize) assert_equal(n0['onion']['reachable'], True) assert_equal(n0['i2p']['reachable'], False) + assert_equal(n0['cjdns']['reachable'], False) n1 = networks_dict(self.nodes[1].getnetworkinfo()) assert_equal(NETWORKS, n1.keys()) @@ -240,6 +274,7 @@ class ProxyTest(BitcoinTestFramework): assert_equal(n2[net]['proxy_randomize_credentials'], expected_randomize) assert_equal(n2['onion']['reachable'], True) assert_equal(n2['i2p']['reachable'], False) + assert_equal(n2['cjdns']['reachable'], False) if self.have_ipv6: n3 = networks_dict(self.nodes[3].getnetworkinfo()) @@ -253,6 +288,22 @@ class ProxyTest(BitcoinTestFramework): assert_equal(n3[net]['proxy_randomize_credentials'], False) assert_equal(n3['onion']['reachable'], False) assert_equal(n3['i2p']['reachable'], False) + assert_equal(n3['cjdns']['reachable'], False) + + n4 = networks_dict(self.nodes[4].getnetworkinfo()) + assert_equal(NETWORKS, n4.keys()) + for net in NETWORKS: + if net == NET_I2P: + expected_proxy = '' + expected_randomize = False + else: + expected_proxy = '%s:%i' % (self.conf1.addr) + expected_randomize = True + assert_equal(n4[net]['proxy'], expected_proxy) + assert_equal(n4[net]['proxy_randomize_credentials'], expected_randomize) + assert_equal(n4['onion']['reachable'], True) + assert_equal(n4['i2p']['reachable'], False) + assert_equal(n4['cjdns']['reachable'], True) if __name__ == '__main__': diff --git a/test/functional/interface_bitcoin_cli.py b/test/functional/interface_bitcoin_cli.py index 239dae2d3e..d7c9ff6dc3 100755 --- a/test/functional/interface_bitcoin_cli.py +++ b/test/functional/interface_bitcoin_cli.py @@ -141,7 +141,7 @@ class TestBitcoinCli(BitcoinTestFramework): network_info = self.nodes[0].getnetworkinfo() cli_get_info_string = self.nodes[0].cli('-getinfo').send_cli() cli_get_info = cli_get_info_string_to_dict(cli_get_info_string) - assert_equal(cli_get_info["Proxies"], "127.0.0.1:9050 (ipv4, ipv6, onion), 127.0.0.1:7656 (i2p)") + assert_equal(cli_get_info["Proxies"], "127.0.0.1:9050 (ipv4, ipv6, onion, cjdns), 127.0.0.1:7656 (i2p)") if self.is_wallet_compiled(): self.log.info("Test -getinfo and dash-cli getwalletinfo return expected wallet info") diff --git a/test/functional/rpc_net.py b/test/functional/rpc_net.py index ca77cc429d..8c8a95f9c2 100755 --- a/test/functional/rpc_net.py +++ b/test/functional/rpc_net.py @@ -99,7 +99,7 @@ class NetTest(DashTestFramework): assert_net_servicesnames(int(info[0]["services"], 0x10), info[0]["servicesnames"]) # Check dynamically generated networks list in getpeerinfo help output. - assert "(ipv4, ipv6, onion, i2p, not_publicly_routable)" in self.nodes[0].help("getpeerinfo") + assert "(ipv4, ipv6, onion, i2p, cjdns, not_publicly_routable)" in self.nodes[0].help("getpeerinfo") # This part is slightly different comparing to the Bitcoin implementation. This is expected because we create connections on network setup a bit differently too. # We also create more connection during the test itself to test mn specific stats assert_equal(peer_info[0][0]['connection_type'], 'inbound') @@ -167,7 +167,7 @@ class NetTest(DashTestFramework): assert_net_servicesnames(int(info["localservices"], 0x10), info["localservicesnames"]) # Check dynamically generated networks list in getnetworkinfo help output. - assert "(ipv4, ipv6, onion, i2p)" in self.nodes[0].help("getnetworkinfo") + assert "(ipv4, ipv6, onion, i2p, cjdns)" in self.nodes[0].help("getnetworkinfo") self.log.info('Test extended connections info') # Connect nodes both ways. @@ -252,8 +252,8 @@ class NetTest(DashTestFramework): assert_equal(res[0]["port"], 8333) assert_equal(res[0]["services"], services) - # Test for the absence of onion and I2P addresses. - for network in ["onion", "i2p"]: + # Test for the absence of onion, I2P and CJDNS addresses. + for network in ["onion", "i2p", "cjdns"]: assert_equal(self.nodes[0].getnodeaddresses(0, network), []) # Test invalid arguments.