8000 Handle CJDNS from LookupSubNet() by vasild · Pull Request #27071 · bitcoin/bitcoin · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Handle CJDNS from LookupSubNet() #27071

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Oct 19, 2023
Merged

Conversation

vasild
Copy link
Contributor
@vasild vasild commented Feb 10, 2023

LookupSubNet() would treat addresses that start with fc as IPv6 even if -cjdnsreachable is set. This creates the following problems where it is called:

  • NetWhitelistPermissions::TryParse(): otherwise -whitelist= fails to white list CJDNS addresses: when a CJDNS peer connects to us, it will be matched against IPv6 fc... subnet and the match will never succeed.

  • BanMapFromJson(): CJDNS bans are stored as just IPv6 addresses in banlist.json. Upon reading from disk they have to be converted back to CJDNS, otherwise, after restart, a ban entry like (fc00::1, IPv6) would not match a peer (fc00::1, CJDNS).

  • RPCConsole::unbanSelectedNode(): in the GUI the ban entries go through CSubNet::ToString() and back via LookupSubNet(). Then it must match whatever is stored in BanMan, otherwise it is impossible to unban via the GUI.

These were uncovered by #26859.

Thus, flip the result of LookupSubNet() to CJDNS if the network base address starts with fc and -cjdnsreachable is set. Since subnetting/masking does not make sense for CJDNS (the address is "random" bytes, like Tor and I2P, there is no hierarchy) treat fc.../mask as an invalid CSubNet.

To achieve that, MaybeFlipIPv6toCJDNS() has to be moved from net to netbase and thus also IsReachable(). In the process of moving IsReachable(), SetReachable() and vfLimited[] encapsulate those in a class.

@DrahtBot
Copy link
Contributor
DrahtBot commented Feb 10, 2023

The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

Code Coverage

For detailed information about the code coverage, see the test coverage report.

Reviews

See the guideline for information on the review process.

Type Reviewers
ACK mzumsande, jonatack, achow101
Concept ACK brunoerg

If your review is incorrectly listed, please react with 👎 to this comment and the bot will ignore it on the next update.

Conflicts

Reviewers, this pull request conflicts with the following ones:

  • #28248 (p2p: peer connection bug fixes, logic and logging improvements by jonatack)
  • #28170 (p2p: adaptive connections services flags by furszy)
  • #27837 (net: introduce block tracker to retry to download blocks after failure by furszy)
  • #27557 (net: call getaddrinfo() in detachable thread to prevent stalling by pinheadmz)
  • #27375 (net: support unix domain sockets for -proxy and -onion by pinheadmz)
  • #25390 (sync: introduce a thread-safe generic container and use it to remove a bunch of "GlobalMutex"es by vasild)

If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

Copy link
Contributor
@mzumsande mzumsande left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Concept ACK to fixing this - however, I'm unsure about how the approach fits into the larger architecture:

I think of netbase as a module for mostly context-free basic low-level methods - this PR moves IsReachable to it and saves its state in a global variable, accessing it both from different threads within the node and the GUI. Shouldn't the information flow between the Node and the GUI rather go through node/interfaces instead of having a shared global variable?

Did you consider the alternative approach of letting banman be agnostic of CJDNS (everything stays IPv6, CJDNS addresses get a /128 netmask just like single IPv6 addresses), and add special "flipping" logic to the points where we need to compare actual addresses from peers with the results from banman? I didn't try it yet so not sure if viable, but that was my first idea of how to fix the issue of not being able to ban CJDNS peers.

src/netbase.cpp Outdated
@@ -694,6 +694,7 @@ bool LookupSubNet(const std::string& subnet_str, CSubNet& subnet_out)
CNetAddr addr;

if (LookupHost(str_addr, addr, /*fAllowLookup=*/false)) {
addr = MaybeFlipIPv6toCJDNS(CService{addr, /*port=*/0});
Copy link
Contributor
@mzumsande mzumsande Feb 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that MaybeFlipIPv6toCJDNS is available in netbase, would it make sense (maybe in follow-ups) to also use it in the Lookup() functions, not just LookupSubNet) and reduce the number of occurrences we have to call MaybeFlipIPv6toCJDNS from outside it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, well. I feel like MaybeFlipIPv6toCJDNS() is warranted in some of the lower level Lookup*() functions. But they are called from so many places, the change would be very invasive. I have to gather some bravery in order to look into that. If that is to be done, maybe it should be preceded by some simplification of the Lookup*() family of functions. I get a headache every time I have to deal with:

