Skip to content

feat: kyber Tailscale exit node with vpn fish shorthand - #1432

Merged
shunkakinoki merged 3 commits into
mainfrom
feat/kyber-exit-node
Apr 12, 2026
Merged

feat: kyber Tailscale exit node with vpn fish shorthand#1432
shunkakinoki merged 3 commits into
mainfrom
feat/kyber-exit-node

Conversation

@shunkakinoki

@shunkakinoki shunkakinoki commented Apr 11, 2026

Copy link
Copy Markdown
Owner

Summary

  • Advertise kyber as Tailscale exit node (--advertise-exit-node)
  • Enable IP forwarding on kyber via home-manager activation
  • Add vpn fish function (vpn on / vpn off / vpn status)
  • Add fish tests for vpn function

Usage

vpn on      # route all traffic through kyber
vpn off     # disconnect
vpn status  # show tailscale status

Summary by cubic

Turned kyber into a Tailscale exit node and added a vpn fish command (vpn on|off|status) to route all traffic through kyber. Also enables IP forwarding on kyber, registers the function for autoloading, and adds basic tests.

  • New Features
    • Kyber: advertise as exit node (--advertise-exit-node) and enable IPv4/IPv6 forwarding via home-manager activation; persists settings in /etc/sysctl.d/99-tailscale.conf.
    • Fish: added _vpn_function.fish wired as vpn; supports on, off, status, and toggle; targets kyber 100.74.174.97; registered in functions mapping/list for reliable autoloading.
    • Tests: added fish specs to verify the function is loaded and vpn status calls tailscale status.

Written for commit 6672f24. Summary will update on new commits.

Copilot AI review requested due to automatic review settings April 11, 2026 15:38
@mesa-dot-dev

mesa-dot-dev Bot commented Apr 11, 2026

Copy link
Copy Markdown

You do not have enough credits to review this pull request. Please purchase more credits to continue.

@coderabbitai

coderabbitai Bot commented Apr 11, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 98775f8a-7a2d-4b2c-9314-0f1b16eb9973

📥 Commits

Reviewing files that changed from the base of the PR and between 3b46529 and 6672f24.

📒 Files selected for processing (1)
  • home-manager/programs/fish/default.nix

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.


📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added a VPN shell command with on/off/status and toggle modes to manage exit-node connectivity.
  • Chores

    • Enabled IPv4/IPv6 forwarding on the kyber host.
    • Configured the host to advertise itself as an exit node.
  • Tests

    • Added tests validating the VPN command is defined and invokes expected status behavior.

Walkthrough

Adds a fish shell vpn abbreviation backed by a new _vpn_function to control Tailscale exit-node (toggle/on/off/status), and configures the kyber host to advertise itself as a Tailscale exit node and enable IPv4/IPv6 forwarding.

Changes

Cohort / File(s) Summary
Fish VPN function & abbreviation
home-manager/programs/fish/default.nix, home-manager/programs/fish/functions/_vpn_function.fish
Adds vpn = "_vpn_function" abbreviation and deploys fish/functions/_vpn_function.fish. _vpn_function supports on, off, status, and toggle (no-arg) modes using tailscale CLI and checks tailscale status --json.
Exit-node host configuration
named-hosts/kyber/default.nix
Enables IPv4/IPv6 forwarding via sysctl and persists settings to /etc/sysctl.d/99-tailscale.conf (when enabled). Appends --advertise-exit-node to Tailscale startup args to advertise kyber as an exit node.
Test spec
spec/fish/_vpn_function_test.fish
Adds test that sources the function, stubs sudo and tailscale, verifies function is defined and that status invokes tailscale status as expected.

Sequence Diagram

