Skip to content

Commit 9ff4c44

Browse files
authored
Merge pull request #3425 from mkg20001/pr
Add user@address:port support
2 parents 55f6ff3 + 49ba061 commit 9ff4c44

18 files changed

Lines changed: 312 additions & 101 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
synopsis: "Add support for user@address:port syntax in store URIs"
3+
prs: [3425]
4+
issues: [7044]
5+
---
6+
7+
It's now possible to specify the port used for the SSH stores directly in the store URL in accordance with [RFC3986](https://datatracker.ietf.org/doc/html/rfc3986). Previously the only way to specify custom ports was via `ssh_config` or `NIX_SSHOPTS` environment variable, because Nix incorrectly passed the port number together with the host name to the SSH executable. This has now been fixed.
8+
9+
This change affects [store references](@docroot@/store/types/index.md#store-url-format) passed via the `--store` and similar flags in CLI as well as in the configuration for [remote builders](@docroot@/command-ref/conf-file.md#conf-builders). For example, the following store URIs now work:
10+
11+
- `ssh://127.0.0.1:2222`
12+
- `ssh://[b573:6a48:e224:840b:6007:6275:f8f7:ebf3]:22`
13+
- `ssh-ng://[b573:6a48:e224:840b:6007:6275:f8f7:ebf3]:22`

src/libfetchers/git-lfs-fetch.cc

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,19 @@ static void downloadToSink(
4444

4545
static std::string getLfsApiToken(const ParsedURL & url)
4646
{
47+
assert(url.authority.has_value());
48+
49+
// FIXME: Not entirely correct.
4750
auto [status, output] = runProgram(
4851
RunOptions{
4952
.program = "ssh",
50-
.args = {*url.authority, "git-lfs-authenticate", url.path, "download"},
53+
.args = {url.authority->to_string(), "git-lfs-authenticate", url.path, "download"},
5154
});
5255

5356
if (output.empty())
5457
throw Error(
5558
"git-lfs-authenticate: no output (cmd: ssh %s git-lfs-authenticate %s download)",
56-
url.authority.value_or(""),
59+
url.authority.value_or(ParsedURL::Authority{}).to_string(),
5760
url.path);
5861

5962
auto queryResp = nlohmann::json::parse(output);

src/libfetchers/path.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ struct PathInputScheme : InputScheme
1515
if (url.scheme != "path")
1616
return {};
1717

18-
if (url.authority && *url.authority != "")
18+
if (url.authority && url.authority->host.size())
1919
throw Error("path URL '%s' should not have an authority ('%s')", url, *url.authority);
2020

2121
Input input{settings};

src/libflake/flakeref.cc

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ std::pair<FlakeRef, std::string> parsePathFlakeRefWithFragment(
142142
if (pathExists(flakeRoot + "/.git")) {
143143
auto parsedURL = ParsedURL{
144144
.scheme = "git+file",
145-
.authority = "",
145+
.authority = ParsedURL::Authority{},
146146
.path = flakeRoot,
147147
.query = query,
148148
.fragment = fragment,
@@ -172,7 +172,7 @@ std::pair<FlakeRef, std::string> parsePathFlakeRefWithFragment(
172172

173173
return fromParsedURL(
174174
fetchSettings,
175-
{.scheme = "path", .authority = "", .path = path, .query = query, .fragment = fragment},
175+
{.scheme = "path", .authority = ParsedURL::Authority{}, .path = path, .query = query, .fragment = fragment},
176176
isFlake);
177177
}
178178

@@ -192,7 +192,7 @@ parseFlakeIdRef(const fetchers::Settings & fetchSettings, const std::string & ur
192192
if (std::regex_match(url, match, flakeRegex)) {
193193
auto parsedURL = ParsedURL{
194194
.scheme = "flake",
195-
.authority = "",
195+
.authority = ParsedURL::Authority{},
196196
.path = match[1],
197197
};
198198

src/libstore-tests/machines.cc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,14 @@ TEST(machines, getMachinesUriOnly)
3939
EXPECT_THAT(actual[0], Field(&Machine::sshPublicHostKey, SizeIs(0)));
4040
}
4141

42+
TEST(machines, getMachinesUriWithPort)
43+
{
44+
auto actual = Machine::parseConfig({"TEST_ARCH-TEST_OS"}, "nix@scratchy.labs.cs.uu.nl:2222");
45+
ASSERT_THAT(actual, SizeIs(1));
46+
EXPECT_THAT(
47+
actual[0], Field(&Machine::storeUri, Eq(StoreReference::parse("ssh://nix@scratchy.labs.cs.uu.nl:2222"))));
48+
}
49+
4250
TEST(machines, getMachinesDefaults)
4351
{
4452
auto actual = Machine::parseConfig({"TEST_ARCH-TEST_OS"}, "nix@scratchy.labs.cs.uu.nl - - - - - - -");

src/libstore/common-ssh-store-config.cc

Lines changed: 6 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,33 +5,22 @@
55

66
namespace nix {
77

8-
static std::string extractConnStr(std::string_view scheme, std::string_view _connStr)
8+
CommonSSHStoreConfig::CommonSSHStoreConfig(std::string_view scheme, std::string_view authority, const Params & params)
9+
: CommonSSHStoreConfig(scheme, ParsedURL::Authority::parse(authority), params)
910
{
10-
if (_connStr.empty())
11-
throw UsageError("`%s` store requires a valid SSH host as the authority part in Store URI", scheme);
12-
13-
std::string connStr{_connStr};
14-
15-
std::smatch result;
16-
static std::regex v6AddrRegex("^((.*)@)?\\[(.*)\\]$");
17-
18-
if (std::regex_match(connStr, result, v6AddrRegex)) {
19-
connStr = result[1].matched ? result.str(1) + result.str(3) : result.str(3);
20-
}
21-
22-
return connStr;
2311
}
2412

25-
CommonSSHStoreConfig::CommonSSHStoreConfig(std::string_view scheme, std::string_view host, const Params & params)
13+
CommonSSHStoreConfig::CommonSSHStoreConfig(
14+
std::string_view scheme, const ParsedURL::Authority & authority, const Params & params)
2615
: StoreConfig(params)
27-
, host(extractConnStr(scheme, host))
16+
, authority(authority)
2817
{
2918
}
3019

3120
SSHMaster CommonSSHStoreConfig::createSSHMaster(bool useMaster, Descriptor logFD) const
3221
{
3322
return {
34-
host,
23+
authority,
3524
sshKey.get(),
3625
sshPublicHostKey.get(),
3726
useMaster,

src/libstore/include/nix/store/common-ssh-store-config.hh

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
///@file
33

44
#include "nix/store/store-api.hh"
5+
#include "nix/util/url.hh"
56

67
namespace nix {
78

@@ -11,7 +12,8 @@ struct CommonSSHStoreConfig : virtual StoreConfig
1112
{
1213
using StoreConfig::StoreConfig;
1314

14-
CommonSSHStoreConfig(std::string_view scheme, std::string_view host, const Params & params);
15+
CommonSSHStoreConfig(std::string_view scheme, const ParsedURL::Authority & authority, const Params & params);
16+
CommonSSHStoreConfig(std::string_view scheme, std::string_view authority, const Params & params);
1517

1618
const Setting<Path> sshKey{
1719
this, "", "ssh-key", "Path to the SSH private key used to authenticate to the remote machine."};
@@ -32,23 +34,9 @@ struct CommonSSHStoreConfig : virtual StoreConfig
3234
)"};
3335

3436
/**
35-
* The `parseURL` function supports both IPv6 URIs as defined in
36-
* RFC2732, but also pure addresses. The latter one is needed here to
37-
* connect to a remote store via SSH (it's possible to do e.g. `ssh root@::1`).
38-
*
39-
* When initialized, the following adjustments are made:
40-
*
41-
* - If the URL looks like `root@[::1]` (which is allowed by the URL parser and probably
42-
* needed to pass further flags), it
43-
* will be transformed into `root@::1` for SSH (same for `[::1]` -> `::1`).
44-
*
45-
* - If the URL looks like `root@::1` it will be left as-is.
46-
*
47-
* - In any other case, the string will be left as-is.
48-
*
49-
* Will throw an error if `connStr` is empty too.
37+
* Authority representing the SSH host to connect to.
5038
*/
51-
std::string host;
39+
ParsedURL::Authority authority;
5240

5341
/**
5442
* Small wrapper around `SSHMaster::SSHMaster` that gets most

src/libstore/include/nix/store/globals.hh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -291,7 +291,7 @@ public:
291291
Only the first element is required.
292292
To leave a field at its default, set it to `-`.
293293
294-
1. The URI of the remote store in the format `ssh://[username@]hostname`.
294+
1. The URI of the remote store in the format `ssh://[username@]hostname[:port]`.
295295
296296
> **Example**
297297
>

src/libstore/include/nix/store/ssh.hh

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
///@file
33

44
#include "nix/util/sync.hh"
5+
#include "nix/util/url.hh"
56
#include "nix/util/processes.hh"
67
#include "nix/util/file-system.hh"
78

@@ -11,7 +12,8 @@ class SSHMaster
1112
{
1213
private:
1314

14-
const std::string host;
15+
ParsedURL::Authority authority;
16+
std::string hostnameAndUser;
1517
bool fakeSSH;
1618
const std::string keyFile;
1719
/**
@@ -43,7 +45,7 @@ private:
4345
public:
4446

4547
SSHMaster(
46-
std::string_view host,
48+
const ParsedURL::Authority & authority,
4749
std::string_view keyFile,
4850
std::string_view sshPublicHostKey,
4951
bool useMaster,

src/libstore/legacy-ssh-store.cc

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ namespace nix {
1818

1919
LegacySSHStoreConfig::LegacySSHStoreConfig(std::string_view scheme, std::string_view authority, const Params & params)
2020
: StoreConfig(params)
21-
, CommonSSHStoreConfig(scheme, authority, params)
21+
, CommonSSHStoreConfig(scheme, ParsedURL::Authority::parse(authority), params)
2222
{
2323
}
2424

@@ -71,25 +71,26 @@ ref<LegacySSHStore::Connection> LegacySSHStore::openConnection()
7171
TeeSource tee(conn->from, saved);
7272
try {
7373
conn->remoteVersion =
74-
ServeProto::BasicClientConnection::handshake(conn->to, tee, SERVE_PROTOCOL_VERSION, config->host);
74+
ServeProto::BasicClientConnection::handshake(conn->to, tee, SERVE_PROTOCOL_VERSION, config->authority.host);
7575
} catch (SerialisationError & e) {
7676
// in.close(): Don't let the remote block on us not writing.
7777
conn->sshConn->in.close();
7878
{
7979
NullSink nullSink;
8080
tee.drainInto(nullSink);
8181
}
82-
throw Error("'nix-store --serve' protocol mismatch from '%s', got '%s'", config->host, chomp(saved.s));
82+
throw Error(
83+
"'nix-store --serve' protocol mismatch from '%s', got '%s'", config->authority.host, chomp(saved.s));
8384
} catch (EndOfFile & e) {
84-
throw Error("cannot connect to '%1%'", config->host);
85+
throw Error("cannot connect to '%1%'", config->authority.host);
8586
}
8687

8788
return conn;
8889
};
8990

9091
std::string LegacySSHStore::getUri()
9192
{
92-
return *Config::uriSchemes().begin() + "://" + config->host;
93+
return *Config::uriSchemes().begin() + "://" + config->authority.to_string();
9394
}
9495

9596
std::map<StorePath, UnkeyedValidPathInfo> LegacySSHStore::queryPathInfosUncached(const StorePathSet & paths)
@@ -99,7 +100,10 @@ std::map<StorePath, UnkeyedValidPathInfo> LegacySSHStore::queryPathInfosUncached
99100
/* No longer support missing NAR hash */
100101
assert(GET_PROTOCOL_MINOR(conn->remoteVersion) >= 4);
101102

102-
debug("querying remote host '%s' for info on '%s'", config->host, concatStringsSep(", ", printStorePathSet(paths)));
103+
debug(
104+
"querying remote host '%s' for info on '%s'",
105+
config->authority.host,
106+
concatStringsSep(", ", printStorePathSet(paths)));
103107

104108
auto infos = conn->queryPathInfos(*this, paths);
105109

@@ -136,7 +140,7 @@ void LegacySSHStore::queryPathInfoUncached(
136140

137141
void LegacySSHStore::addToStore(const ValidPathInfo & info, Source & source, RepairFlag repair, CheckSigsFlag checkSigs)
138142
{
139-
debug("adding path '%s' to remote host '%s'", printStorePath(info.path), config->host);
143+
debug("adding path '%s' to remote host '%s'", printStorePath(info.path), config->authority.host);
140144

141145
auto conn(connections->get());
142146

@@ -157,7 +161,8 @@ void LegacySSHStore::addToStore(const ValidPathInfo & info, Source & source, Rep
157161
conn->to.flush();
158162

159163
if (readInt(conn->from) != 1)
160-
throw Error("failed to add path '%s' to remote host '%s'", printStorePath(info.path), config->host);
164+
throw Error(
165+
"failed to add path '%s' to remote host '%s'", printStorePath(info.path), config->authority.host);
161166

162167
} else {
163168

0 commit comments

Comments
 (0)