bool LookupHost(const std::string& name, std::vector<CNetAddr>& vIP, unsigned int nMaxSolutions, bool fAllowLookup, DNSLookupFn dns_lookup_function = g_dns_lookup);
bool LookupHost(const std::string& name, CNetAddr& addr, bool fAllowLookup, DNSLookupFn dns_lookup_function = g_dns_lookup);
bool Lookup(const std::string& name, std::vector<CService>& vAddr, uint16_t portDefault, bool fAllowLookup, unsigned int nMaxSolutions, DNSLookupFn dns_lookup_function = g_dns_lookup);
bool Lookup(const std::string& name, CService& addr, uint16_t portDefault, bool fAllowLookup, DNSLookupFn dns_lookup_function = g_dns_lookup);
CService LookupNumeric(const std::string& name, uint16_t portDefault = 0, DNSLookupFn dns_lookup_function = g_dns_lookup);
bool LookupIntern(const std::string& name, std::vector<CNetAddr>& vIP, unsigned int nMaxSolutions, bool fAllowLookup, DNSLookupFn dns_lookup_function);

@vasild
Copy link
Contributor Author
vasild commented Feb 13, 2023

I think of netbase as a module for mostly context-free basic low-level methods - this PR moves IsReachable to it and saves its state in a global variable,

I was thinking about the same, but netbase is already stateful - proxyInfo, nameProxy, nConnectTimeout, fNameLookup, g_socks5_recv_timeout, interruptSocks5Recv are global variables that keep state, which influence the output of the functions defined in netbase, making them context-dependent.

SetProxy(), GetProxy() (already defined in netbase) are very similar to the SetReachable(), IsReachable() functions (which this PR moves into netbase). Thus I think the move is appropriate.

accessing it both from different threads within the node and the GUI. Shouldn't the information flow between the Node and the GUI rather go through node/interfaces instead of having a shared global variable?

I am not sure IsReachable() fits into that. It is not like that in master where there is the global variable vfLimited[].

Did you consider the alternative approach of letting banman be agnostic of CJDNS (everything stays IPv6, CJDNS addresses get a /128 netmask just like single IPv6 addresses), and add special "flipping" logic to the points where we need to compare actual addresses from peers with the results from banman? I didn't try it yet so not sure if viable, but that was my first idea of how to fix the issue of not being able to ban CJDNS peers.

banman is using CSubNet::Match(). This would mean in that method we should match IPv6 vs CJDNS. I did not like the idea because CSubNet is used also elsewhere and it could influence other parts of the code. But in general it feels wrong to "pretend" that an address from network X belongs to a subnet from network Y. I even coded that, but eventually decided to avoid it:

