Kieran's opinionated (and probably slightly dumb) nix config

feat: add frp for tunnels

dunkirk.sh 47068211 d9544f15

verified
Changed files
+276
machines
atalanta
terebithia
modules
home
apps
nixos
services
secrets
+4
machines/atalanta/default.nix
···
file = ../../secrets/context7.age;
owner = "kierank";
};
+
frp-auth-token = {
+
file = ../../secrets/frp-auth-token.age;
+
owner = "kierank";
+
};
};
environment.variables = {
+5
machines/atalanta/home/default.nix
···
{
inputs,
pkgs,
+
osConfig,
...
}:
{
···
enable = true;
swift = true;
};
+
};
+
frpc = {
+
enable = true;
+
authTokenFile = osConfig.age.secrets.frp-auth-token.path;
};
};
+9
machines/terebithia/default.nix
···
file = ../../secrets/battleship-arena.age;
owner = "battleship-arena";
};
+
frp-auth-token = {
+
file = ../../secrets/frp-auth-token.age;
+
};
};
environment.sessionVariables = {
···
atelier.services.knot-sync = {
enable = true;
secretsFile = config.age.secrets.github-knot-sync.path;
+
};
+
+
atelier.services.frps = {
+
enable = true;
+
domain = "bore.dunkirk.sh";
+
authTokenFile = config.age.secrets.frp-auth-token.path;
};
services.n8n = {
+114
modules/home/apps/frpc.nix
···
+
{
+
config,
+
lib,
+
pkgs,
+
...
+
}:
+
let
+
cfg = config.atelier.frpc;
+
+
frpc-tunnel = pkgs.writeShellScriptBin "frpc-tunnel" ''
+
# Check if gum is available
+
if ! command -v ${pkgs.gum}/bin/gum >/dev/null 2>&1; then
+
echo "Error: gum is required but not installed"
+
exit 1
+
fi
+
+
# Get subdomain
+
if [ -n "$1" ]; then
+
subdomain="$1"
+
else
+
${pkgs.gum}/bin/gum style --foreground 212 --bold "🚇 FRP Tunnel"
+
echo
+
subdomain=$(${pkgs.gum}/bin/gum input --placeholder "Enter subdomain (e.g., myapp)")
+
if [ -z "$subdomain" ]; then
+
${pkgs.gum}/bin/gum style --foreground 196 "❌ Subdomain cannot be empty"
+
exit 1
+
fi
+
fi
+
+
# Validate subdomain
+
if ! echo "$subdomain" | ${pkgs.gnugrep}/bin/grep -qE '^[a-z0-9-]+$'; then
+
${pkgs.gum}/bin/gum style --foreground 196 "❌ Subdomain must contain only lowercase letters, numbers, and hyphens"
+
exit 1
+
fi
+
+
# Get port
+
if [ -n "$2" ]; then
+
port="$2"
+
else
+
port=$(${pkgs.gum}/bin/gum input --placeholder "Enter local port (e.g., 8000)")
+
if [ -z "$port" ]; then
+
${pkgs.gum}/bin/gum style --foreground 196 "❌ Port cannot be empty"
+
exit 1
+
fi
+
fi
+
+
# Validate port
+
if ! echo "$port" | ${pkgs.gnugrep}/bin/grep -qE '^[0-9]+$'; then
+
${pkgs.gum}/bin/gum style --foreground 196 "❌ Port must be a number"
+
exit 1
+
fi
+
+
config_file=$(${pkgs.coreutils}/bin/mktemp)
+
trap "${pkgs.coreutils}/bin/rm -f $config_file" EXIT
+
+
${pkgs.coreutils}/bin/cat > $config_file <<EOF
+
serverAddr = "${cfg.serverAddr}"
+
serverPort = ${toString cfg.serverPort}
+
+
auth.method = "token"
+
auth.tokenSource.type = "file"
+
auth.tokenSource.file.path = "${cfg.authTokenFile}"
+
+
[[proxies]]
+
name = "$subdomain-tunnel"
+
type = "http"
+
localIP = "127.0.0.1"
+
localPort = $port
+
subdomain = "$subdomain"
+
EOF
+
+
echo
+
${pkgs.gum}/bin/gum style --border double --padding "1 2" --border-foreground 212 \
+
"🌐 Tunnel Active" \
+
"" \
+
"Local: $(${pkgs.gum}/bin/gum style --foreground 212 --bold "localhost:$port")" \
+
"Public: $(${pkgs.gum}/bin/gum style --foreground 212 --bold "https://$subdomain.bore.dunkirk.sh")" \
+
"" \
+
"Press $(${pkgs.gum}/bin/gum style --foreground 196 --bold "Ctrl+C") to stop"
+
echo
+
+
exec ${pkgs.frp}/bin/frpc -c $config_file
+
'';
+
in
+
{
+
options.atelier.frpc = {
+
enable = lib.mkEnableOption "frp client for tunneling services";
+
+
serverAddr = lib.mkOption {
+
type = lib.types.str;
+
default = "terebithia.dunkirk.sh";
+
description = "frp server address";
+
};
+
+
serverPort = lib.mkOption {
+
type = lib.types.port;
+
default = 7000;
+
description = "frp server port";
+
};
+
+
authTokenFile = lib.mkOption {
+
type = lib.types.nullOr lib.types.path;
+
default = null;
+
description = "Path to file containing authentication token";
+
};
+
};
+
+
config = lib.mkIf cfg.enable {
+
home.packages = [
+
pkgs.frp
+
frpc-tunnel
+
];
+
};
+
}
+128
modules/nixos/services/frps.nix
···
+
{
+
config,
+
lib,
+
pkgs,
+
...
+
}:
+
let
+
cfg = config.atelier.services.frps;
+
in
+
{
+
options.atelier.services.frps = {
+
enable = lib.mkEnableOption "frp server for tunneling services";
+
+
bindAddr = lib.mkOption {
+
type = lib.types.str;
+
default = "0.0.0.0";
+
description = "Address to bind frp server to";
+
};
+
+
bindPort = lib.mkOption {
+
type = lib.types.port;
+
default = 7000;
+
description = "Port for frp control connection";
+
};
+
+
vhostHTTPPort = lib.mkOption {
+
type = lib.types.port;
+
default = 7080;
+
description = "Port for HTTP virtual host traffic";
+
};
+
+
authToken = lib.mkOption {
+
type = lib.types.nullOr lib.types.str;
+
default = null;
+
description = "Authentication token for clients (deprecated: use authTokenFile)";
+
};
+
+
authTokenFile = lib.mkOption {
+
type = lib.types.nullOr lib.types.path;
+
default = null;
+
description = "Path to file containing authentication token";
+
};
+
+
domain = lib.mkOption {
+
type = lib.types.str;
+
example = "bore.dunkirk.sh";
+
description = "Base domain for subdomains (e.g., *.bore.dunkirk.sh)";
+
};
+
+
enableCaddy = lib.mkOption {
+
type = lib.types.bool;
+
default = true;
+
description = "Automatically configure Caddy reverse proxy for wildcard domain";
+
};
+
};
+
+
config = lib.mkIf cfg.enable {
+
assertions = [
+
{
+
assertion = cfg.authToken != null || cfg.authTokenFile != null;
+
message = "Either authToken or authTokenFile must be set for frps";
+
}
+
];
+
+
# Open firewall port for frp control connection
+
networking.firewall.allowedTCPPorts = [ cfg.bindPort ];
+
+
# frp server service
+
systemd.services.frps =
+
let
+
tokenConfig =
+
if cfg.authTokenFile != null then
+
''
+
auth.tokenSource.type = "file"
+
auth.tokenSource.file.path = "${cfg.authTokenFile}"
+
''
+
else
+
''auth.token = "${cfg.authToken}"'';
+
+
configFile = pkgs.writeText "frps.toml" ''
+
bindAddr = "${cfg.bindAddr}"
+
bindPort = ${toString cfg.bindPort}
+
vhostHTTPPort = ${toString cfg.vhostHTTPPort}
+
+
# Authentication token - clients need this to connect
+
auth.method = "token"
+
${tokenConfig}
+
+
# Subdomain support for *.${cfg.domain}
+
subDomainHost = "${cfg.domain}"
+
+
# Logging
+
log.to = "console"
+
log.level = "info"
+
'';
+
in
+
{
+
description = "frp server for ${cfg.domain} tunneling";
+
after = [ "network.target" ];
+
wantedBy = [ "multi-user.target" ];
+
serviceConfig = {
+
Type = "simple";
+
Restart = "on-failure";
+
RestartSec = "5s";
+
ExecStart = "${pkgs.frp}/bin/frps -c ${configFile}";
+
};
+
};
+
+
# Automatically configure Caddy for wildcard domain
+
services.caddy = lib.mkIf cfg.enableCaddy {
+
virtualHosts."*.${cfg.domain}" = {
+
extraConfig = ''
+
tls {
+
dns cloudflare {env.CLOUDFLARE_API_TOKEN}
+
}
+
header {
+
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
+
}
+
reverse_proxy localhost:${toString cfg.vhostHTTPPort} {
+
header_up X-Forwarded-Proto {scheme}
+
header_up X-Forwarded-For {remote}
+
header_up Host {host}
+
}
+
'';
+
};
+
};
+
};
+
}
+13
secrets/frp-auth-token.age
···
+
age-encryption.org/v1
+
-> ssh-rsa DqcG0Q
+
O9AWztjhnwqJ/hh/jmHu0E+7DTK27ZB7A4eJBowY2hoMrEPabrGBPhZlLoTlSk6x
+
8GTzzdC4cU6jBBwJm3CrrVgxRlIBL23Vz0AWOKfyoFUraYZxDsoifeNAqQ/hgQ1Y
+
BntX55o5z5UhE7M1Dwa57haRnBD/2K2TRQR3BNrFGjHUhiiC4ovavULf3/Ac9trh
+
+umeYLBLK7pPQGhGytrVIIOmGdRq4ZzV8bfUsZyaJiVO5VACshsQWkx0Pj4szKXk
+
/gRerjo2P8yZJ7kg+aRn7cD6WdcpCMQZVbKgtIbe5BE15AzCdxprgQBz4N0Uthox
+
J53hLGvAOgHYdR8CHebymxBNMFdaYBPjBwhHyAlTi5TrPy5S9XtyBEg6h0mHeo+L
+
kSlUacifevkE4qZM5pVVf29YCvAPEC6VlQLAb3m4bppnpg2NBZJQo8iXNP88G53X
+
3axYCAufQttqcUe97yzcHYqXin8UXN8yJXjFNBXPtQ3ScVuSPWO+2V9pyDPRZhk8
+
+
--- 1JfGe2s3sQu5LP07sNvuBwqGUOiOQnrtQXp9+pE4ms8
+
(I ��Pfc�h,X��d�� ��x��O��p��p�F���'`�����u�
+3
secrets/secrets.nix
···
"battleship-arena.age".publicKeys = [
kierank
];
+
"frp-auth-token.age".publicKeys = [
+
kierank
+
];
}