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:
lassulus
2023-07-25 13:52:22 +00:00
11 changed files with 222 additions and 24 deletions

2
.gitignore vendored
View File

@@ -7,4 +7,6 @@ __pycache__
.mypy_cache .mypy_cache
.pytest_cache .pytest_cache
.pythonenv .pythonenv
.reports
.ruff_cache .ruff_cache
htmlcov

View File

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

View File

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

View File

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

View File

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

View File

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

View 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)

View File

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

View File

@@ -7,6 +7,7 @@
in in
{ {
packages.${name} = package; packages.${name} = package;
packages.default = package;
checks = package.tests; checks = package.tests;
}; };
} }

View File

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

View 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"