Skip to content

Commit

Permalink
ssh: sockets forwards; remote and dynamic forwards
Browse files Browse the repository at this point in the history
This commit adds support for forwarding paths rather than just
addresses/ports. It also adds options for specifying remote and
dynamic forwards.
  • Loading branch information
davidtwco authored and rycee committed Oct 2, 2019
1 parent 3d546e0 commit e8dbc35
Show file tree
Hide file tree
Showing 14 changed files with 377 additions and 26 deletions.
115 changes: 89 additions & 26 deletions modules/programs/ssh.nix
Expand Up @@ -6,26 +6,39 @@ let

cfg = config.programs.ssh;

isPath = x: builtins.substring 0 1 (toString x) == "/";

addressPort = entry:
if isPath entry.address
then " ${entry.address}"
else " [${entry.address}]:${toString entry.port}";

yn = flag: if flag then "yes" else "no";

unwords = builtins.concatStringsSep " ";

localForwardModule = types.submodule ({ ... }: {
options = {
bind = {
address = mkOption {
type = types.str;
default = "localhost";
example = "example.org";
description = "The address where to bind the port.";
};
bindOptions = {
address = mkOption {
type = types.str;
default = "localhost";
example = "example.org";
description = "The address where to bind the port.";
};

port = mkOption {
type = types.port;
example = 8080;
description = "Specifies port number to bind on bind address.";
};
};
port = mkOption {
type = types.port;
example = 8080;
description = "Specifies port number to bind on bind address.";
};
};

dynamicForwardModule = types.submodule {
options = bindOptions;
};

forwardModule = types.submodule {
options = {
bind = bindOptions;

host = {
address = mkOption {
Expand All @@ -41,7 +54,7 @@ let
};
};
};
});
};

matchBlockModule = types.submodule ({ name, ... }: {
options = {
Expand Down Expand Up @@ -186,7 +199,7 @@ let
};

localForwards = mkOption {
type = types.listOf localForwardModule;
type = types.listOf forwardModule;
default = [];
example = literalExample ''
[
Expand All @@ -202,7 +215,43 @@ let
<citerefentry>
<refentrytitle>ssh_config</refentrytitle>
<manvolnum>5</manvolnum>
</citerefentry> for LocalForward.
</citerefentry> for <literal>LocalForward</literal>.
'';
};

remoteForwards = mkOption {
type = types.listOf forwardModule;
default = [];
example = literalExample ''
[
{
bind.port = 8080;
host.address = "10.0.0.13";
host.port = 80;
}
];
'';
description = ''
Specify remote port forwardings. See
<citerefentry>
<refentrytitle>ssh_config</refentrytitle>
<manvolnum>5</manvolnum>
</citerefentry> for <literal>RemoteForward</literal>.
'';
};

dynamicForwards = mkOption {
type = types.listOf dynamicForwardModule;
default = [];
example = literalExample ''
[ { port = 8080; } ];
'';
description = ''
Specify dynamic port forwardings. See
<citerefentry>
<refentrytitle>ssh_config</refentrytitle>
<manvolnum>5</manvolnum>
</citerefentry> for <literal>DynamicForward</literal>.
'';
};

Expand Down Expand Up @@ -235,14 +284,9 @@ let
++ optional (cf.proxyCommand != null) " ProxyCommand ${cf.proxyCommand}"
++ optional (cf.proxyJump != null) " ProxyJump ${cf.proxyJump}"
++ map (file: " IdentityFile ${file}") cf.identityFile
++ map (f:
let
addressPort = entry: " [${entry.address}]:${toString entry.port}";
in
" LocalForward"
+ addressPort f.bind
+ addressPort f.host
) cf.localForwards
++ map (f: " LocalForward" + addressPort f.bind + addressPort f.host) cf.localForwards
++ map (f: " RemoteForward" + addressPort f.bind + addressPort f.host) cf.remoteForwards
++ map (f: " DynamicForward" + addressPort f) cf.dynamicForwards
++ mapAttrsToList (n: v: " ${n} ${v}") cf.extraOptions
);

Expand Down Expand Up @@ -370,6 +414,25 @@ in
};

config = mkIf cfg.enable {
assertions = [
{
assertion =
let
# `builtins.any`/`lib.lists.any` does not return `true` if there are no elements.
any' = pred: items: if items == [] then true else any pred items;
# Check that if `entry.address` is defined, and is a path, that `entry.port` has not
# been defined.
noPathWithPort = entry: entry ? address && isPath entry.address -> !(entry ? port);
checkDynamic = block: any' noPathWithPort block.dynamicForwards;
checkBindAndHost = fwd: noPathWithPort fwd.bind && noPathWithPort fwd.host;
checkLocal = block: any' checkBindAndHost block.localForwards;
checkRemote = block: any' checkBindAndHost block.remoteForwards;
checkMatchBlock = block: all (fn: fn block) [ checkLocal checkRemote checkDynamic ];
in any' checkMatchBlock (builtins.attrValues cfg.matchBlocks);
message = "Forwarded paths cannot have ports.";
}
];

home.file.".ssh/config".text = ''
${concatStringsSep "\n" (
mapAttrsToList (n: v: "${n} ${v}") cfg.extraOptionOverrides)}
Expand Down
7 changes: 7 additions & 0 deletions tests/modules/programs/ssh/default-config.nix
Expand Up @@ -8,9 +8,16 @@ with lib;
enable = true;
};

home.file.assertions.text =
builtins.toJSON
(map (a: a.message)
(filter (a: !a.assertion)
config.assertions));

nmt.script = ''
assertFileExists home-files/.ssh/config
assertFileContent home-files/.ssh/config ${./default-config-expected.conf}
assertFileContent home-files/assertions ${./no-assertions.json}
'';
};
}
13 changes: 13 additions & 0 deletions tests/modules/programs/ssh/default.nix
@@ -1,4 +1,17 @@
{
ssh-defaults = ./default-config.nix;
ssh-match-blocks = ./match-blocks-attrs.nix;

ssh-forwards-dynamic-valid-bind-no-asserts =
./forwards-dynamic-valid-bind-no-asserts.nix;
ssh-forwards-dynamic-bind-path-with-port-asserts =
./forwards-dynamic-bind-path-with-port-asserts.nix;
ssh-forwards-local-bind-path-with-port-asserts =
./forwards-local-bind-path-with-port-asserts.nix;
ssh-forwards-local-host-path-with-port-asserts =
./forwards-local-host-path-with-port-asserts.nix;
ssh-forwards-remote-bind-path-with-port-asserts =
./forwards-remote-bind-path-with-port-asserts.nix;
ssh-forwards-remote-host-path-with-port-asserts =
./forwards-remote-host-path-with-port-asserts.nix;
}
@@ -0,0 +1,32 @@
{ config, lib, pkgs, ... }:

