merge bitcoin#23077: Full CJDNS support

This commit is contained in:
Kittywhiskers Van Gogh 2024-05-27 09:24:31 +00:00
parent 7596a7320a
commit f9d1a9a00d
No known key found for this signature in database
GPG Key ID: 30CD0C065E5C4AAD
11 changed files with 136 additions and 26 deletions

View File

@ -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)

View File

@ -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=<n>", strprintf("Default duration (in seconds) of manually configured bans (default: %u)", DEFAULT_MISBEHAVING_BANTIME), ArgsManager::ALLOW_ANY, OptionsCategory::CONNECTION);
argsman.AddArg("-bind=<addr>[:<port>][=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=<ip>", "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
}

View File

@ -255,9 +255,27 @@ std::optional<CAddress> 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<CService> 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;
}

View File

@ -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<unsigned char> CNetAddr::GetGroup(const std::vector<bool> &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);

View File

@ -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<BigEndianFormatter<2>>(obj.port));
}
friend CService MaybeFlipIPv6toCJDNS(const CService& service);
};
bool SanityCheckASMap(const std::vector<bool>& asmap);

View File

@ -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<std::string> GetNetworkNames(bool append_unroutable)
std::vector<std::string> names;
for (int n = 0; n < NET_MAX; ++n) {
const enum Network network{static_cast<Network>(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) {

View File

@ -585,7 +585,7 @@ static UniValue GetNetworksInfo()
UniValue networks(UniValue::VARR);
for (int n = 0; n < NET_MAX; ++n) {
enum Network network = static_cast<enum Network>(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);

View File

@ -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);

View File

@ -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__':

View File

@ -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")

View File

@ -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.