sequenceDiagram
    participant User
    participant FishShell as Fish Shell
    participant VPNFunc as _vpn_function
    participant Tailscale as Tailscale CLI
    participant Host as kyber Host

    User->>FishShell: run `vpn` / `vpn on|off|status`
    FishShell->>VPNFunc: invoke _vpn_function

    alt Toggle (no arg)
        VPNFunc->>Tailscale: tailscale status --json 2>/dev/null
        Tailscale-->>VPNFunc: JSON output
        alt ExitNodeStatus present
            VPNFunc->>Tailscale: sudo tailscale set --exit-node=
            Tailscale-->>VPNFunc: cleared
            VPNFunc-->>User: "VPN disconnected"
        else ExitNodeStatus absent
            VPNFunc->>Tailscale: sudo tailscale set --exit-node=100.74.174.97
            Tailscale-->>VPNFunc: set to kyber
            VPNFunc-->>User: "VPN connected through kyber"
        end
    else On
        VPNFunc->>Tailscale: sudo tailscale set --exit-node=100.74.174.97
        Tailscale-->>User: success
    else Off
        VPNFunc->>Tailscale: sudo tailscale set --exit-node=
        Tailscale-->>User: success
    else Status
        VPNFunc->>Tailscale: tailscale status
        Tailscale-->>User: display status
    end

    Note right of Host: kyber sysctl changes + --advertise-exit-node ensure Host can act as exit node
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 A hop, a twitch, a network cheer,
I toggle tunnels far and near,
Kyber forwards, tailscale sings,
Fish whispers commands on springy wings,
VPN set — hop, I'm in gear!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: kyber Tailscale exit node with vpn fish shorthand' accurately summarizes the main changes: setting up kyber as a Tailscale exit node and adding a vpn fish command shorthand.
Description check ✅ Passed The description is directly related to the changeset, providing a clear summary of the changes, usage examples, and detailed breakdown of new features across all modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/kyber-exit-node

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@mesa-dot-dev

mesa-dot-dev Bot commented Apr 11, 2026

Copy link
Copy Markdown

Mesa Description

TL;DR

Configured Kyber as a Tailscale exit node and introduced a vpn fish shell function for easy management of the VPN connection.

What changed?

  • home-manager/programs/fish/default.nix: Added a new vpn alias and its corresponding _vpn_function to the Fish shell configuration.
  • home-manager/programs/fish/functions/_vpn_function.fish: Introduced a fish shell function _vpn_function for managing Tailscale exit node connections to 'kyber' (100.74.174.97), supporting on, off, status, and toggle commands.
  • named-hosts/kyber/default.nix: Updated to enable and configure Kyber as a Tailscale exit node by adding an activation script for IPv4/IPv6 forwarding and modifying Tailscale configuration to advertise the exit node.
  • spec/fish/_vpn_function_test.fish: Added a new test file to verify the _vpn_function is defined correctly and that _vpn_function status calls tailscale status using mock functions.

Description generated by Mesa. Update settings

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a vpn function for the Fish shell to manage Tailscale exit nodes and configures the kyber host to act as an exit node by enabling IP forwarding. Feedback focuses on fixing the toggle logic in the Fish function, which currently fails to correctly parse Tailscale's JSON output, adding $DRY_RUN_CMD support to the Nix activation script for safer dry runs, and replacing a hardcoded IP address with a dynamic lookup.

Comment on lines +13 to +20
set -l current (tailscale status --json 2>/dev/null | string match -rg '"ExitNodeStatus"')
if test -n "$current"
sudo tailscale set --exit-node=
echo "VPN disconnected"
else
sudo tailscale set --exit-node=$kyber_ip
echo "VPN connected through kyber"
end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The toggle logic is likely broken. string match -rg '"ExitNodeStatus"' matches the key name in the JSON output even if its value is null (which Tailscale returns when no exit node is active). Consequently, test -n "$current" will always be true, and the function will always attempt to disconnect rather than toggle.

A more robust check is to look for the opening brace of the object value, indicating an active exit node configuration.

      if tailscale status --json 2>/dev/null | string match -q '*"ExitNodeStatus": {*'
        sudo tailscale set --exit-node=
        echo "VPN disconnected"
      else
        sudo tailscale set --exit-node=$kyber_ip
        echo "VPN connected through kyber"
      end

Comment on lines +150 to +160
home.activation.enableIpForwarding = config.lib.dag.entryAfter [ "writeBoundary" ] ''
if [ "$(cat /proc/sys/net/ipv4/ip_forward)" != "1" ]; then
echo "Enabling IP forwarding for Tailscale exit node..."
sudo sysctl -w net.ipv4.ip_forward=1
sudo sysctl -w net.ipv6.conf.all.forwarding=1
fi
if [ ! -f /etc/sysctl.d/99-tailscale.conf ]; then
echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding=1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
fi
'';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This activation script is missing $DRY_RUN_CMD, which is standard for home-manager activation scripts in this repository (see lines 68, 74). Without it, a dry-run (e.g., home-manager build) will still attempt to execute sudo commands.

