Merge pull request 'hidden-ssh' (#21) from hidden-ssh into main
Reviewed-on: https://git.clan.lol/clan/clan-core/pulls/21
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -7,4 +7,6 @@ __pycache__
|
|||||||
.mypy_cache
|
.mypy_cache
|
||||||
.pytest_cache
|
.pytest_cache
|
||||||
.pythonenv
|
.pythonenv
|
||||||
|
.reports
|
||||||
.ruff_cache
|
.ruff_cache
|
||||||
|
htmlcov
|
||||||
|
|||||||
@@ -1,11 +1,17 @@
|
|||||||
{ self, lib, ... }: {
|
{ self, lib, ... }: {
|
||||||
flake.packages.x86_64-linux = {
|
flake.packages.x86_64-linux =
|
||||||
install-iso = (lib.nixosSystem {
|
let
|
||||||
system = "x86_64-linux";
|
installer = lib.nixosSystem {
|
||||||
modules = [
|
system = "x86_64-linux";
|
||||||
self.nixosModules.installer
|
modules = [
|
||||||
self.inputs.nixos-generators.nixosModules.all-formats
|
self.nixosModules.installer
|
||||||
];
|
self.inputs.nixos-generators.nixosModules.all-formats
|
||||||
}).config.formats.install-iso;
|
];
|
||||||
};
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
install-iso = installer.config.formats.install-iso;
|
||||||
|
install-vm-nogui = installer.config.formats.vm-nogui;
|
||||||
|
install-vm = installer.config.formats.vm;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,12 +31,12 @@
|
|||||||
installer = {
|
installer = {
|
||||||
imports = [
|
imports = [
|
||||||
./installer.nix
|
./installer.nix
|
||||||
./hidden-announce.nix
|
./hidden-ssh-announce.nix
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
hidden-announce = {
|
hidden-announce = {
|
||||||
imports = [
|
imports = [
|
||||||
./hidden-announce.nix
|
./hidden-ssh-announce.nix
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
, pkgs
|
, pkgs
|
||||||
, ...
|
, ...
|
||||||
}: {
|
}: {
|
||||||
options.hidden-announce = {
|
options.hidden-ssh-announce = {
|
||||||
enable = lib.mkEnableOption "hidden-announce";
|
enable = lib.mkEnableOption "hidden-ssh-announce";
|
||||||
script = lib.mkOption {
|
script = lib.mkOption {
|
||||||
type = lib.types.package;
|
type = lib.types.package;
|
||||||
default = pkgs.writers.writeDash "test-output";
|
default = pkgs.writers.writeDash "test-output" "echo $1";
|
||||||
description = ''
|
description = ''
|
||||||
script to run when the hidden tor service was started and they hostname is known.
|
script to run when the hidden tor service was started and they hostname is known.
|
||||||
takes the hostname as $1
|
takes the hostname as $1
|
||||||
@@ -15,7 +15,8 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf config.hidden-announce.enable {
|
config = lib.mkIf config.hidden-ssh-announce.enable {
|
||||||
|
services.openssh.enable = true;
|
||||||
services.tor = {
|
services.tor = {
|
||||||
enable = true;
|
enable = true;
|
||||||
relay.onionServices.hidden-ssh = {
|
relay.onionServices.hidden-ssh = {
|
||||||
@@ -43,7 +44,7 @@
|
|||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
|
|
||||||
${config.hidden-announce.script} "$(cat ${config.services.tor.settings.DataDirectory}/onion/hidden-ssh/hostname)"
|
${config.hidden-ssh-announce.script} "$(cat ${config.services.tor.settings.DataDirectory}/onion/hidden-ssh/hostname)"
|
||||||
'';
|
'';
|
||||||
PrivateTmp = "true";
|
PrivateTmp = "true";
|
||||||
User = "tor";
|
User = "tor";
|
||||||
@@ -7,19 +7,31 @@
|
|||||||
];
|
];
|
||||||
services.openssh.settings.PermitRootLogin = "yes";
|
services.openssh.settings.PermitRootLogin = "yes";
|
||||||
system.activationScripts.root-password = ''
|
system.activationScripts.root-password = ''
|
||||||
|
mkdir -p /var/shared
|
||||||
${pkgs.pwgen}/bin/pwgen -s 16 1 > /var/shared/root-password
|
${pkgs.pwgen}/bin/pwgen -s 16 1 > /var/shared/root-password
|
||||||
echo "root:$(cat /var/shared/root-password)" | chpasswd
|
echo "root:$(cat /var/shared/root-password)" | chpasswd
|
||||||
'';
|
'';
|
||||||
hidden-announce = {
|
hidden-ssh-announce = {
|
||||||
enable = true;
|
enable = true;
|
||||||
script = pkgs.writers.writeDash "write-hostname" ''
|
script = pkgs.writers.writeDash "write-hostname" ''
|
||||||
|
mkdir -p /var/shared
|
||||||
echo "$1" > /var/shared/onion-hostname
|
echo "$1" > /var/shared/onion-hostname
|
||||||
|
${pkgs.jq}/bin/jq -nc \
|
||||||
|
--arg password "$(cat /var/shared/root-password)" \
|
||||||
|
--arg address "$(cat /var/shared/onion-hostname)" '{
|
||||||
|
password: $password, address: $address
|
||||||
|
}' > /var/shared/login.info
|
||||||
|
cat /var/shared/login.info |
|
||||||
|
${pkgs.qrencode}/bin/qrencode -t utf8 > /var/shared/qrcode.utf8
|
||||||
|
cat /var/shared/login.info |
|
||||||
|
${pkgs.qrencode}/bin/qrencode -t png > /var/shared/qrcode.png
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
services.getty.autologinUser = lib.mkForce "root";
|
services.getty.autologinUser = lib.mkForce "root";
|
||||||
programs.bash.interactiveShellInit = ''
|
programs.bash.interactiveShellInit = ''
|
||||||
if [ "$(tty)" = "/dev/tty1" ]; then
|
if [ "$(tty)" = "/dev/tty1" ]; then
|
||||||
echo "ssh://root:$(cat /var/shared/root-password)@$(cat /var/shared/onion-hostname)"
|
until test -e /var/shared/qrcode.utf8; do sleep 1; done
|
||||||
|
cat /var/shared/qrcode.utf8
|
||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
formatConfigs.install-iso = {
|
formatConfigs.install-iso = {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from . import admin
|
from . import admin, ssh
|
||||||
|
|
||||||
has_argcomplete = True
|
has_argcomplete = True
|
||||||
try:
|
try:
|
||||||
@@ -18,12 +18,20 @@ def main() -> None:
|
|||||||
|
|
||||||
parser_admin = subparsers.add_parser("admin")
|
parser_admin = subparsers.add_parser("admin")
|
||||||
admin.register_parser(parser_admin)
|
admin.register_parser(parser_admin)
|
||||||
|
|
||||||
|
parser_ssh = subparsers.add_parser("ssh", help="ssh to a remote machine")
|
||||||
|
ssh.register_parser(parser_ssh)
|
||||||
|
|
||||||
if has_argcomplete:
|
if has_argcomplete:
|
||||||
argcomplete.autocomplete(parser)
|
argcomplete.autocomplete(parser)
|
||||||
parser.parse_args()
|
|
||||||
if len(sys.argv) == 1:
|
if len(sys.argv) == 1:
|
||||||
parser.print_help()
|
parser.print_help()
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
if hasattr(args, "func"):
|
||||||
|
args.func(args) # pragma: no cover
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": # pragma: no cover
|
if __name__ == "__main__": # pragma: no cover
|
||||||
main()
|
main()
|
||||||
|
|||||||
84
pkgs/clan-cli/clan_cli/ssh.py
Normal file
84
pkgs/clan-cli/clan_cli/ssh.py
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
def ssh(
|
||||||
|
host: str,
|
||||||
|
user: str = "root",
|
||||||
|
password: Optional[str] = None,
|
||||||
|
ssh_args: list[str] = [],
|
||||||
|
) -> None:
|
||||||
|
nix_shell_args = []
|
||||||
|
password_args = []
|
||||||
|
if password:
|
||||||
|
nix_shell_args = [
|
||||||
|
"nix",
|
||||||
|
"shell",
|
||||||
|
"nixpkgs#sshpass",
|
||||||
|
"-c",
|
||||||
|
]
|
||||||
|
password_args = [
|
||||||
|
"sshpass",
|
||||||
|
"-p",
|
||||||
|
password,
|
||||||
|
]
|
||||||
|
_ssh_args = ssh_args + [
|
||||||
|
"ssh",
|
||||||
|
"-o",
|
||||||
|
"UserKnownHostsFile=/dev/null",
|
||||||
|
"-o",
|
||||||
|
"StrictHostKeyChecking=no",
|
||||||
|
f"{user}@{host}",
|
||||||
|
]
|
||||||
|
cmd = nix_shell_args + ["torify"] + password_args + _ssh_args
|
||||||
|
subprocess.run(cmd)
|
||||||
|
|
||||||
|
|
||||||
|
def qrcode_scan(pictureFile: str) -> str:
|
||||||
|
return (
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"nix",
|
||||||
|
"shell",
|
||||||
|
"nixpkgs#zbar",
|
||||||
|
"-c",
|
||||||
|
"zbarimg",
|
||||||
|
"--quiet",
|
||||||
|
"--raw",
|
||||||
|
pictureFile,
|
||||||
|
],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
.stdout.decode()
|
||||||
|
.strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main(args: argparse.Namespace) -> None: # pragma: no cover
|
||||||
|
if args.json:
|
||||||
|
with open(args.json) as file:
|
||||||
|
ssh_data = json.load(file)
|
||||||
|
ssh(host=ssh_data["address"], password=ssh_data["password"])
|
||||||
|
elif args.png:
|
||||||
|
ssh_data = json.loads(qrcode_scan(args.png))
|
||||||
|
ssh(host=ssh_data["address"], password=ssh_data["password"])
|
||||||
|
|
||||||
|
|
||||||
|
def register_parser(parser: argparse.ArgumentParser) -> None:
|
||||||
|
group = parser.add_mutually_exclusive_group(required=True)
|
||||||
|
group.add_argument(
|
||||||
|
"-j",
|
||||||
|
"--json",
|
||||||
|
help="specify the json file for ssh data (generated by starting the clan installer)",
|
||||||
|
)
|
||||||
|
group.add_argument(
|
||||||
|
"-P",
|
||||||
|
"--png",
|
||||||
|
help="specify the json file for ssh data as the qrcode image (generated by starting the clan installer)",
|
||||||
|
)
|
||||||
|
# TODO pass all args we don't parse into ssh_args, currently it fails if arg starts with -
|
||||||
|
parser.add_argument("ssh_args", nargs="*", default=[])
|
||||||
|
parser.set_defaults(func=main)
|
||||||
@@ -41,7 +41,7 @@ let
|
|||||||
propagatedBuildInputs =
|
propagatedBuildInputs =
|
||||||
dependencies
|
dependencies
|
||||||
++ [ ];
|
++ [ ];
|
||||||
passthru.tests = { inherit clan-tests clan-mypy; };
|
passthru.tests = { inherit clan-black clan-mypy clan-pytest clan-ruff; };
|
||||||
passthru.devDependencies = devDependencies;
|
passthru.devDependencies = devDependencies;
|
||||||
postInstall = ''
|
postInstall = ''
|
||||||
installShellCompletion --bash --name clan \
|
installShellCompletion --bash --name clan \
|
||||||
@@ -49,10 +49,19 @@ let
|
|||||||
installShellCompletion --fish --name clan.fish \
|
installShellCompletion --fish --name clan.fish \
|
||||||
<(${python3.pkgs.argcomplete}/bin/register-python-argcomplete --shell fish clan)
|
<(${python3.pkgs.argcomplete}/bin/register-python-argcomplete --shell fish clan)
|
||||||
'';
|
'';
|
||||||
|
meta.mainProgram = "clan";
|
||||||
};
|
};
|
||||||
|
|
||||||
checkPython = python3.withPackages (_ps: devDependencies ++ dependencies);
|
checkPython = python3.withPackages (_ps: devDependencies ++ dependencies);
|
||||||
|
|
||||||
|
clan-black = runCommand "${name}-black" { } ''
|
||||||
|
cp -r ${src} ./src
|
||||||
|
chmod +w -R ./src
|
||||||
|
cd src
|
||||||
|
${checkPython}/bin/black --check .
|
||||||
|
touch $out
|
||||||
|
'';
|
||||||
|
|
||||||
clan-mypy = runCommand "${name}-mypy" { } ''
|
clan-mypy = runCommand "${name}-mypy" { } ''
|
||||||
cp -r ${src} ./src
|
cp -r ${src} ./src
|
||||||
chmod +w -R ./src
|
chmod +w -R ./src
|
||||||
@@ -61,11 +70,20 @@ let
|
|||||||
touch $out
|
touch $out
|
||||||
'';
|
'';
|
||||||
|
|
||||||
clan-tests = runCommand "${name}-tests" { } ''
|
clan-pytest = runCommand "${name}-tests" { } ''
|
||||||
cp -r ${src} ./src
|
cp -r ${src} ./src
|
||||||
chmod +w -R ./src
|
chmod +w -R ./src
|
||||||
cd src
|
cd src
|
||||||
${checkPython}/bin/python -m pytest ./tests
|
${checkPython}/bin/python -m pytest ./tests \
|
||||||
|
|| echo -e "generate coverage report py running:\n pytest; firefox .reports/html/index.html"
|
||||||
|
touch $out
|
||||||
|
'';
|
||||||
|
|
||||||
|
clan-ruff = runCommand "${name}-ruff" { } ''
|
||||||
|
cp -r ${src} ./src
|
||||||
|
chmod +w -R ./src
|
||||||
|
cd src
|
||||||
|
${pkgs.ruff}/bin/ruff check .
|
||||||
touch $out
|
touch $out
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
in
|
in
|
||||||
{
|
{
|
||||||
packages.${name} = package;
|
packages.${name} = package;
|
||||||
|
packages.default = package;
|
||||||
checks = package.tests;
|
checks = package.tests;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ dynamic = ["version"]
|
|||||||
scripts = {clan = "clan_cli:main"}
|
scripts = {clan = "clan_cli:main"}
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
addopts = "--cov . --cov-report term --cov-fail-under=100 --no-cov-on-fail"
|
addopts = "--cov . --cov-report term --cov-report html:.reports/html --cov-fail-under=100 --no-cov-on-fail"
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
python_version = "3.10"
|
python_version = "3.10"
|
||||||
|
|||||||
66
pkgs/clan-cli/tests/test_clan_ssh.py
Normal file
66
pkgs/clan-cli/tests/test_clan_ssh.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import sys
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import pytest_subprocess.fake_process
|
||||||
|
from pytest_subprocess import utils
|
||||||
|
|
||||||
|
import clan_cli.ssh
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_args(
|
||||||
|
capsys: pytest.CaptureFixture, monkeypatch: pytest.MonkeyPatch
|
||||||
|
) -> None:
|
||||||
|
monkeypatch.setattr(sys, "argv", ["", "ssh"])
|
||||||
|
with pytest.raises(SystemExit):
|
||||||
|
clan_cli.main()
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
assert captured.err.startswith("usage:")
|
||||||
|
|
||||||
|
|
||||||
|
# using fp fixture from pytest-subprocess
|
||||||
|
def test_ssh_no_pass(fp: pytest_subprocess.fake_process.FakeProcess) -> None:
|
||||||
|
host = "somehost"
|
||||||
|
user = "user"
|
||||||
|
cmd: list[Union[str, utils.Any]] = [
|
||||||
|
"torify",
|
||||||
|
"ssh",
|
||||||
|
"-o",
|
||||||
|
"UserKnownHostsFile=/dev/null",
|
||||||
|
"-o",
|
||||||
|
"StrictHostKeyChecking=no",
|
||||||
|
f"{user}@{host}",
|
||||||
|
fp.any(),
|
||||||
|
]
|
||||||
|
fp.register(cmd)
|
||||||
|
clan_cli.ssh.ssh(
|
||||||
|
host=host,
|
||||||
|
user=user,
|
||||||
|
)
|
||||||
|
assert fp.call_count(cmd) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_ssh_with_pass(fp: pytest_subprocess.fake_process.FakeProcess) -> None:
|
||||||
|
host = "somehost"
|
||||||
|
user = "user"
|
||||||
|
cmd: list[Union[str, utils.Any]] = [
|
||||||
|
"nix",
|
||||||
|
"shell",
|
||||||
|
"nixpkgs#sshpass",
|
||||||
|
"-c",
|
||||||
|
fp.any(),
|
||||||
|
]
|
||||||
|
fp.register(cmd)
|
||||||
|
clan_cli.ssh.ssh(
|
||||||
|
host=host,
|
||||||
|
user=user,
|
||||||
|
password="XXX",
|
||||||
|
)
|
||||||
|
assert fp.call_count(cmd) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_qrcode_scan(fp: pytest_subprocess.fake_process.FakeProcess) -> None:
|
||||||
|
cmd: list[Union[str, utils.Any]] = [fp.any()]
|
||||||
|
fp.register(cmd, stdout="https://test.test")
|
||||||
|
result = clan_cli.ssh.qrcode_scan("test.png")
|
||||||
|
assert result == "https://test.test"
|
||||||
Reference in New Issue
Block a user