From 7cc654afa996db8eb5e67df8972084d3f5e7bf87 Mon Sep 17 00:00:00 2001 From: Sergei Zimmerman Date: Tue, 9 Sep 2025 00:18:41 +0300 Subject: [PATCH] libstore: Reallow unbracketed IPv6 addresses in store references This implements a special back-compat shim to specifically allow unbracketed IPv6 addresses in store references. This is something that is relied upon in the wild and the old parsing logic accepted both ways (brackets were optional). This patch restores this behavior. As always, we didn't have any tests for this. Addresses #13937. --- .../ssh_unbracketed_ipv6_1.txt | 1 + .../ssh_unbracketed_ipv6_2.txt | 1 + .../ssh_unbracketed_ipv6_3.txt | 1 + src/libstore-tests/store-reference.cc | 35 ++++++++++++ src/libstore/meson.build | 1 + src/libstore/store-reference.cc | 54 ++++++++++++++++++- 6 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_1.txt create mode 100644 src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_2.txt create mode 100644 src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_3.txt diff --git a/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_1.txt b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_1.txt new file mode 100644 index 000000000..861b5bb35 --- /dev/null +++ b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_1.txt @@ -0,0 +1 @@ +ssh://::1 \ No newline at end of file diff --git a/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_2.txt b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_2.txt new file mode 100644 index 000000000..952d5a55d --- /dev/null +++ b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_2.txt @@ -0,0 +1 @@ +ssh://userinfo@fea5:23e1:3916:fc24:cb52:2837:2ecb:ea8e \ No newline at end of file diff --git a/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_3.txt b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_3.txt new file mode 100644 index 000000000..d1f17adac --- /dev/null +++ b/src/libstore-tests/data/store-reference/ssh_unbracketed_ipv6_3.txt @@ -0,0 +1 @@ +ssh://userinfo@fea5:23e1:3916:fc24:cb52:2837:2ecb:ea8e?a=b&c=d \ No newline at end of file diff --git a/src/libstore-tests/store-reference.cc b/src/libstore-tests/store-reference.cc index d9f040ab6..7b42b45a2 100644 --- a/src/libstore-tests/store-reference.cc +++ b/src/libstore-tests/store-reference.cc @@ -148,4 +148,39 @@ URI_TEST( .params = {}, })) +static StoreReference sshLoopbackIPv6{ + .variant = + StoreReference::Specified{ + .scheme = "ssh", + .authority = "[::1]", + }, +}; + +URI_TEST_READ(ssh_unbracketed_ipv6_1, sshLoopbackIPv6) + +static StoreReference sshIPv6AuthorityWithUserinfo{ + .variant = + StoreReference::Specified{ + .scheme = "ssh", + .authority = "userinfo@[fea5:23e1:3916:fc24:cb52:2837:2ecb:ea8e]", + }, +}; + +URI_TEST_READ(ssh_unbracketed_ipv6_2, sshIPv6AuthorityWithUserinfo) + +static StoreReference sshIPv6AuthorityWithUserinfoAndParams{ + .variant = + StoreReference::Specified{ + .scheme = "ssh", + .authority = "userinfo@[fea5:23e1:3916:fc24:cb52:2837:2ecb:ea8e]", + }, + .params = + { + {"a", "b"}, + {"c", "d"}, + }, +}; + +URI_TEST_READ(ssh_unbracketed_ipv6_3, sshIPv6AuthorityWithUserinfoAndParams) + } // namespace nix diff --git a/src/libstore/meson.build b/src/libstore/meson.build index 253152772..7aeacbab7 100644 --- a/src/libstore/meson.build +++ b/src/libstore/meson.build @@ -105,6 +105,7 @@ boost = dependency( 'container', # Shouldn't list, because can header-only, and Meson currently looks for libs #'regex', + 'url', ], include_type : 'system', ) diff --git a/src/libstore/store-reference.cc b/src/libstore/store-reference.cc index 2c54e497e..96ee829d0 100644 --- a/src/libstore/store-reference.cc +++ b/src/libstore/store-reference.cc @@ -1,11 +1,12 @@ -#include - #include "nix/util/error.hh" +#include "nix/util/split.hh" #include "nix/util/url.hh" #include "nix/store/store-reference.hh" #include "nix/util/file-system.hh" #include "nix/util/util.hh" +#include + namespace nix { static bool isNonUriPath(const std::string & spec) @@ -43,6 +44,29 @@ std::string StoreReference::render(bool withParams) const return res; } +namespace { + +struct SchemeAndAuthorityWithPath +{ + std::string_view scheme; + std::string_view authority; +}; + +} // namespace + +/** + * Return the 'scheme' and remove the '://' or ':' separator. + */ +static std::optional splitSchemePrefixTo(std::string_view string) +{ + auto scheme = splitPrefixTo(string, ':'); + if (!scheme) + return std::nullopt; + + splitPrefix(string, "//"); + return SchemeAndAuthorityWithPath{.scheme = *scheme, .authority = string}; +} + StoreReference StoreReference::parse(const std::string & uri, const StoreReference::Params & extraParams) { auto params = extraParams; @@ -90,6 +114,32 @@ StoreReference StoreReference::parse(const std::string & uri, const StoreReferen }, .params = std::move(params), }; + } else if (auto schemeAndAuthority = splitSchemePrefixTo(baseURI)) { + /* Back-compatibility shim to accept unbracketed IPv6 addresses after the scheme. + * Old versions of nix allowed that. Note that this is ambiguous and does not allow + * specifying the port number. For that the address must be bracketed, otherwise it's + * greedily assumed to be the part of the host address. */ + auto authorityString = schemeAndAuthority->authority; + auto userinfo = splitPrefixTo(authorityString, '@'); + auto maybeIpv6 = boost::urls::parse_ipv6_address(authorityString); + if (maybeIpv6) { + std::string fixedAuthority; + if (userinfo) { + fixedAuthority += *userinfo; + fixedAuthority += '@'; + } + fixedAuthority += '['; + fixedAuthority += authorityString; + fixedAuthority += ']'; + return { + .variant = + Specified{ + .scheme = std::string(schemeAndAuthority->scheme), + .authority = fixedAuthority, + }, + .params = std::move(params), + }; + } } }