Also, the check for /etc/sysctl.d/99-tailscale.conf only verifies existence. It's safer to use $DRY_RUN_CMD with sudo and ensure the content is correctly applied.

        home.activation.enableIpForwarding = config.lib.dag.entryAfter [ "writeBoundary" ] ''
          if [ "$(cat /proc/sys/net/ipv4/ip_forward)" != "1" ]; then
            echo "Enabling IP forwarding for Tailscale exit node..."
            $DRY_RUN_CMD sudo sysctl -w net.ipv4.ip_forward=1
            $DRY_RUN_CMD sudo sysctl -w net.ipv6.conf.all.forwarding=1
          fi
          if [ ! -f /etc/sysctl.d/99-tailscale.conf ]; then
            echo 'net.ipv4.ip_forward=1' | $DRY_RUN_CMD sudo tee /etc/sysctl.d/99-tailscale.conf > /dev/null
            echo 'net.ipv6.conf.all.forwarding=1' | $DRY_RUN_CMD sudo tee -a /etc/sysctl.d/99-tailscale.conf > /dev/null
          fi
        '';

@@ -0,0 +1,25 @@
function _vpn_function --description "Connect/disconnect Tailscale exit node through kyber"
set -l kyber_ip 100.74.174.97

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The IP address for kyber is hardcoded. Consider retrieving it dynamically (e.g., tailscale ip -4 kyber) or making it a configurable variable to improve maintainability.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a Tailscale exit-node setup for the kyber host and introduces a fish shorthand (vpn) to control using kyber as an exit node from interactive fish shells.

Changes:

  • Advertises kyber as a Tailscale exit node and adds a Home Manager activation hook to enable IP forwarding.
  • Adds _vpn_function fish function and wires vpn as a fish abbreviation.
  • Adds fishtape tests for the new fish function.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
named-hosts/kyber/default.nix Enables IP forwarding via activation and adds --advertise-exit-node to Tailscale up args.
home-manager/programs/fish/functions/_vpn_function.fish Implements vpn on/off/status (and a toggle default) for Tailscale exit-node usage.
home-manager/programs/fish/default.nix Registers vpn abbreviation and ships _vpn_function into fish functions.
spec/fish/_vpn_function_test.fish Adds initial fishtape tests for the new fish function.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +151 to +158
if [ "$(cat /proc/sys/net/ipv4/ip_forward)" != "1" ]; then
echo "Enabling IP forwarding for Tailscale exit node..."
sudo sysctl -w net.ipv4.ip_forward=1
sudo sysctl -w net.ipv6.conf.all.forwarding=1
fi
if [ ! -f /etc/sysctl.d/99-tailscale.conf ]; then
echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding=1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Activation script runs privileged commands (sysctl/tee) without using $DRY_RUN_CMD or the repo’s existing root-command helper pattern (see modules/tailscale install hook). This means home-manager switch --dry-run could still modify sysctl settings, and activation will hard-fail/prompt if sudo isn’t available or is interactive. Prefer wrapping all mutating commands with $DRY_RUN_CMD and using a resolved root helper (sudo/doas/root) like the tailscale module does, or move sysctl config to a system-level module instead of home activation.

Suggested change
if [ "$(cat /proc/sys/net/ipv4/ip_forward)" != "1" ]; then
echo "Enabling IP forwarding for Tailscale exit node..."
sudo sysctl -w net.ipv4.ip_forward=1
sudo sysctl -w net.ipv6.conf.all.forwarding=1
fi
if [ ! -f /etc/sysctl.d/99-tailscale.conf ]; then
echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding=1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
if [ "$(id -u)" -eq 0 ]; then
root_cmd=""
elif command -v sudo >/dev/null 2>&1; then
root_cmd="sudo"
elif command -v doas >/dev/null 2>&1; then
root_cmd="doas"
else
echo "Unable to enable IP forwarding: need root privileges but neither sudo nor doas is available." >&2
exit 1
fi
run_root() {
if [ -n "$root_cmd" ]; then
$DRY_RUN_CMD "$root_cmd" "$@"
else
$DRY_RUN_CMD "$@"
fi
}
if [ "$(cat /proc/sys/net/ipv4/ip_forward)" != "1" ]; then
echo "Enabling IP forwarding for Tailscale exit node..."
run_root sysctl -w net.ipv4.ip_forward=1
run_root sysctl -w net.ipv6.conf.all.forwarding=1
fi
if [ ! -f /etc/sysctl.d/99-tailscale.conf ]; then
printf '%s\n' 'net.ipv4.ip_forward=1' | run_root tee /etc/sysctl.d/99-tailscale.conf >/dev/null
printf '%s\n' 'net.ipv6.conf.all.forwarding=1' | run_root tee -a /etc/sysctl.d/99-tailscale.conf >/dev/null

Copilot uses AI. Check for mistakes.
Comment on lines 170 to 174
extraUpArgs = [
"--reset"
"--accept-dns=false"
"--advertise-exit-node"
];

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--advertise-exit-node is being added via extraUpArgs even though the tailscale module already exposes a first-class modules.tailscale.advertiseExitNode option. Using the option is clearer, avoids accidental duplication if args/options are later combined, and keeps intent declarative.

