From 448c22c280192d5d3fc0d6b2fe4a41f3152486cc Mon Sep 17 00:00:00 2001 From: lassulus Date: Mon, 18 Aug 2025 11:38:12 +0200 Subject: [PATCH 1/3] clan-cli: use automatic networking for vars upload and machines update This uses the networking module to find the best_host, as we already do with ssh and install. So if we don't supply a --target-host and a networking module is configured, the remote should be autodetected. Since vars upload doesn't have a --target-host argument, we always try to use get_best_remote --- pkgs/clan-cli/clan_cli/machines/update.py | 55 +++++++++++++++++------ pkgs/clan-cli/clan_cli/vars/upload.py | 8 +++- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/pkgs/clan-cli/clan_cli/machines/update.py b/pkgs/clan-cli/clan_cli/machines/update.py index a3674c12c..7a809e6af 100644 --- a/pkgs/clan-cli/clan_cli/machines/update.py +++ b/pkgs/clan-cli/clan_cli/machines/update.py @@ -12,6 +12,7 @@ from clan_lib.machines.list import instantiate_inventory_to_machines from clan_lib.machines.machines import Machine from clan_lib.machines.suggestions import validate_machine_names from clan_lib.machines.update import run_machine_update +from clan_lib.network.network import get_best_remote from clan_lib.nix import nix_config from clan_lib.ssh.host import Host from clan_lib.ssh.host_key import HostKeyCheck @@ -27,6 +28,42 @@ from clan_cli.completions import ( log = logging.getLogger(__name__) +def run_update_with_network( + machine: Machine, + build_host: Remote | LocalHost | None, + upload_inputs: bool, + host_key_check: HostKeyCheck, + target_host_override: str | None = None, +) -> None: + """Run machine update with proper network context handling. + + If target_host_override is provided, use it directly. + Otherwise, use get_best_remote to establish network connection. + """ + if target_host_override: + # Direct connection without network context + target_host = Remote.from_ssh_uri( + machine_name=machine.name, + address=target_host_override, + ).override(host_key_check=host_key_check) + run_machine_update( + machine=machine, + target_host=target_host, + build_host=build_host, + upload_inputs=upload_inputs, + ) + else: + # Use network context + with get_best_remote(machine) as remote: + target_host = remote.override(host_key_check=host_key_check) + run_machine_update( + machine=machine, + target_host=target_host, + build_host=build_host, + upload_inputs=upload_inputs, + ) + + def requires_explicit_update(m: Machine) -> bool: try: if m.select("config.clan.deployment.requireExplicitUpdate"): @@ -144,27 +181,19 @@ def update_command(args: argparse.Namespace) -> None: ).override(host_key_check=host_key_check) else: build_host = machine.build_host() - # Figure out the target host - if args.target_host: - target_host = Remote.from_ssh_uri( - machine_name=machine.name, - address=args.target_host, - ).override(host_key_check=host_key_check) - else: - target_host = machine.target_host().override( - host_key_check=host_key_check - ) - # run the update + + # Schedule the update with network handling runtime.async_run( AsyncOpts( tid=machine.name, async_ctx=AsyncContext(prefix=machine.name), ), - run_machine_update, + run_update_with_network, machine=machine, - target_host=target_host, build_host=build_host, upload_inputs=args.upload_inputs, + host_key_check=host_key_check, + target_host_override=args.target_host, ) runtime.join_all() runtime.check_all() diff --git a/pkgs/clan-cli/clan_cli/vars/upload.py b/pkgs/clan-cli/clan_cli/vars/upload.py index 700b5e385..4d2f62b95 100644 --- a/pkgs/clan-cli/clan_cli/vars/upload.py +++ b/pkgs/clan-cli/clan_cli/vars/upload.py @@ -5,6 +5,7 @@ from pathlib import Path from clan_cli.completions import add_dynamic_completer, complete_machines from clan_lib.flake import require_flake from clan_lib.machines.machines import Machine +from clan_lib.network.network import get_best_remote from clan_lib.ssh.host import Host log = logging.getLogger(__name__) @@ -32,7 +33,12 @@ def upload_command(args: argparse.Namespace) -> None: populate_secret_vars(machine, directory) return - with machine.target_host().host_connection() as host, host.become_root() as host: + # Use get_best_remote to handle networking + with ( + get_best_remote(machine) as remote, + remote.host_connection() as host, + host.become_root() as host, + ): upload_secret_vars(machine, host) From e2eb26345f7f636dc3a9bda72cf6408d613dd8c6 Mon Sep 17 00:00:00 2001 From: lassulus Date: Tue, 19 Aug 2025 23:26:28 +0200 Subject: [PATCH 2/3] networking: add documentation, unhide from CLI --- docs/mkdocs.yml | 2 +- docs/site/guides/networking.md | 184 ++++++++++++++++++++++++++++++++ docs/site/guides/target-host.md | 84 --------------- pkgs/clan-cli/clan_cli/cli.py | 24 ++++- 4 files changed, 204 insertions(+), 90 deletions(-) create mode 100644 docs/site/guides/networking.md delete mode 100644 docs/site/guides/target-host.md diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index fb372c48b..9dc2c9391 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -64,7 +64,7 @@ nav: - Disk Encryption: guides/disk-encryption.md - Age Plugins: guides/age-plugins.md - Secrets management: guides/secrets.md - - Target Host: guides/target-host.md + - Networking: guides/networking.md - Zerotier VPN: guides/mesh-vpn.md - Secure Boot: guides/secure-boot.md - Flake-parts: guides/flake-parts.md diff --git a/docs/site/guides/networking.md b/docs/site/guides/networking.md new file mode 100644 index 000000000..f9c0ae34c --- /dev/null +++ b/docs/site/guides/networking.md @@ -0,0 +1,184 @@ +# Connecting to Your Machines + +Clan provides automatic networking with fallback mechanisms to reliably connect to your machines. + +## Option 1: Automatic Networking with Fallback (Recommended) + +Clan's networking module automatically manages connections through various network technologies with intelligent fallback. When you run `clan ssh` or `clan machines update`, Clan tries each configured network by priority until one succeeds. + +### Basic Setup with Internet Service + +For machines with public IPs or DNS names, use the `internet` service to configure direct SSH while keeping fallback options: + +```{.nix title="flake.nix" hl_lines="7-10 14-16"} +{ + outputs = { self, clan-core, ... }: + let + clan = clan-core.lib.clan { + inventory.instances = { + # Direct SSH with fallback support + internet = { + roles.default.machines.server1 = { + settings.address = "server1.example.com"; + }; + roles.default.machines.server2 = { + settings.address = "192.168.1.100"; + }; + }; + + # Fallback: Secure connections via Tor + tor = { + roles.server.tags.nixos = { }; + }; + }; + }; + in + { + inherit (clan.config) nixosConfigurations; + }; +} +``` + +### Advanced Setup with Multiple Networks + +```{.nix title="flake.nix" hl_lines="7-10 13-16 19-21"} +{ + outputs = { self, clan-core, ... }: + let + clan = clan-core.lib.clan { + inventory.instances = { + # Priority 1: Try direct connection first + internet = { + roles.default.machines.publicserver = { + settings.address = "public.example.com"; + }; + }; + + # Priority 2: VPN for internal machines + zerotier = { + roles.controller.machines."controller" = { }; + roles.peer.tags.nixos = { }; + }; + + # Priority 3: Tor as universal fallback + tor = { + roles.server.tags.nixos = { }; + }; + }; + }; + in + { + inherit (clan.config) nixosConfigurations; + }; +} +``` + +### How It Works + +Clan automatically tries networks in order of priority: +1. Direct internet connections (if configured) +2. VPN networks (ZeroTier, Tailscale, etc.) +3. Tor hidden services +4. Any other configured networks + +If one network fails, Clan automatically tries the next. + +### Useful Commands + +```bash +# View all configured networks and their status +clan network list + +# Test connectivity through all networks +clan network ping machine1 + +# Show complete network topology +clan network overview +``` + +## Option 2: Manual targetHost (Bypasses Fallback!) + +!!! warning + Setting `targetHost` directly **disables all automatic networking and fallback**. Only use this if you need complete control and don't want Clan's intelligent connection management. + +### Using Inventory (For Static Addresses) + +Use inventory-level `targetHost` when the address is **static** and doesn't depend on NixOS configuration: + +```{.nix title="flake.nix" hl_lines="8"} +{ + outputs = { self, clan-core, ... }: + let + clan = clan-core.lib.clan { + inventory.machines.server = { + # WARNING: This bypasses all networking modules! + # Use for: Static IPs, DNS names, known hostnames + deploy.targetHost = "root@192.168.1.100"; + }; + }; + in + { + inherit (clan.config) nixosConfigurations; + }; +} +``` + +**When to use inventory-level:** +- Static IP addresses: `"root@192.168.1.100"` +- DNS names: `"user@server.example.com"` +- Any address that doesn't change based on machine configuration + +### Using NixOS Configuration (For Dynamic Addresses) + +Use machine-level `targetHost` when you need to **interpolate values from the NixOS configuration**: + +```{.nix title="flake.nix" hl_lines="7"} +{ + outputs = { self, clan-core, ... }: + let + clan = clan-core.lib.clan { + machines.server = { config, ... }: { + # WARNING: This also bypasses all networking modules! + # REQUIRED for: Addresses that depend on NixOS config + clan.core.networking.targetHost = "root@${config.networking.hostName}.local"; + }; + }; + in + { + inherit (clan.config) nixosConfigurations; + }; +} +``` + +**When to use machine-level (NixOS config):** +- Using hostName from config: `"root@${config.networking.hostName}.local"` +- Building from multiple config values: `"${config.users.users.deploy.name}@${config.networking.hostName}"` +- Any address that depends on evaluated NixOS configuration + +!!! info "Key Difference" + **Inventory-level** (`deploy.targetHost`) is evaluated immediately and works with static strings. + **Machine-level** (`clan.core.networking.targetHost`) is evaluated after NixOS configuration and can access `config.*` values. + +## Quick Decision Guide + +| Scenario | Recommended Approach | Why | +|----------|---------------------|-----| +| Public servers | `internet` service | Keeps fallback options | +| Mixed infrastructure | Multiple networks | Automatic failover | +| Machines behind NAT | ZeroTier/Tor | NAT traversal with fallback | +| Testing/debugging | Manual targetHost | Full control, no magic | +| Single static machine | Manual targetHost | Simple, no overhead | + +## Command-Line Override + +The `--target-host` flag bypasses ALL networking configuration: + +```bash +# Emergency access - ignores all networking config +clan machines update server --target-host root@backup-ip.com + +# Direct SSH - no fallback attempted +clan ssh laptop --target-host user@10.0.0.5 +``` + +Use this for debugging or emergency access when automatic networking isn't working. diff --git a/docs/site/guides/target-host.md b/docs/site/guides/target-host.md deleted file mode 100644 index d413ac6cb..000000000 --- a/docs/site/guides/target-host.md +++ /dev/null @@ -1,84 +0,0 @@ -# How to Set `targetHost` for a Machine - -The `targetHost` defines where the machine can be reached for operations like SSH or deployment. You can set it in two ways, depending on your use case. - ---- - -## ✅ Option 1: Use the Inventory (Recommended for Static Hosts) - -If the hostname is **static**, like `server.example.com`, set it in the **inventory**: - -```{.nix title="flake.nix" hl_lines="8"} -{ - # edlided - outputs = - { self, clan-core, ... }: - let - # Sometimes this attribute set is defined in clan.nix - clan = clan-core.lib.clan { - inventory.machines.jon = { - deploy.targetHost = "root@server.example.com"; - }; - }; - in - { - inherit (clan.config) nixosConfigurations nixosModules clanInternals; - # elided - }; -} -``` - -This is fast, simple and explicit, and doesn’t require evaluating the NixOS config. We can also displayed it in the clan-cli or clan-app. - ---- - -## ✅ Option 2: Use NixOS (Only for Dynamic Hosts) - -If your target host depends on a **dynamic expression** (like using the machine’s evaluated FQDN), set it inside the NixOS module: - -```{.nix title="flake.nix" hl_lines="8"} -{ - # edlided - outputs = - { self, clan-core, ... }: - let - # Sometimes this attribute set is defined in clan.nix - clan = clan-core.lib.clan { - machines.jon = {config, ...}: { - clan.core.networking.targetHost = "jon@${config.networking.fqdn}"; - }; - }; - in - { - inherit (clan.config) nixosConfigurations nixosModules clanInternals; - # elided - }; -} -``` - -Use this **only if the value cannot be made static**, because it’s slower and won't be displayed in the clan-cli or clan-app yet. - ---- - -## 📝 TL;DR - -| Use Case | Use Inventory? | Example | -| ------------------------- | -------------- | -------------------------------- | -| Static hostname | ✅ Yes | `root@server.example.com` | -| Dynamic config expression | ❌ No | `jon@${config.networking.fqdn}` | - ---- - -## 🚀 Coming Soon: Unified Networking Module - -We’re working on a new networking module that will automatically do all of this for you. - -- Easier to use -- Sane defaults: You’ll always be able to reach the machine — no need to worry about hostnames. -- ✨ Migration from **either method** will be supported and simple. - -## Summary - -- Ask: *Does this hostname dynamically change based on NixOS config?* -- If **no**, use the inventory. -- If **yes**, then use NixOS config. diff --git a/pkgs/clan-cli/clan_cli/cli.py b/pkgs/clan-cli/clan_cli/cli.py index c9990fa25..bfd89b3e5 100644 --- a/pkgs/clan-cli/clan_cli/cli.py +++ b/pkgs/clan-cli/clan_cli/cli.py @@ -442,17 +442,31 @@ Examples: parser_network = subparsers.add_parser( "network", aliases=["net"], - # TODO: Add help="Manage networks" when network code is ready - # help="Manage networks", + help="Manage networks", description="Manage networks", epilog=( """ -show information about configured networks +Manage and monitor network connections for machines. + +Clan supports multiple network technologies (direct SSH, Tor, etc.) that can be +configured with different priorities. When connecting to a machine, Clan will: +1. Check for targetHost in inventory +2. Try configured networks by priority +3. Fall back to targetHost from machine config + +Commands like 'ssh' and 'machines update' automatically use the best +available network connection unless overridden with --target-host. Examples: $ clan network list - Will list networks + List all configured networks and their peers + + $ clan network ping machine1 + Check connectivity to machine1 across all networks + + $ clan network overview + Show complete network status and connectivity """ ), formatter_class=argparse.RawTextHelpFormatter, @@ -495,7 +509,7 @@ For more detailed information, visit: {help_hyperlink("getting-started", "https: register_common_flags(parser) if argcomplete: - argcomplete.autocomplete(parser, exclude=["morph", "network", "net"]) + argcomplete.autocomplete(parser, exclude=["morph"]) return parser From fb094e8f3b3551bdc3f2a8dfd7df991ecb8a07e7 Mon Sep 17 00:00:00 2001 From: lassulus Date: Tue, 19 Aug 2025 23:26:43 +0200 Subject: [PATCH 3/3] add tor network to default template --- templates/clan/default/clan.nix | 7 +++++++ templates/clan/flake-parts/clan.nix | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/templates/clan/default/clan.nix b/templates/clan/default/clan.nix index 9ac930a7e..35b189060 100644 --- a/templates/clan/default/clan.nix +++ b/templates/clan/default/clan.nix @@ -37,6 +37,13 @@ # tags.all means 'all machines' will joined roles.peer.tags.all = { }; }; + + # Docs: https://docs.clan.lol/reference/clanServices/tor/ + # Tor network provides secure, anonymous connections to your machines + # All machines will be accessible via Tor as a fallback connection method + tor = { + roles.server.tags.nixos = { }; + }; }; # Additional NixOS configuration can be added here. diff --git a/templates/clan/flake-parts/clan.nix b/templates/clan/flake-parts/clan.nix index 9ac930a7e..35b189060 100644 --- a/templates/clan/flake-parts/clan.nix +++ b/templates/clan/flake-parts/clan.nix @@ -37,6 +37,13 @@ # tags.all means 'all machines' will joined roles.peer.tags.all = { }; }; + + # Docs: https://docs.clan.lol/reference/clanServices/tor/ + # Tor network provides secure, anonymous connections to your machines + # All machines will be accessible via Tor as a fallback connection method + tor = { + roles.server.tags.nixos = { }; + }; }; # Additional NixOS configuration can be added here.