with lib;

{
config = {
programs.ssh = {
enable = true;
matchBlocks = {
dynamicBindPathWithPort = {
dynamicForwards = [
{
# Error:
address = "/run/user/1000/gnupg/S.gpg-agent.extra";
port = 3000;
}
];
};
};
};

home.file.result.text =
builtins.toJSON
(map (a: a.message)
(filter (a: !a.assertion)
config.assertions));

nmt.script = ''
assertFileContent home-files/result ${./forwards-paths-with-ports-error.json}
'';
};
}
@@ -0,0 +1,19 @@


Host dynamicBindAddressWithPort
DynamicForward [127.0.0.1]:3000

Host dynamicBindPathNoPort
DynamicForward /run/user/1000/gnupg/S.gpg-agent.extra

Host *
ForwardAgent no
Compression no
ServerAliveInterval 0
HashKnownHosts no
UserKnownHostsFile ~/.ssh/known_hosts
ControlMaster no
ControlPath ~/.ssh/master-%r@%n:%p
ControlPersist no


@@ -0,0 +1,45 @@
{ config, lib, pkgs, ... }:

with lib;

{
config = {
programs.ssh = {
enable = true;
matchBlocks = {
dynamicBindPathNoPort = {
dynamicForwards = [
{
# OK:
address = "/run/user/1000/gnupg/S.gpg-agent.extra";
}
];
};

dynamicBindAddressWithPort = {
dynamicForwards = [
{
# OK:
address = "127.0.0.1";
port = 3000;
}
];
};
};
};

home.file.result.text =
builtins.toJSON
(map (a: a.message)
(filter (a: !a.assertion)
config.assertions));

nmt.script = ''
assertFileExists home-files/.ssh/config
assertFileContent \
home-files/.ssh/config \
${./forwards-dynamic-valid-bind-no-asserts-expected.conf}
assertFileContent home-files/result ${./no-assertions.json}
'';
};
}
@@ -0,0 +1,36 @@
{ config, lib, pkgs, ... }:

with lib;

{
config = {
programs.ssh = {
enable = true;
matchBlocks = {
localBindPathWithPort = {
localForwards = [
{
# OK:
host.address = "127.0.0.1";
host.port = 3000;

# Error:
bind.address = "/run/user/1000/gnupg/S.gpg-agent.extra";
bind.port = 3000;
}
];
};
};
};

home.file.result.text =
builtins.toJSON
(map (a: a.message)
(filter (a: !a.assertion)
config.assertions));

nmt.script = ''
assertFileContent home-files/result ${./forwards-paths-with-ports-error.json}
'';
};
}
@@ -0,0 +1,36 @@
{ config, lib, pkgs, ... }:

with lib;

{
config = {
programs.ssh = {
enable = true;
matchBlocks = {
localHostPathWithPort = {
localForwards = [
{
# OK:
bind.address = "127.0.0.1";
bind.port = 3000;

# Error:
host.address = "/run/user/1000/gnupg/S.gpg-agent.extra";
host.port = 3000;
}
];
};
};
};

home.file.result.text =
builtins.toJSON
(map (a: a.message)
(filter (a: !a.assertion)
config.assertions));

nmt.script = ''
assertFileContent home-files/result ${./forwards-paths-with-ports-error.json}
'';
};
}
@@ -0,0 +1 @@
["Forwarded paths cannot have ports."]

0 comments on commit e8dbc35

Please sign in to comment.