Copilot uses AI. Check for mistakes.
Comment on lines +12 to +20
# Toggle: if exit node is set, turn off; otherwise turn on
set -l current (tailscale status --json 2>/dev/null | string match -rg '"ExitNodeStatus"')
if test -n "$current"
sudo tailscale set --exit-node=
echo "VPN disconnected"
else
sudo tailscale set --exit-node=$kyber_ip
echo "VPN connected through kyber"
end

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Toggle mode (case '') attempts to detect whether an exit node is set by regex-matching the literal JSON key name "ExitNodeStatus". This doesn’t actually inspect a value (e.g., null vs object, or which exit node is selected), so the toggle decision is fragile and may be incorrect depending on the JSON shape. Parse a specific field/value (and ideally confirm it matches the intended kyber exit node) before deciding on/off.

Suggested change
# Toggle: if exit node is set, turn off; otherwise turn on
set -l current (tailscale status --json 2>/dev/null | string match -rg '"ExitNodeStatus"')
if test -n "$current"
sudo tailscale set --exit-node=
echo "VPN disconnected"
else
sudo tailscale set --exit-node=$kyber_ip
echo "VPN connected through kyber"
end
# Toggle: turn off only if the current exit node is kyber; otherwise turn on.
set -l status_json (tailscale status --json 2>/dev/null)
if test $status -ne 0
echo "Unable to determine Tailscale status"
return 1
end
echo "$status_json" | python3 -c '
import json
import sys
kyber_ip = sys.argv[1]
try:
data = json.load(sys.stdin)
except Exception:
sys.exit(2)
exit_node_status = data.get("ExitNodeStatus")
tailscale_ips = []
if isinstance(exit_node_status, dict):
ips = exit_node_status.get("TailscaleIPs")
if isinstance(ips, list):
tailscale_ips = [str(ip) for ip in ips]
sys.exit(0 if kyber_ip in tailscale_ips else 1)
' "$kyber_ip"
switch $status
case 0
sudo tailscale set --exit-node=
echo "VPN disconnected"
case 1
sudo tailscale set --exit-node=$kyber_ip
echo "VPN connected through kyber"
case '*'
echo "Unable to parse Tailscale status"
return 1
end

Copilot uses AI. Check for mistakes.
function tailscale; echo "tailscale $argv"; end

@test "function is defined after sourcing" (functions -q _vpn_function; echo $status) = 0
@test "vpn status calls tailscale status" (_vpn_function status) = "tailscale status"

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new fish tests only cover sourcing and the status branch. Since _vpn_function introduces side-effecting behavior (on, off, and toggle default case) plus an error path, add tests that stub sudo/tailscale and assert the expected commands/output for each branch to prevent regressions.

Suggested change
@test "vpn status calls tailscale status" (_vpn_function status) = "tailscale status"
@test "vpn status calls tailscale status" (_vpn_function status) = "tailscale status"
@test "vpn on calls sudo tailscale up" (_vpn_function on) = "sudo tailscale up"
@test "vpn off calls sudo tailscale down" (_vpn_function off) = "sudo tailscale down"
@test "vpn with no args uses toggle/default branch" (_vpn_function) = "tailscale status"
@test "vpn invalid arg returns usage/error output" (_vpn_function invalid) = "usage: vpn [on|off|status]"

Copilot uses AI. Check for mistakes.
Comment on lines +151 to +155
if [ "$(cat /proc/sys/net/ipv4/ip_forward)" != "1" ]; then
echo "Enabling IP forwarding for Tailscale exit node..."
sudo sysctl -w net.ipv4.ip_forward=1
sudo sysctl -w net.ipv6.conf.all.forwarding=1
fi

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The guard only checks /proc/sys/net/ipv4/ip_forward before setting both IPv4 and IPv6 forwarding. If IPv4 forwarding is already enabled but IPv6 forwarding is still disabled, this block won’t run and IPv6 forwarding won’t be enabled as intended. Consider checking both sysctls (or unconditionally applying both) so the IPv6 setting isn’t skipped.

Copilot uses AI. Check for mistakes.
Comment on lines +152 to +159
echo "Enabling IP forwarding for Tailscale exit node..."
sudo sysctl -w net.ipv4.ip_forward=1
sudo sysctl -w net.ipv6.conf.all.forwarding=1
fi
if [ ! -f /etc/sysctl.d/99-tailscale.conf ]; then
echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding=1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
fi

