ssh: sockets forwards; remote and dynamic forwards

This commit adds support for forwarding paths rather than just
addresses/ports. It also adds options for specifying remote and
dynamic forwards.
This commit is contained in:
David Wood 2019-08-20 12:20:39 +01:00 committed by Robert Helgesson
parent 3d546e0d01
commit e8dbc35613
No known key found for this signature in database
GPG key ID: 36BDAA14C2797E89
14 changed files with 377 additions and 26 deletions

View file

@ -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 {
@ -41,7 +54,7 @@ let
};
};
};
});
};
matchBlockModule = types.submodule ({ name, ... }: {
options = {
@ -186,7 +199,7 @@ let
};
localForwards = mkOption {
type = types.listOf localForwardModule;
type = types.listOf forwardModule;
default = [];
example = literalExample ''
[
@ -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>.
'';
};
@ -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
);
@ -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)}

View file

@ -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}
'';
};
}

View file

@ -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;
}

View file

@ -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}
'';
};
}

View file

@ -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

View file

@ -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}
'';
};
}

View file

@ -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}
'';
};
}

View file

@ -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}
'';
};
}

View file

@ -0,0 +1 @@
["Forwarded paths cannot have ports."]

View file

@ -0,0 +1,36 @@
{ config, lib, pkgs, ... }:
with lib;
{
config = {
programs.ssh = {
enable = true;
matchBlocks = {
remoteBindPathWithPort = {
remoteForwards = [
{
# 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}
'';
};
}

View file

@ -0,0 +1,36 @@
{ config, lib, pkgs, ... }:
with lib;
{
config = {
programs.ssh = {
enable = true;
matchBlocks = {
remoteHostPathWithPort = {
remoteForwards = [
{
# 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}
'';
};
}

View file

@ -12,6 +12,9 @@ Host xyz
ServerAliveInterval 60
IdentityFile file
LocalForward [localhost]:8080 [10.0.0.1]:80
RemoteForward [localhost]:8081 [10.0.0.2]:80
RemoteForward /run/user/1000/gnupg/S.gpg-agent.extra /run/user/1000/gnupg/S.gpg-agent
DynamicForward [localhost]:2839
Host *
ForwardAgent no

View file

@ -22,6 +22,22 @@ with lib;
host.port = 80;
}
];
remoteForwards = [
{
bind.port = 8081;
host.address = "10.0.0.2";
host.port = 80;
}
{
bind.address = "/run/user/1000/gnupg/S.gpg-agent.extra";
host.address = "/run/user/1000/gnupg/S.gpg-agent";
}
];
dynamicForwards = [
{
port = 2839;
}
];
};
"* !github.com" = {
@ -31,11 +47,18 @@ with lib;
};
};
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 \
${./match-blocks-attrs-expected.conf}
assertFileContent home-files/assertions ${./no-assertions.json}
'';
};
}

View file

@ -0,0 +1 @@
[]