Skip to content
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

Args: -noconnect=0 is interpreted as -connect=0.0.0.1 #31426

Open
hodlinator opened this issue Dec 5, 2024 · 14 comments
Open

Args: -noconnect=0 is interpreted as -connect=0.0.0.1 #31426

hodlinator opened this issue Dec 5, 2024 · 14 comments

Comments

@hodlinator
Copy link
Contributor

hodlinator commented Dec 5, 2024

Motivation

-noconnect=0 is interpreted as -connect=1 which is interpreted as -connect=0.0.0.1

₿ build/src/bitcoind -noconnect=0 -debug=net

Produces the following output:

2024-12-05T08:20:41Z Warning: parsed potentially confusing double-negative -connect=0
2024-12-05T08:20:41Z Bitcoin Core version v28.99.0-95a0104f2e98-dirty (release build)
2024-12-05T08:20:41Z parameter interaction: -connect or -maxconnections=0 set -> setting -dnsseed=0
2024-12-05T08:20:41Z parameter interaction: -connect or -maxconnections=0 set -> setting -listen=0
2024-12-05T08:20:41Z parameter interaction: -listen=0 -> setting -natpmp=0
2024-12-05T08:20:41Z parameter interaction: -listen=0 -> setting -discover=0
2024-12-05T08:20:41Z parameter interaction: -listen=0 -> setting -listenonion=0
2024-12-05T08:20:41Z parameter interaction: -listen=0 -> setting -i2pacceptincoming=0
...
2024-12-05T08:20:44Z net thread start
2024-12-05T08:20:49Z [net] connection attempt to 0.0.0.1:8333 timed out
2024-12-05T08:20:50Z [net] trying v2 connection 1 lastseen=0.0hrs

Main issue

bitcoind should not try to connect to the 0.0.0.1 IPv4 address.

Bonus

-noconnect=0 should not result in -dnsseed=0 and -listen=0 in the parameter interaction logic.

Issue inspiration: #31212 (comment)

Possible solution

Probably best to fail the Init-stage for invalid -(no)connect(=value) permutations.

Should include a functional test verifying that there is an Init-error for this case, possibly added to test/functional/feature_config_args.py.

@vasild
Copy link
Contributor

vasild commented Dec 5, 2024

Probably best to just fail the Init-stage with an error if IsArgNegated() for -connect is true and the value is not 0.

But:

config args.IsArgNegated("-connect") args.GetArg("-connect").value() args.GetArgs("-connect")[0]
-connect=1.2.3.4:1234 false "1.2.3.4:1234" "1.2.3.4:1234"
-connect=0 false "0" "0"
-connect=1 false "1" "1"
-connect=xyz false "xyz" "xyz"
-connect false "" ""
-noconnect=1.2.3.4:1234 true "0" (the array is empty)
-noconnect=0 false "1" "1"
-noconnect=1 true "0" (the array is empty)
-noconnect=xyz false "1" "1"
-noconnect true "0" (the array is empty)

😭

@hodlinator
Copy link
Contributor Author

Oh boy, maybe not a good first issue. 😮‍💨

@hodlinator
Copy link
Contributor Author

@vasild how does the table look if you re-test using args.GetArgs("-connect") as is used in the code instead of args.GetArg("-connect")?

@Julian128
Copy link

Hi guys, did you just produce this table or is it from a documentation?

@hodlinator
Copy link
Contributor Author

Hi guys, did you just produce this table or is it from a documentation?

I believe vasild hand-rolled modifications to output the values.

On another note, it seems like I don't have permissions to remove the "good first issue" label. Anyone with access, please remove it.

@Julian128 feel free to take a stab at it anyway, but I'm no longer clear on what the solution would be. Will edit the description.

@vasild
Copy link
Contributor

vasild commented Dec 5, 2024

@vasild how does the table look if you re-test using args.GetArgs("-connect") as is used in the code instead of args.GetArg("-connect")?

I updated the table... but why did you had to ask! Now it looks even more convoluted 🫤 😀

Hi guys, did you just produce this table or is it from a documentation?

I created the table manually, and used something like this to get the values:

[patch] print -connect value
--- i/src/init.cpp
+++ w/src/init.cpp
@@ -1939,12 +1939,16 @@ bool AppInitMain(NodeContext& node, interfaces::BlockAndHeaderTipInfo* tip_info)
             connOptions.vWhitelistedRangeOutgoing.push_back(subnet);
         }
     }
     
     connOptions.vSeedNodes = args.GetArgs("-seednode");