Copilot AI Apr 11, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Persistence step only runs when /etc/sysctl.d/99-tailscale.conf does not exist. If the file exists but is missing one of the forwarding keys (or has them set to 0), activation won’t correct it. Consider ensuring the desired contents (e.g., write/replace after a compare) rather than a simple existence check.

Suggested change
echo "Enabling IP forwarding for Tailscale exit node..."
sudo sysctl -w net.ipv4.ip_forward=1
sudo sysctl -w net.ipv6.conf.all.forwarding=1
fi
if [ ! -f /etc/sysctl.d/99-tailscale.conf ]; then
echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding=1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
fi
echo "Enabling IPv4 forwarding for Tailscale exit node..."
sudo sysctl -w net.ipv4.ip_forward=1
fi
if [ "$(cat /proc/sys/net/ipv6/conf/all/forwarding)" != "1" ]; then
echo "Enabling IPv6 forwarding for Tailscale exit node..."
sudo sysctl -w net.ipv6.conf.all.forwarding=1
fi
tmp_sysctl_file="$(mktemp)"
cat > "$tmp_sysctl_file" <<'EOF'
net.ipv4.ip_forward=1
net.ipv6.conf.all.forwarding=1
EOF
if [ ! -f /etc/sysctl.d/99-tailscale.conf ] || ! cmp -s "$tmp_sysctl_file" /etc/sysctl.d/99-tailscale.conf; then
echo "Updating persistent IP forwarding configuration for Tailscale exit node..."
sudo install -Dm644 "$tmp_sysctl_file" /etc/sysctl.d/99-tailscale.conf
fi
rm -f "$tmp_sysctl_file"

Copilot uses AI. Check for mistakes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
home-manager/programs/fish/functions/_vpn_function.fish (1)

1-2: Avoid hardcoding kyber's Tailscale IP in a second place.

