mirror of
https://github.com/nix-community/home-manager
synced 2024-11-27 05:29:46 +01:00
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:
parent
3d546e0d01
commit
e8dbc35613
14 changed files with 377 additions and 26 deletions
|
@ -6,13 +6,18 @@ let
|
||||||
|
|
||||||
cfg = config.programs.ssh;
|
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";
|
yn = flag: if flag then "yes" else "no";
|
||||||
|
|
||||||
unwords = builtins.concatStringsSep " ";
|
unwords = builtins.concatStringsSep " ";
|
||||||
|
|
||||||
localForwardModule = types.submodule ({ ... }: {
|
bindOptions = {
|
||||||
options = {
|
|
||||||
bind = {
|
|
||||||
address = mkOption {
|
address = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
default = "localhost";
|
default = "localhost";
|
||||||
|
@ -27,6 +32,14 @@ let
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
dynamicForwardModule = types.submodule {
|
||||||
|
options = bindOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
forwardModule = types.submodule {
|
||||||
|
options = {
|
||||||
|
bind = bindOptions;
|
||||||
|
|
||||||
host = {
|
host = {
|
||||||
address = mkOption {
|
address = mkOption {
|
||||||
type = types.str;
|
type = types.str;
|
||||||
|
@ -41,7 +54,7 @@ let
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
});
|
};
|
||||||
|
|
||||||
matchBlockModule = types.submodule ({ name, ... }: {
|
matchBlockModule = types.submodule ({ name, ... }: {
|
||||||
options = {
|
options = {
|
||||||
|
@ -186,7 +199,7 @@ let
|
||||||
};
|
};
|
||||||
|
|
||||||
localForwards = mkOption {
|
localForwards = mkOption {
|
||||||
type = types.listOf localForwardModule;
|
type = types.listOf forwardModule;
|
||||||
default = [];
|
default = [];
|
||||||
example = literalExample ''
|
example = literalExample ''
|
||||||
[
|
[
|
||||||
|
@ -202,7 +215,43 @@ let
|
||||||
<citerefentry>
|
<citerefentry>
|
||||||
<refentrytitle>ssh_config</refentrytitle>
|
<refentrytitle>ssh_config</refentrytitle>
|
||||||
<manvolnum>5</manvolnum>
|
<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.proxyCommand != null) " ProxyCommand ${cf.proxyCommand}"
|
||||||
++ optional (cf.proxyJump != null) " ProxyJump ${cf.proxyJump}"
|
++ optional (cf.proxyJump != null) " ProxyJump ${cf.proxyJump}"
|
||||||
++ map (file: " IdentityFile ${file}") cf.identityFile
|
++ map (file: " IdentityFile ${file}") cf.identityFile
|
||||||
++ map (f:
|
++ map (f: " LocalForward" + addressPort f.bind + addressPort f.host) cf.localForwards
|
||||||
let
|
++ map (f: " RemoteForward" + addressPort f.bind + addressPort f.host) cf.remoteForwards
|
||||||
addressPort = entry: " [${entry.address}]:${toString entry.port}";
|
++ map (f: " DynamicForward" + addressPort f) cf.dynamicForwards
|
||||||
in
|
|
||||||
" LocalForward"
|
|
||||||
+ addressPort f.bind
|
|
||||||
+ addressPort f.host
|
|
||||||
) cf.localForwards
|
|
||||||
++ mapAttrsToList (n: v: " ${n} ${v}") cf.extraOptions
|
++ mapAttrsToList (n: v: " ${n} ${v}") cf.extraOptions
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -370,6 +414,25 @@ in
|
||||||
};
|
};
|
||||||
|
|
||||||
config = mkIf cfg.enable {
|
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 = ''
|
home.file.".ssh/config".text = ''
|
||||||
${concatStringsSep "\n" (
|
${concatStringsSep "\n" (
|
||||||
mapAttrsToList (n: v: "${n} ${v}") cfg.extraOptionOverrides)}
|
mapAttrsToList (n: v: "${n} ${v}") cfg.extraOptionOverrides)}
|
||||||
|
|
|
@ -8,9 +8,16 @@ with lib;
|
||||||
enable = true;
|
enable = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
home.file.assertions.text =
|
||||||
|
builtins.toJSON
|
||||||
|
(map (a: a.message)
|
||||||
|
(filter (a: !a.assertion)
|
||||||
|
config.assertions));
|
||||||
|
|
||||||
nmt.script = ''
|
nmt.script = ''
|
||||||
assertFileExists home-files/.ssh/config
|
assertFileExists home-files/.ssh/config
|
||||||
assertFileContent home-files/.ssh/config ${./default-config-expected.conf}
|
assertFileContent home-files/.ssh/config ${./default-config-expected.conf}
|
||||||
|
assertFileContent home-files/assertions ${./no-assertions.json}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,17 @@
|
||||||
{
|
{
|
||||||
ssh-defaults = ./default-config.nix;
|
ssh-defaults = ./default-config.nix;
|
||||||
ssh-match-blocks = ./match-blocks-attrs.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,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}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
|
@ -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}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
|
@ -12,6 +12,9 @@ Host xyz
|
||||||
ServerAliveInterval 60
|
ServerAliveInterval 60
|
||||||
IdentityFile file
|
IdentityFile file
|
||||||
LocalForward [localhost]:8080 [10.0.0.1]:80
|
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 *
|
Host *
|
||||||
ForwardAgent no
|
ForwardAgent no
|
||||||
|
|
|
@ -22,6 +22,22 @@ with lib;
|
||||||
host.port = 80;
|
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" = {
|
"* !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 = ''
|
nmt.script = ''
|
||||||
assertFileExists home-files/.ssh/config
|
assertFileExists home-files/.ssh/config
|
||||||
assertFileContent \
|
assertFileContent \
|
||||||
home-files/.ssh/config \
|
home-files/.ssh/config \
|
||||||
${./match-blocks-attrs-expected.conf}
|
${./match-blocks-attrs-expected.conf}
|
||||||
|
assertFileContent home-files/assertions ${./no-assertions.json}
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
1
tests/modules/programs/ssh/no-assertions.json
Normal file
1
tests/modules/programs/ssh/no-assertions.json
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[]
|
Loading…
Reference in a new issue