CSubNet::Match() mix IPv6 and CJDNS
--- a/src/netaddress.cpp
+++ b/src/netaddress.cpp
@@ -1039,24 +1039,40 @@ CSubNet::CSubNet(const CNetAddr& addr) : CSubNet()
 /**
  * @returns True if this subnet is valid, the specified address is valid, and
  *          the specified address belongs in this subnet.
  */
 bool CSubNet::Match(const CNetAddr &addr) const
 {
-    if (!valid || !addr.IsValid() || network.m_net != addr.m_net)
+    if (!valid || !addr.IsValid()) {
         return false;
+    }
 
-    switch (network.m_net) {
+    Network subnet_net = network.m_net;
+    Network addr_net = addr.m_net;
+
+    if ((subnet_net == NET_IPV6 && addr_net == NET_CJDNS) ||
+        (subnet_net == NET_CJDNS && addr_net == NET_IPV6)) {
+        // Treat both as CJDNS.
+        subnet_net = NET_CJDNS;
+        addr_net = NET_CJDNS;
+    }
+
+    if (subnet_net != addr_net) {
+        return false;
+    }
+
+    switch (subnet_net) {
     case NET_IPV4:
     case NET_IPV6:
         break;
     case NET_ONION:
     case NET_I2P:
-    case NET_CJDNS:
     case NET_INTERNAL:
         return addr == network;
+    case NET_CJDNS:
+        return addr.m_addr == network.m_addr;
     case NET_UNROUTABLE:
     case NET_MAX:
         return false;
     }
 
     assert(network.m_addr.size() == addr.m_addr.size());

Note also that the current PR also resolves two other issues, in addition to the banman one - listed as items in the OP.

8000

@mzumsande
Copy link
Contributor

I am not sure IsReachable() fits into that. It is not like that in master where there is the global variable vfLimited[].

Yes, within Node it's still one of several global variables, so this is not a change wrt master.
However, vfLimited wasn't accessed by the GUI because it wasn't part of LookupSubnet() before, while g_reachable_nets is. So I'm curious if this would work well together with multiprocess (see #10102, fyi @ryanofsky ).
I think most of the data in vfLimited is determined during init and does not change later (so it might be initialised at the fork point?) - but torcontrol calling SetReachable() later could be an exception?

@vasild vasild force-pushed the lookup_subnet_cjdns branch from 9b656a5 to 7a93d7b Compare February 14, 2023 14:19
@vasild
Copy link
Contributor Author
vasild commented Feb 14, 2023

9b656a5...7a93d7b: rebase and address suggestions

Copy link
Member
@jonatack jonatack left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approach ACK. (Edit: it looks like the fixed issues could use test coverage, if feasible.)

NET_ONION,
NET_I2P,
NET_CJDNS,
NET_INTERNAL
Copy link
Member
@jonatack jonatack Feb 14, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9e47818 As BIP155 networks are added or removed over time, m_reachable could go out of sync with enum Network in netaddress.h ... perhaps construct it programmatically from enum Network (now or in a follow-up)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no built in construct in the language to do that and I do not like NET_MAX which I perceive as a hack.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree with you about NET_MAX but using it to populate m_reachable may still be preferable to having a silent dependency where we need to keep ReachableNets::m_reachable in netbase.h manually in sync with the Network enumerators in netaddress.h. If you prefer the latter approach, perhaps comment next to ReachableNets::m_reachable that it needs to be kept in sync.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about something like the following as a followup, aiming to workaround the limitation in C++ in general:

C++ enum + a way to list all values + ToString() method, encapsulated
namespace Nets
{
    enum Net : uint8_t {
        UNROUTABLE,
        IPV4,
        IPV6,
        ONION,
        I2P,
        CJDNS,
        INTERNAL
    };

    static constexpr std::array<Net, 7> all{UNROUTABLE, IPV4, IPV6, ONION, I2P, CJDNS, INTERNAL};

    std::string ToString(Net net)
    {
        switch (net) {
        case UNROUTABLE: return "UNROUTABLE";
        case IPV4: return "IPV4";
        case IPV6: return "IPV6";
        case ONION: return "ONION";
        case I2P: return "I2P";
        case CJDNS: return "CJDNS";
        case INTERNAL: return "INTERNAL";
        }
    }
};

...

printf("IPV6 = %d\n", Nets::IPV6);

for (const auto& net: Nets::all) {
    std::cout << Nets::ToString(net) << std::endl;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd review it.

@vasild
Copy link
Contributor Author
vasild commented Feb 15, 2023

7a93d7b0f9...8991ed2c6e: address suggestions

Copy link
Member
@jonatack jonatack left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 8991ed2

@vasild
Copy link
Contributor Author
vasild commented Feb 16, 2023

Just thinking aloud:

  1. Subnetting does not make sense for tor/i2p/cjdns
  2. BanMan entries are always subnets (CSubNet objects), even for single host bans

It follows that banman is doing something that does not make sense (e.g. subnetting tor). This is the root of the problem. I think if that is eradicated, then the rest will untangle by itself.

What this means - store two ban maps in BanMan - one for single hosts and one for subnets (only for IPv[46] subnets). This way also lookup will be faster for single hosts - if a hash table is used for the single host bans, then lookup will be O(1). Right now we iterate over all ban entries and check if CSubNet::Match() is true (which is O(number of bans)).

Hmm...

@brunoerg
Copy link
Contributor

Subnetting does not make sense for tor/i2p/cjdns
It follows that banman is doing something that does not make sense (e.g. subnetting tor). This is the root of the problem. I think if that is eradicated, then the rest will untangle by itself.

CConnman::DisconnectNode also handles addresses as subnets. Is this a problem?

@vasild
Copy link
Contributor Author
vasild commented Feb 20, 2023

CConnman::DisconnectNode also handles addresses as subnets. Is this a problem?

Well, it works, but since it does something nonsensical (e.g. subnetting tor) it could be a source of confusion or ultimately, bugs. IMO it shouldn't be doing that. I think the existence and implementation of CConnman::DisconnectNode(const CSubNet& subnet) is ok - it will/should disconnect any IPv[46] address that matches the given IPv[46] subnet. But this one:

bool CConnman::DisconnectNode(const CNetAddr& addr)
{
    return DisconnectNode(CSubNet(addr));
}

would better iterate m_nodes and check for equality between the given addr and CNode::addr.

I will try to allow only IPv[46] CSubNets and see if everything blows up in a spectacular way... 💣 🎊

Copy link
Contributor
@mzumsande mzumsande left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It follows that banman is doing something that does not make sense (e.g. subnetting tor). This is the root of the problem.

Not sure if I agree that this is the root of the problem - I think conceptually it's fine to internally treat single addresses as a special case of a subnet (like <ipv4>/32 or <ipv6>/128), and just prevent nonsensical things during user input.

In my opinion, the root of the problem is that fc... addresses are sometimes interpreted as CJDNS, and sometimes as IPv6, depending on startup options. Since large parts of the networking code were designed before CJDNS support was introduced, this makes workarounds like MaybeFlipIPv6toCJDNS() and global availability of reachability necessary.

However, I still feel strongly that having a global, non-constant variable shared by the GUI and node is something we should absolutely avoid.
Another possibility to achieve this would be to pass a bool cjdnsreachable as a variable to LookupSubNet() and add IsReachable() to node/interfaces.cpp. With this, there could still be a global g_reachable_nets used for the call sites within node, but the information to the GUI would flow through the interface.
A similar approach was chosen for the proxy, which is also state from netbase: The GUI code calls getProxy from node/interfaces, which then calls GetProxy() to retrieve the proxy info from netbase. The GUI doesn't use GetProxy() directly.

@vasild
Copy link
Contributor Author
vasild commented Feb 24, 2023

8991ed2c6e...c2a86ba667: rebase due to conflicts and address suggestion

Invalidates ACK from @jonatack

@mzumsande, actually there are two problems - I am talking about Tor subnets and you are talking about a global variable used by the GUI. I will try to mimic the proxy in order to address the latter.

@jonatack
Copy link
Member
jonatack commented Feb 24, 2023

I will try to mimic the proxy in order to address the latter.

In case you plan to do that, or the Tor subnets issue, in a follow-up:

re-ACK c2a86ba per git range-diff be2e748 8991ed2 c2a86ba6

@vasild
Copy link
Contributor Author
vasild commented Mar 6, 2023

@mzumsande, it is possible to avoid calling LookupSubNet() from the GUI, see bitcoin-core/gui#717. If that gets merged then this PR will be simplified - no need to touch the GUI code from here.

vasild added 4 commits October 5, 2023 15:10
The fuzz testing framework runs as if `-cjdnsreachable` is set and in
this case addresses like `{net=IPv6, addr=fc...}` are not possible.
`vfLimited`, `IsReachable()`, `SetReachable()` need not be in the `net`
module. Move them to `netbase` because they will be needed in
`LookupSubNet()` to possibly flip the result to CJDNS (if that network
is reachable).

In the process, encapsulate them in a class.

`NET_UNROUTABLE` and `NET_INTERNAL` are no longer ignored when adding
or removing reachable networks. This was unnecessary.
It need not be in the `net` module and we need to call it from
`LookupSubNet()`, thus move it to `netbase`.
@vasild vasild force-pushed the lookup_subnet_cjdns branch from 6ff2241 to 2f3f793 Compare October 5, 2023 13:15
@vasild
Copy link
Contributor Author
vasild commented Oct 5, 2023

6ff2241e47...2f3f793605: rebase due to conflicts and address suggestion

@achow101 achow101 added this to the 26.0 milestone Oct 12, 2023
Copy link
Member
@jonatack jonatack left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ACK 2f3f793

In 0994d45 the commit message might be out of date regarding unbanSelectedNode after the merge of bitcoin-core/gui#717.

A couple questions and a comment, feel free to ignore

All callers of `LookupSubNet()` need the result to be of CJDNS type if
`-cjdnsreachable` is set and the address begins with `fc`:

* `NetWhitelistPermissions::TryParse()`: otherwise `-whitelist=` fails
  to white list CJDNS addresses: when a CJDNS peer connects to us, it
  will be matched against IPv6 `fc...` subnet and the match will never
  succeed.

* `BanMapFromJson()`: CJDNS bans are stored as just IPv6 addresses in
  `banlist.json`. Upon reading from disk they have to be converted back
  to CJDNS, otherwise, after restart, a ban entry like (`fc00::1`, IPv6)
  would not match a peer (`fc00::1`, CJDNS).

* `setban()` (in `rpc/net.cpp`): otherwise `setban fc.../mask add` would
  add an IPv6 entry to BanMan. Subnetting does not make sense for CJDNS
  addresses, thus treat `fc.../mask` as invalid `CSubNet`. The result of
  `LookupHost()` has to be converted for the case of banning a single
  host.

* `InitHTTPAllowList()`: not necessary since before this change
  `-rpcallowip=fc...` would match IPv6 subnets against IPv6 peers even
  if they started with `fc`. But because it is necessary for the above,
  `HTTPRequest::GetPeer()` also has to be adjusted to return CJDNS peer,
  so that now CJDNS peers are matched against CJDNS subnets.
@vasild vasild force-pushed the lookup_subnet_cjdns branch from 2f3f793 to 0e6f6eb Compare October 16, 2023 11:00
@vasild
Copy link
Contributor Author
vasild commented Oct 16, 2023

2f3f793605...0e6f6ebc06: address suggestions, thanks!

In 0994d45 the commit message might be out of date...

Updated!

Copy link
Contributor
@mzumsande mzumsande left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

re-ACK 0e6f6eb

only rebase / minor changes since last ACK

@DrahtBot DrahtBot requested a review from jonatack October 16, 2023 18:38
@jonatack
Copy link
Member

Code review ACK 0e6f6eb

@DrahtBot DrahtBot removed the request for review from jonatack October 16, 2023 18:50
@achow101
Copy link
Member

ACK 0e6f6eb

@achow101 achow101 merged commit 0655e9d into bitcoin:master Oct 19, 2023
Frank-GER pushed a commit to syscoin/syscoin that referenced this pull request Oct 21, 2023
@maflcko
Copy link
Member
maflcko commented Oct 22, 2023

This crashes https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=63434

echo '////ICAgICAgICD/ICAgIAYgICAgeXn/ICAgIAAAAIAgIP8gICA=' | base64 --decode > /tmp/minimized-banman-crash
FUZZ=banman ./src/test/fuzz/fuzz /tmp/mi
3D11
nimized-banman-crash


fuzz: test/fuzz/banman.cpp:112: void banman_fuzz_target(FuzzBufferType): Assertion `banmap == banmap_read' failed.
9482cb780fe04c1f1d9050edd1b8e549e52c86ce is the first bad commit
commit 9482cb780fe04c1f1d9050edd1b8e549e52c86ce
Author: Vasil Dimov <vd@FreeBSD.org>
Date:   Tue Feb 7 15:16:57 2023 +0100

    netbase: possibly change the result of LookupSubNet() to CJDNS

@vasild vasild deleted the lookup_subnet_cjdns branch October 23, 2023 12:24
@bitcoin bitcoin locked and limited conversation to collaborators Oct 22, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants
0