100.74.174.97 is already duplicated in home-manager/programs/ssh/default.nix. If kyber is rekeyed or rebuilt, the SSH config and VPN helper can drift. Please lift this into one shared source of truth.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@home-manager/programs/fish/functions/_vpn_function.fish` around lines 1 - 2,
Avoid duplicating the hardcoded IP in _vpn_function: remove the literal
100.74.174.97 assignment to the local kyber_ip and instead read the value from
the single shared source of truth (e.g., an environment variable or
fish_user_variable that your home-manager/Nix config sets). Update _vpn_function
to use that external variable (referencing kyber_ip as read from
$KYBER_TAILSCALE_IP or from fish_user_variables) so the VPN helper and SSH
config both derive the kyber Tailscale IP from the same central configuration.
spec/fish/_vpn_function_test.fish (1)

7-8: Cover the public vpn entrypoint too.

These specs still pass if the vpn wiring in home-manager/programs/fish/default.nix is removed or renamed, because they source the private helper directly. Please add an assertion for the exported vpn command path as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/fish/_vpn_function_test.fish` around lines 7 - 8, The tests currently
only source the private helper _vpn_function so they pass even if the public vpn
entrypoint is missing; update spec/fish/_vpn_function_test.fish to also assert
the exported command exists and forwards correctly by adding an assertion that
the public function is defined (invoke `functions -q vpn` and check exit status
is 0) and a test that calling the public entrypoint (e.g., `(vpn status)`)
returns the same output as the helper (e.g., `"tailscale status"`), referencing
the existing `_vpn_function` and `vpn` names.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@home-manager/programs/fish/default.nix`:
- Line 162: The current assignment sets vpn to an abbreviation (_vpn_function)
which isn't callable from scripts; change this to expose a real function by
either renaming the existing _vpn_function to vpn (so the function symbol is
actually "vpn") or adding a thin wrapper function file (e.g., vpn.fish) that
calls _vpn_function, and update the declaration so the exported name is "vpn"
instead of "_vpn_function"; ensure the function symbol vpn is defined in the
fish functions list so commands like `fish -c 'vpn status'` succeed.

In `@named-hosts/kyber/default.nix`:
- Around line 150-159: The activation script in
home.activation.enableIpForwarding only guards on IPv4 (the cat
/proc/sys/net/ipv4/ip_forward check) which leaves IPv6 forwarding unaddressed
and the /etc/sysctl.d/99-tailscale.conf file unchanged if it exists but is
stale; update the shell snippet inside home.activation.enableIpForwarding to
test and set both net.ipv4.ip_forward and net.ipv6.conf.all.forwarding
independently (e.g., check each /proc/sys value and call sysctl -w for each when
needed) and ensure the persistent file /etc/sysctl.d/99-tailscale.conf is
created or repaired idempotently (replace or merge the two settings into the
file rather than only appending when the file is missing) so enabling forwarding
is correct for both IPv4 and IPv6 and repeated runs are safe.

---

Nitpick comments:
In `@home-manager/programs/fish/functions/_vpn_function.fish`:
- Around line 1-2: Avoid duplicating the hardcoded IP in _vpn_function: remove
the literal 100.74.174.97 assignment to the local kyber_ip and instead read the
value from the single shared source of truth (e.g., an environment variable or
fish_user_variable that your home-manager/Nix config sets). Update _vpn_function
to use that external variable (referencing kyber_ip as read from
$KYBER_TAILSCALE_IP or from fish_user_variables) so the VPN helper and SSH
config both derive the kyber Tailscale IP from the same central configuration.

In `@spec/fish/_vpn_function_test.fish`:
- Around line 7-8: The tests currently only source the private helper
_vpn_function so they pass even if the public vpn entrypoint is missing; update
spec/fish/_vpn_function_test.fish to also assert the exported command exists and
forwards correctly by adding an assertion that the public function is defined
(invoke `functions -q vpn` and check exit status is 0) and a test that calling
the public entrypoint (e.g., `(vpn status)`) returns the same output as the
helper (e.g., `"tailscale status"`), referencing the existing `_vpn_function`
and `vpn` names.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 87e052a6-d831-4dc9-9466-26c4e87d0111

📥 Commits

Reviewing files that changed from the base of the PR and between 5819b8f and 3b46529.

📒 Files selected for processing (4)
  • home-manager/programs/fish/default.nix
  • home-manager/programs/fish/functions/_vpn_function.fish
  • named-hosts/kyber/default.nix
  • spec/fish/_vpn_function_test.fish

two = "_two_function";
tzo = "_tzo_function";
ulb = "_update_local_binaries_function";
vpn = "_vpn_function";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In fish shell, do abbreviations configured via Home Manager programs.fish.shellAbbrs become callable commands/functions, or are they only interactive editor expansions?

💡 Result:

In fish shell, abbreviations configured via Home Manager's programs.fish.shellAbbrs are interactive editor expansions, not callable commands or functions. They are added to ~/.config/fish/config.fish using abbr --add commands, which register them for expansion during interactive input (e.g., typing the abbr and pressing space or enter expands it). They do not create executable commands; running the abbr name directly treats it as an unknown command unless it matches an existing command or function. Fish official documentation confirms: "Only typed-in commands use abbreviations. Abbreviations are not expanded in scripts." Home Manager tests verify abbr --add generation in config.fish, confirming interactive setup.

Citations:


vpn needs to be a real function, not an abbreviation.

The abbreviation at line 162 only expands during interactive input; fish -c 'vpn status' and scripts will fail with "unknown command." Fish abbreviations are not callable commands. Either create a thin vpn.fish wrapper function or rename _vpn_function to vpn to make it actually callable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@home-manager/programs/fish/default.nix` at line 162, The current assignment
sets vpn to an abbreviation (_vpn_function) which isn't callable from scripts;
change this to expose a real function by either renaming the existing
_vpn_function to vpn (so the function symbol is actually "vpn") or adding a thin
wrapper function file (e.g., vpn.fish) that calls _vpn_function, and update the
declaration so the exported name is "vpn" instead of "_vpn_function"; ensure the
function symbol vpn is defined in the fish functions list so commands like `fish
-c 'vpn status'` succeed.

Comment on lines +150 to +159
home.activation.enableIpForwarding = config.lib.dag.entryAfter [ "writeBoundary" ] ''
if [ "$(cat /proc/sys/net/ipv4/ip_forward)" != "1" ]; then
echo "Enabling IP forwarding for Tailscale exit node..."
sudo sysctl -w net.ipv4.ip_forward=1
sudo sysctl -w net.ipv6.conf.all.forwarding=1
fi
if [ ! -f /etc/sysctl.d/99-tailscale.conf ]; then
echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding=1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make the forwarding activation idempotent for IPv6 too.

The guard on Line 151 only checks IPv4. If net.ipv4.ip_forward=1 is already set but net.ipv6.conf.all.forwarding=0, this block skips both writes and the host is still advertised as an exit node with incomplete forwarding. Lines 156-159 also never repair an existing but stale /etc/sysctl.d/99-tailscale.conf.

Suggested fix
 home.activation.enableIpForwarding = config.lib.dag.entryAfter [ "writeBoundary" ] ''