+    return InitError(strprintf(_("is negated: %s, size: %u, value[0]: %s"),
+                               args.IsArgNegated("-connect") ? "true" : "false",
+                               args.GetArgs("-connect").size(),
+                               args.GetArgs("-connect").size() > 0 ? args.GetArgs("-connect")[0] : "empty"));
     // Initiate outbound connections unless connect=0
     connOptions.m_use_addrman_outgoing = !args.IsArgSet("-connect");
     if (!connOptions.m_use_addrman_outgoing) {
         const auto connect = args.GetArgs("-connect");
         if (connect.size() != 1 || connect[0] != "0") {
             connOptions.m_specified_outgoing = connect;

What about, generally, forbidding =value if the config argument is prefixed with -no? That is, if we have -foo config option, then only -nofoo is allowed, but -nofoo=whatever gives an error.

@ryanofsky
Copy link
Contributor

ryanofsky commented Dec 5, 2024

I think a good way to handle this is to make the -connect setting typed using the syntax implemented in #31260:

using ConnectSetting = common::Setting<
    "-connect=<ip>", std::variant<common::Disabled, std::vector<std::string>>, SettingOptions{.network_only = true},
    "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.">
    ::Category<OptionsCategory::CONNECTION>;

Retrieving the setting with ConnectSetting::Get() would return the specified std::variant<common::Disabled, std::vector<std::string>> type which could be interpreted unambiguously. With the runtime type checking from #16545 also applied (the corresponding flags there are ALLOW_STRING | ALLOW_LIST), the results would be:

argument behavior
-connect=1.2.3.4:1234 Get returns vector {"1.2.3.4:1234"}
-connect=0 Get returns vector {"0"}
-connect=1 Get returns vector {"1"}
-connect=xyz Get returns vector {"xyz"}
-connect Startup error "Cannot set -connect with no value. Please specify value with -connect=value. It must be set to a string."
-noconnect=1.2.3.4:1234 Startup error "Cannot negate -connect at the same time as setting a value (''1.2.3.4:1234")."
-noconnect=0 Startup error "Cannot negate -connect at the same time as setting a value (''0")."
-noconnect=1 Get returns common::Disabled
-noconnect=xyz Startup error "Cannot negate -connect at the same time as setting a value (''xyz")."
-noconnect Get returns common::Disabled
(none) Get returns empty vector.`

If using std::variant as suggested above seems cumbersome, #31260 also allows specifying the type as std::optional<std::vector<std::string>> which may be a little less obvious but is still unambiguous. Using that type, -noconnect=1 and -noconnect would cause Get() to return the empty vector, and specifying no -connect arguments would make Get() return std::nullopt

I do think, in general 0 and 1 addresses should be allowed since most network tools do allow them, 0 can be a convenient way to specify localhost, and in general network stacks probably have more context for interpreting addresses than we do.

@Julian128
Copy link

Julian128 commented Dec 5, 2024

These double negatives are inherently confusing.

I wonder if this is making things more convoluted than they have to be, what if we simply disallow double negatives?
So any negated arg with value 0 would return null.

Or is there any reason to use such a flag?

edit: I found this comment regarding this:
// Double negatives like -nofoo=0 are supported (but discouraged)

@l0rinc
Copy link
Contributor

l0rinc commented Dec 6, 2024

Agree, can we rather prohibit double negations?

@hodlinator
Copy link
Contributor Author

Agree on the double negatives, I think @ryanofsky's change #16545 is pretty spot on in his table above for the -noconnect behavior. Just error when specifying any other value than -noconnect=1. Maybe that behavior could be broken out and submitted in its own PR? Shouldn't depend on the typing changes.

I do think, in general 0 and 1 addresses should be allowed

Oof, that's suprising to me, but never thought to try them in other networking contexts. -connect=10 is currently interpreted as 0.0.0.10, would almost have expected hex when there are no dots. -connect=256 becomes 0.0.1.0 which I guess makes sense.

@ryanofsky
Copy link
Contributor

These double negatives are inherently confusing.

Agree, can we rather prohibit double negations?

Agree on the double negatives

Not sure when double negatives (-noconnect=0) were introduced but they have been around a long time, and there has been a warning whenever double negatives are used since #12713. It seems ok if someone wants to make a PR changing double negatives from a warning to an error and including tests, release notes, etc. Though I think a more direct fix for this issue would probably be:

--- a/src/common/args.cpp
+++ b/src/common/args.cpp
@@ -363,7 +363,8 @@ std::vector<std::string> ArgsManager::GetArgs(const std::string& strArg) const
 {
     std::vector<std::string> result;
     for (const common::SettingsValue& value : GetSettingsList(strArg)) {
-        result.push_back(value.isFalse() ? "0" : value.isTrue() ? "1" : value.get_str());
+        // Treat double-negative -noarg=0 the same as -arg and return ""
+        result.push_back(value.isFalse() ? "0" : value.isTrue() ? "" : value.get_str());
     }
     return result;
 }

IMO double negatives are just tip of the iceberg when it comes to unusual option parsing behavior, compared to more harmful behavior like interpreting -booloption=true as false and hard to predict merging behaviors described #17508. My PR #16545 and the followups listed there try to clean up all of these behaviors comprehensively by introducing typed settings. However the ALLOW_ flags introduced in #16545 have been shown to be pretty confusing and unappealing to reviewers, so I worked on #31260 to replace the ALLOW_ flags with c++ types and hopefully this can unblock #16545 and its followups.

#16545 does disallow double negatives for typed settings. But apart from this bug, which I think is caused more by bad API design than by the double negatives, that double negatives are pretty innocuous and just a throwback to a more casual computing culture where developers trusted users more and thought things like this were convenient and cute.

@Julian128
Copy link

So there is nothing left to do here, your existing PRs will eventually take care of it?

@pinheadmz
Copy link
Member

Do we use type checking for any configuration argument? I just noticed i could start bitcoind with -prune=pancakes with no errors: also no idea what the prune limit would actually be set at.

@hodlinator
Copy link
Contributor Author

So there is nothing left to do here, your existing PRs will eventually take care of it?

Given the complexity of existing behavior, it may be better to help review one of @ryanofsky's giga-PRs. The older one has had its 5th year anniversary already.

Do we use type checking for any configuration argument?

Approximately zero type checking on a generalized level (ArgsManager).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

8 participants