-  if [ "$(cat /proc/sys/net/ipv4/ip_forward)" != "1" ]; then
-    echo "Enabling IP forwarding for Tailscale exit node..."
-    sudo sysctl -w net.ipv4.ip_forward=1
-    sudo sysctl -w net.ipv6.conf.all.forwarding=1
-  fi
-  if [ ! -f /etc/sysctl.d/99-tailscale.conf ]; then
-    echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-tailscale.conf
-    echo 'net.ipv6.conf.all.forwarding=1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
-  fi
+  if [ "$(cat /proc/sys/net/ipv4/ip_forward)" != "1" ]; then
+    sudo sysctl -w net.ipv4.ip_forward=1
+  fi
+  if [ "$(cat /proc/sys/net/ipv6/conf/all/forwarding)" != "1" ]; then
+    sudo sysctl -w net.ipv6.conf.all.forwarding=1
+  fi
+  cat <<'EOF' | sudo tee /etc/sysctl.d/99-tailscale.conf >/dev/null
+net.ipv4.ip_forward=1
+net.ipv6.conf.all.forwarding=1
+EOF
 '';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@named-hosts/kyber/default.nix` around lines 150 - 159, The activation script
in home.activation.enableIpForwarding only guards on IPv4 (the cat
/proc/sys/net/ipv4/ip_forward check) which leaves IPv6 forwarding unaddressed
and the /etc/sysctl.d/99-tailscale.conf file unchanged if it exists but is
stale; update the shell snippet inside home.activation.enableIpForwarding to
test and set both net.ipv4.ip_forward and net.ipv6.conf.all.forwarding
independently (e.g., check each /proc/sys value and call sysctl -w for each when
needed) and ensure the persistent file /etc/sysctl.d/99-tailscale.conf is
created or repaired idempotently (replace or merge the two settings into the
file rather than only appending when the file is missing) so enabling forwarding
is correct for both IPv4 and IPv6 and repeated runs are safe.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 4 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="named-hosts/kyber/default.nix">

<violation number="1" location="named-hosts/kyber/default.nix:151">
P1: IPv6 forwarding may remain disabled because the condition only checks IPv4 state before applying both sysctl updates.</violation>

<violation number="2" location="named-hosts/kyber/default.nix:153">
P2: Missing `$DRY_RUN_CMD` before `sudo` commands. Other activation scripts in this file use `$DRY_RUN_CMD` to prevent privileged commands from running during `home-manager build` / `--dry-run`. Without it, a dry-run will still execute `sudo sysctl` and `sudo tee`.</violation>

<violation number="3" location="named-hosts/kyber/default.nix:156">
P2: The sysctl persistence logic is not self-healing: existing but incomplete/incorrect config files are never corrected.</violation>
</file>

<file name="home-manager/programs/fish/functions/_vpn_function.fish">

<violation number="1" location="home-manager/programs/fish/functions/_vpn_function.fish:13">
P1: Toggle logic is broken: `string match -rg '"ExitNodeStatus"'` matches the JSON key name itself, which is present in the output even when the value is `null` (no exit node active). This means `test -n "$current"` is always true, so the toggle will always disconnect and never connect. Match against the value shape instead, e.g. `string match -q '*"ExitNodeStatus": {*'` to detect an active exit node object.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.


# IP forwarding for Tailscale exit node
home.activation.enableIpForwarding = config.lib.dag.entryAfter [ "writeBoundary" ] ''
if [ "$(cat /proc/sys/net/ipv4/ip_forward)" != "1" ]; then

@cubic-dev-ai cubic-dev-ai Bot Apr 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: IPv6 forwarding may remain disabled because the condition only checks IPv4 state before applying both sysctl updates.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At named-hosts/kyber/default.nix, line 151:

<comment>IPv6 forwarding may remain disabled because the condition only checks IPv4 state before applying both sysctl updates.</comment>

<file context>
@@ -146,6 +146,19 @@ home-manager.lib.homeManagerConfiguration {
 
+        # IP forwarding for Tailscale exit node
+        home.activation.enableIpForwarding = config.lib.dag.entryAfter [ "writeBoundary" ] ''
+          if [ "$(cat /proc/sys/net/ipv4/ip_forward)" != "1" ]; then
+            echo "Enabling IP forwarding for Tailscale exit node..."
+            sudo sysctl -w net.ipv4.ip_forward=1
</file context>
Suggested change
if [ "$(cat /proc/sys/net/ipv4/ip_forward)" != "1" ]; then
if [ "$(cat /proc/sys/net/ipv4/ip_forward)" != "1" ] || [ "$(cat /proc/sys/net/ipv6/conf/all/forwarding)" != "1" ]; then
Fix with Cubic

tailscale status
case ''
# Toggle: if exit node is set, turn off; otherwise turn on
set -l current (tailscale status --json 2>/dev/null | string match -rg '"ExitNodeStatus"')

@cubic-dev-ai cubic-dev-ai Bot Apr 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Toggle logic is broken: string match -rg '"ExitNodeStatus"' matches the JSON key name itself, which is present in the output even when the value is null (no exit node active). This means test -n "$current" is always true, so the toggle will always disconnect and never connect. Match against the value shape instead, e.g. string match -q '*"ExitNodeStatus": {*' to detect an active exit node object.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At home-manager/programs/fish/functions/_vpn_function.fish, line 13:

<comment>Toggle logic is broken: `string match -rg '"ExitNodeStatus"'` matches the JSON key name itself, which is present in the output even when the value is `null` (no exit node active). This means `test -n "$current"` is always true, so the toggle will always disconnect and never connect. Match against the value shape instead, e.g. `string match -q '*"ExitNodeStatus": {*'` to detect an active exit node object.</comment>

<file context>
@@ -0,0 +1,25 @@
+      tailscale status
+    case ''
+      # Toggle: if exit node is set, turn off; otherwise turn on
+      set -l current (tailscale status --json 2>/dev/null | string match -rg '"ExitNodeStatus"')
+      if test -n "$current"
+        sudo tailscale set --exit-node=
</file context>
Fix with Cubic

sudo sysctl -w net.ipv4.ip_forward=1
sudo sysctl -w net.ipv6.conf.all.forwarding=1
fi
if [ ! -f /etc/sysctl.d/99-tailscale.conf ]; then

@cubic-dev-ai cubic-dev-ai Bot Apr 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The sysctl persistence logic is not self-healing: existing but incomplete/incorrect config files are never corrected.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At named-hosts/kyber/default.nix, line 156:

<comment>The sysctl persistence logic is not self-healing: existing but incomplete/incorrect config files are never corrected.</comment>

<file context>
@@ -146,6 +146,19 @@ home-manager.lib.homeManagerConfiguration {
+            sudo sysctl -w net.ipv4.ip_forward=1
+            sudo sysctl -w net.ipv6.conf.all.forwarding=1
+          fi
+          if [ ! -f /etc/sysctl.d/99-tailscale.conf ]; then
+            echo 'net.ipv4.ip_forward=1' | sudo tee /etc/sysctl.d/99-tailscale.conf
+            echo 'net.ipv6.conf.all.forwarding=1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
</file context>
Suggested change
if [ ! -f /etc/sysctl.d/99-tailscale.conf ]; then
if [ ! -f /etc/sysctl.d/99-tailscale.conf ] || ! sudo grep -q '^net.ipv4.ip_forward=1$' /etc/sysctl.d/99-tailscale.conf || ! sudo grep -q '^net.ipv6.conf.all.forwarding=1$' /etc/sysctl.d/99-tailscale.conf; then
Fix with Cubic

home.activation.enableIpForwarding = config.lib.dag.entryAfter [ "writeBoundary" ] ''
if [ "$(cat /proc/sys/net/ipv4/ip_forward)" != "1" ]; then
echo "Enabling IP forwarding for Tailscale exit node..."
sudo sysctl -w net.ipv4.ip_forward=1

@cubic-dev-ai cubic-dev-ai Bot Apr 11, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Missing $DRY_RUN_CMD before sudo commands. Other activation scripts in this file use $DRY_RUN_CMD to prevent privileged commands from running during home-manager build / --dry-run. Without it, a dry-run will still execute sudo sysctl and sudo tee.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At named-hosts/kyber/default.nix, line 153:

<comment>Missing `$DRY_RUN_CMD` before `sudo` commands. Other activation scripts in this file use `$DRY_RUN_CMD` to prevent privileged commands from running during `home-manager build` / `--dry-run`. Without it, a dry-run will still execute `sudo sysctl` and `sudo tee`.</comment>

<file context>
@@ -146,6 +146,19 @@ home-manager.lib.homeManagerConfiguration {
+        home.activation.enableIpForwarding = config.lib.dag.entryAfter [ "writeBoundary" ] ''
+          if [ "$(cat /proc/sys/net/ipv4/ip_forward)" != "1" ]; then
+            echo "Enabling IP forwarding for Tailscale exit node..."
+            sudo sysctl -w net.ipv4.ip_forward=1
+            sudo sysctl -w net.ipv6.conf.all.forwarding=1
+          fi
</file context>
Fix with Cubic

@shunkakinoki
shunkakinoki merged commit 32cca42 into main Apr 12, 2026
24 of 26 checks passed
@shunkakinoki
shunkakinoki deleted the feat/kyber-exit-node branch April 12, 2026 07:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants