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

Compare changes

Choose any two refs to compare.

Changed files
+330 -112
machines
atalanta
john
modules
home
nixos
-1
README.md
···
├── machines
│ ├── atalanta # my macOS M4 machine
│ ├── ember # my dell r210 server (in my basement)
-
│ ├── john # shared server for cedarville
│ ├── moonlark # my framework 13 <dead>
│ ├── nest # shared tilde server through hc
│ ├── prattle # oracle cloud x86_64 server
+28 -28
flake.lock
···
]
},
"locked": {
-
"lastModified": 1764627417,
-
"narHash": "sha256-D6xc3Rl8Ab6wucJWdvjNsGYGSxNjQHzRc2EZ6eeQ6l4=",
+
"lastModified": 1765326679,
+
"narHash": "sha256-fTLX9kDwLr9Y0rH/nG+h1XG5UU+jBcy0PFYn5eneRX8=",
"owner": "nix-community",
"repo": "disko",
-
"rev": "5a88a6eceb8fd732b983e72b732f6f4b8269bef3",
+
"rev": "d64e5cdca35b5fad7c504f615357a7afe6d9c49e",
"type": "github"
},
"original": {
···
]
},
"locked": {
-
"lastModified": 1765170903,
-
"narHash": "sha256-O8VTGey1xxiRW+Fpb+Ps9zU7ShmxUA1a7cMTcENCVNg=",
+
"lastModified": 1765384171,
+
"narHash": "sha256-FuFtkJrW1Z7u+3lhzPRau69E0CNjADku1mLQQflUORo=",
"owner": "nix-community",
"repo": "home-manager",
-
"rev": "20561be440a11ec57a89715480717baf19fe6343",
+
"rev": "44777152652bc9eacf8876976fa72cc77ca8b9d8",
"type": "github"
},
"original": {
···
"nixpkgs": "nixpkgs_3"
},
"locked": {
-
"lastModified": 1765159287,
-
"narHash": "sha256-C+dVEekU31QPaPShMaUbs3LqOVVqzq0b4gKC1jX8Mlk=",
+
"lastModified": 1765418662,
+
"narHash": "sha256-8SSYagIUn+m9CKUYddq3DN1xkh04KCO0itB/LMgEgpc=",
"owner": "nix-community",
"repo": "nix-vscode-extensions",
-
"rev": "dccd0cc3693bff67e4856b5a22445223aabc4d4b",
+
"rev": "0f6679daa3f5bc2b09827b67f49caf0ac8e3a4c8",
"type": "github"
},
"original": {
···
},
"nixos-facter-modules": {
"locked": {
-
"lastModified": 1764252389,
-
"narHash": "sha256-3bbuneTKZBkYXlm0bE36kUjiDsasoIC1GWBw/UEJ9T4=",
+
"lastModified": 1765442039,
+
"narHash": "sha256-k3lYQ+A1F7aTz8HnlU++bd9t/x/NP2A4v9+x6opcVg0=",
"owner": "numtide",
"repo": "nixos-facter-modules",
-
"rev": "5ea68886d95218646d11d3551a476d458df00778",
+
"rev": "9dd775ee92de63f14edd021d59416e18ac2c00f1",
"type": "github"
},
"original": {
···
},
"nixpkgs-unstable": {
"locked": {
-
"lastModified": 1764950072,
-
"narHash": "sha256-BmPWzogsG2GsXZtlT+MTcAWeDK5hkbGRZTeZNW42fwA=",
+
"lastModified": 1765186076,
+
"narHash": "sha256-hM20uyap1a0M9d344I692r+ik4gTMyj60cQWO+hAYP8=",
"owner": "nixos",
"repo": "nixpkgs",
-
"rev": "f61125a668a320878494449750330ca58b78c557",
+
"rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8",
"type": "github"
},
"original": {
···
},
"nixpkgs_4": {
"locked": {
-
"lastModified": 1764983851,
-
"narHash": "sha256-y7RPKl/jJ/KAP/VKLMghMgXTlvNIJMHKskl8/Uuar7o=",
+
"lastModified": 1765311797,
+
"narHash": "sha256-mSD5Ob7a+T2RNjvPvOA1dkJHGVrNVl8ZOrAwBjKBDQo=",
"owner": "nixos",
"repo": "nixpkgs",
-
"rev": "d9bc5c7dceb30d8d6fafa10aeb6aa8a48c218454",
+
"rev": "09eb77e94fa25202af8f3e81ddc7353d9970ac1b",
"type": "github"
},
"original": {
···
},
"locked": {
-
"lastModified": 1765213466,
-
"narHash": "sha256-JdQa7m3a/oWun8TGJ+jamAdxn820RFjqDLNnl4d8a+0=",
+
"lastModified": 1765470296,
+
"narHash": "sha256-bURojPUn8jloR046JNZf6qrYNmEPfFEoDaLTKoP9pg4=",
"owner": "nix-community",
"repo": "NUR",
-
"rev": "0c5cabc4f46e5ce7e45827c22b21173a887acff2",
+
"rev": "441a70568483c0c48b338cca2030e3d9c7aef3ba",
"type": "github"
},
"original": {
···
"sqlite-lib-src": "sqlite-lib-src"
},
"locked": {
-
"lastModified": 1765171220,
-
"narHash": "sha256-K+Cs6k0nQYRwW+RwlKCZabLBOVel84C2wPEZjYOH6JA=",
+
"lastModified": 1765368304,
+
"narHash": "sha256-Q3JC5+FYtsKJU70WIhGhsAYWzu0CvUmmbdYhcFe46Pg=",
"ref": "refs/heads/master",
-
"rev": "ca8217e99806280fa77316b46b0b243647ed491c",
-
"revCount": 1722,
+
"rev": "a53d124ea4746109c1933f7adc72f0bde1309890",
+
"revCount": 1731,
"type": "git",
"url": "https://tangled.org/tangled.org/core"
},
···
"zig2nix": "zig2nix"
},
"locked": {
-
"lastModified": 1765255474,
-
"narHash": "sha256-Bs/wb2EIDe1QHsmHHQ34L0se4eANdGsQpxAsc+gDCrU=",
+
"lastModified": 1765397837,
+
"narHash": "sha256-nMlS9SA8MLJHJ0X/zEg3eG18mLw5vvZpZBbTbVcGFTI=",
"owner": "neurosnap",
"repo": "zmx",
-
"rev": "b5b525c333f086798e319b1a27f2bc0119ebca5c",
+
"rev": "a22dba538a31480ed450b389f397e15880a1c53a",
"type": "github"
},
"original": {
+8 -1
machines/atalanta/home/default.nix
···
zmx = {
enable = true;
-
hosts = [ "t.*" "p.*" "e.*" ];
+
hosts = [ "t.*" "p.*" "e.*" "j.*" ];
};
hosts = {
···
hostname = "192.168.0.94"; # ember
};
+
"j.*" = {
+
hostname = "john.cedarville.edu";
+
user = "klukas";
+
};
+
# Regular hosts
john = {
+
hostname = "john.cedarville.edu";
user = "klukas";
+
zmx = true;
};
bandit = {
-36
machines/john/default.nix
···
-
{
-
inputs,
-
pkgs,
-
...
-
}:
-
{
-
imports = [
-
(inputs.import-tree ../../modules/home)
-
../../modules/home/system/nixpkgs.nix.disabled
-
];
-
-
nixpkgs.enable = true;
-
-
home = {
-
username = "klukas";
-
homeDirectory = "/home/students/2029/klukas";
-
-
packages = with pkgs; [ ];
-
};
-
-
atelier = {
-
shell.enable = true;
-
};
-
-
# Enable home-manager
-
programs.home-manager.enable = true;
-
-
# keep hm in .local/state since we are using nix-portable
-
xdg.enable = true;
-
-
# Nicely reload system units when changing configs
-
systemd.user.startServices = "sd-switch";
-
-
# https://nixos.wiki/wiki/FAQ/When_do_I_update_stateVersion
-
home.stateVersion = "23.05";
-
}
+28 -4
modules/home/apps/bore/bore.1.md
···
# SYNOPSIS
-
**bore** [*SUBDOMAIN*] [*PORT*] [**--label** *LABEL*] [**--save**]
+
**bore** [*SUBDOMAIN*] [*PORT*] [**--protocol** *PROTOCOL*] [**--label** *LABEL*] [**--save**]
**bore** **--list** | **-l**
···
# DESCRIPTION
-
**bore** is a tunneling service that uses frp (fast reverse proxy) to expose local services to the internet via bore.dunkirk.sh. It provides a simple CLI for creating and managing HTTP tunnels with optional labels and persistent configuration.
+
**bore** is a tunneling service that uses frp (fast reverse proxy) to expose local services to the internet via bore.dunkirk.sh. It provides a simple CLI for creating and managing HTTP, TCP, and UDP tunnels with optional labels and persistent configuration.
# OPTIONS
···
**-s**, **--saved**
: List all saved tunnel configurations from bore.toml in the current directory.
+
+
**-p**, **--protocol** *PROTOCOL*
+
: Specify the protocol to use for the tunnel: **http** (default), **tcp**, or **udp**.
**--label** *LABEL*
: Assign a label/tag to the tunnel for organization and identification.
···
[api]
port = 3000
+
protocol = "http"
label = "dev"
+
+
[database]
+
port = 5432
+
protocol = "tcp"
+
label = "postgres"
+
+
[game-server]
+
port = 27015
+
protocol = "udp"
+
label = "game"
```
When running **bore** without arguments in a directory with bore.toml, you'll be prompted to choose between creating a new tunnel or using a saved configuration.
# EXAMPLES
-
Create a simple tunnel:
+
Create a simple HTTP tunnel:
```
$ bore myapp 8000
```
-
Create a tunnel with a label:
+
Create an HTTP tunnel with a label:
```
$ bore api 3000 --label dev
+
```
+
+
Create a TCP tunnel for a database:
+
```
+
$ bore database 5432 --protocol tcp --label postgres
+
```
+
+
Create a UDP tunnel for a game server:
+
```
+
$ bore game-server 27015 --protocol udp --label game
```
Save a tunnel configuration:
+7 -1
modules/home/apps/bore/completions/bore.bash
···
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
-
opts="--list --saved --label --save -l -s"
+
opts="--list --saved --protocol --label --save -l -s -p"
# Complete flags
if [[ ${cur} == -* ]]; then
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
+
return 0
+
fi
+
+
# Complete protocol values after --protocol or -p
+
if [[ ${prev} == "--protocol" ]] || [[ ${prev} == "-p" ]]; then
+
COMPREPLY=( $(compgen -W "http tcp udp" -- ${cur}) )
return 0
fi
+1
modules/home/apps/bore/completions/bore.fish
···
# Complete flags
complete -c bore -s l -l list -d 'List active tunnels'
complete -c bore -s s -l saved -d 'List saved tunnels from bore.toml'
+
complete -c bore -s p -l protocol -d 'Specify protocol' -xa 'http tcp udp'
complete -c bore -l label -d 'Assign a label to the tunnel' -r
complete -c bore -l save -d 'Save tunnel configuration to bore.toml'
+2
modules/home/apps/bore/completions/bore.zsh
···
'-l[List active tunnels]' \
'--saved[List saved tunnels from bore.toml]' \
'-s[List saved tunnels from bore.toml]' \
+
'--protocol[Specify protocol]:protocol:(http tcp udp)' \
+
'-p[Specify protocol]:protocol:(http tcp udp)' \
'--label[Assign a label to the tunnel]:label:' \
'--save[Save tunnel configuration to bore.toml]' \
&& return 0
+174 -39
modules/home/apps/bore/default.nix
···
fi
# Filter only online tunnels with valid conf
-
echo "$tunnels" | ${pkgs.jq}/bin/jq -r '.proxies[] | select(.status == "online" and .conf != null) | "\(.name) → https://\(.conf.subdomain).${cfg.domain}"' | while read -r line; do
+
echo "$tunnels" | ${pkgs.jq}/bin/jq -r '.proxies[] | select(.status == "online" and .conf != null) | if .type == "http" then "\(.name) → https://\(.conf.subdomain).${cfg.domain} [http]" elif .type == "tcp" then "\(.name) → tcp://\(.conf.remotePort) → localhost:\(.conf.localPort) [tcp]" elif .type == "udp" then "\(.name) → udp://\(.conf.remotePort) → localhost:\(.conf.localPort) [udp]" else "\(.name) [\(.type)]" end' | while read -r line; do
${pkgs.gum}/bin/gum style --foreground 35 "✓ $line"
done
exit 0
···
current_tunnel="''${BASH_REMATCH[1]}"
elif [[ "$line" =~ ^port[[:space:]]*=[[:space:]]*([0-9]+) ]]; then
port="''${BASH_REMATCH[1]}"
+
elif [[ "$line" =~ ^protocol[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
+
protocol="''${BASH_REMATCH[1]}"
elif [[ "$line" =~ ^label[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
label="''${BASH_REMATCH[1]}"
-
${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port [$label]"
+
proto_display="''${protocol:-http}"
+
${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port [$proto_display] [$label]"
label=""
+
protocol=""
elif [[ -z "$line" ]] && [[ -n "$current_tunnel" ]] && [[ -n "$port" ]]; then
-
${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port"
+
proto_display="''${protocol:-http}"
+
${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port [$proto_display]"
current_tunnel=""
port=""
+
protocol=""
fi
done < "$CONFIG_FILE"
# Handle last entry if file doesn't end with blank line
if [[ -n "$current_tunnel" ]] && [[ -n "$port" ]]; then
+
proto_display="''${protocol:-http}"
if [[ -n "$label" ]]; then
-
${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port [$label]"
+
${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port [$proto_display] [$label]"
else
-
${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port"
+
${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port [$proto_display]"
fi
fi
exit 0
fi
-
# Get subdomain
+
# Get tunnel name/subdomain
if [ -n "$1" ]; then
-
subdomain="$1"
+
tunnel_name="$1"
else
# Check if we have a bore.toml in current directory
if [ -f "$CONFIG_FILE" ]; then
···
if [ "$choice" = "Use saved tunnel" ]; then
# Extract tunnel names from TOML
saved_names=$(${pkgs.gnugrep}/bin/grep '^\[' "$CONFIG_FILE" | ${pkgs.gnused}/bin/sed 's/^\[\(.*\)\]$/\1/')
-
subdomain=$(echo "$saved_names" | ${pkgs.gum}/bin/gum choose)
+
tunnel_name=$(echo "$saved_names" | ${pkgs.gum}/bin/gum choose)
-
if [ -z "$subdomain" ]; then
+
if [ -z "$tunnel_name" ]; then
${pkgs.gum}/bin/gum style --foreground 196 "No tunnel selected"
exit 1
fi
···
in_section=false
while IFS= read -r line; do
if [[ "$line" =~ ^\[([^]]+)\] ]]; then
-
if [[ "''${BASH_REMATCH[1]}" = "$subdomain" ]]; then
+
if [[ "''${BASH_REMATCH[1]}" = "$tunnel_name" ]]; then
in_section=true
else
in_section=false
···
elif [[ "$in_section" = true ]]; then
if [[ "$line" =~ ^port[[:space:]]*=[[:space:]]*([0-9]+) ]]; then
port="''${BASH_REMATCH[1]}"
+
elif [[ "$line" =~ ^protocol[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
+
protocol="''${BASH_REMATCH[1]}"
elif [[ "$line" =~ ^label[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
label="''${BASH_REMATCH[1]}"
fi
fi
done < "$CONFIG_FILE"
-
${pkgs.gum}/bin/gum style --foreground 35 "✓ Loaded from bore.toml: $subdomain → localhost:$port''${label:+ [$label]}"
+
proto_display="''${protocol:-http}"
+
${pkgs.gum}/bin/gum style --foreground 35 "✓ Loaded from bore.toml: $tunnel_name → localhost:$port [$proto_display]''${label:+ [$label]}"
else
-
# New tunnel
-
subdomain=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ")
-
if [ -z "$subdomain" ]; then
-
${pkgs.gum}/bin/gum style --foreground 196 "No subdomain provided"
+
# New tunnel - prompt for protocol first to determine what to ask for
+
protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp")
+
if [ -z "$protocol" ]; then
+
protocol="http"
+
fi
+
+
if [ "$protocol" = "http" ]; then
+
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ")
+
else
+
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "my-tunnel" --prompt "Tunnel name: ")
+
fi
+
+
if [ -z "$tunnel_name" ]; then
+
${pkgs.gum}/bin/gum style --foreground 196 "No name provided"
exit 1
fi
fi
else
${pkgs.gum}/bin/gum style --bold --foreground 212 "Creating bore tunnel"
echo
-
subdomain=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ")
-
if [ -z "$subdomain" ]; then
-
${pkgs.gum}/bin/gum style --foreground 196 "No subdomain provided"
+
# Prompt for protocol first
+
protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp")
+
if [ -z "$protocol" ]; then
+
protocol="http"
+
fi
+
+
if [ "$protocol" = "http" ]; then
+
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ")
+
else
+
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "my-tunnel" --prompt "Tunnel name: ")
+
fi
+
+
if [ -z "$tunnel_name" ]; then
+
${pkgs.gum}/bin/gum style --foreground 196 "No name provided"
exit 1
fi
fi
else
${pkgs.gum}/bin/gum style --bold --foreground 212 "Creating bore tunnel"
echo
-
subdomain=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ")
-
if [ -z "$subdomain" ]; then
-
${pkgs.gum}/bin/gum style --foreground 196 "No subdomain provided"
+
# Prompt for protocol first
+
protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp")
+
if [ -z "$protocol" ]; then
+
protocol="http"
+
fi
+
+
if [ "$protocol" = "http" ]; then
+
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ")
+
else
+
tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "my-tunnel" --prompt "Tunnel name: ")
+
fi
+
+
if [ -z "$tunnel_name" ]; then
+
${pkgs.gum}/bin/gum style --foreground 196 "No name provided"
exit 1
fi
fi
fi
-
# Validate subdomain
-
if ! echo "$subdomain" | ${pkgs.gnugrep}/bin/grep -qE '^[a-z0-9-]+$'; then
-
${pkgs.gum}/bin/gum style --foreground 196 "Invalid subdomain (use only lowercase letters, numbers, and hyphens)"
-
exit 1
+
# Validate tunnel name (only for http subdomains)
+
if [ "$protocol" = "http" ]; then
+
if ! echo "$tunnel_name" | ${pkgs.gnugrep}/bin/grep -qE '^[a-z0-9-]+$'; then
+
${pkgs.gum}/bin/gum style --foreground 196 "Invalid subdomain (use only lowercase letters, numbers, and hyphens)"
+
exit 1
+
fi
fi
# Get port (skip if loaded from saved config)
···
exit 1
fi
-
# Get optional label and save flag (skip if loaded from saved config)
+
# Get optional protocol, label and save flag (skip if loaded from saved config)
save_config=false
if [ -z "$label" ]; then
shift 2 2>/dev/null || true
while [[ $# -gt 0 ]]; do
case "$1" in
+
--protocol|-p)
+
protocol="$2"
+
shift 2
+
;;
--label|-l)
label="$2"
shift 2
···
esac
done
+
# Prompt for protocol if not provided via flag and not loaded from saved config and not already set
+
if [ -z "$protocol" ]; then
+
protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp")
+
if [ -z "$protocol" ]; then
+
protocol="http"
+
fi
+
fi
+
# Prompt for label if not provided via flag and not loaded from saved config
if [ -z "$label" ]; then
-
echo
# Allow multiple labels selection
labels=$(${pkgs.gum}/bin/gum choose --no-limit --header "Labels (select multiple):" "dev" "prod" "custom")
···
label=$(echo "$labels" | ${pkgs.coreutils}/bin/tr '\n' ',' | ${pkgs.gnused}/bin/sed 's/,$//')
fi
fi
+
fi
+
+
# Default protocol to http if still not set
+
if [ -z "$protocol" ]; then
+
protocol="http"
fi
# Check if local port is accessible
···
# Save configuration if requested
if [ "$save_config" = true ]; then
# Check if tunnel already exists in TOML
-
if [ -f "$CONFIG_FILE" ] && ${pkgs.gnugrep}/bin/grep -q "^\[$subdomain\]" "$CONFIG_FILE"; then
+
if [ -f "$CONFIG_FILE" ] && ${pkgs.gnugrep}/bin/grep -q "^\[$tunnel_name\]" "$CONFIG_FILE"; then
# Update existing entry
-
${pkgs.gnused}/bin/sed -i "/^\[$subdomain\]/,/^\[/{
+
${pkgs.gnused}/bin/sed -i "/^\[$tunnel_name\]/,/^\[/{
s/^port[[:space:]]*=.*/port = $port/
+
s/^protocol[[:space:]]*=.*/protocol = \"$protocol\"/
''${label:+s/^label[[:space:]]*=.*/label = \"$label\"/}
}" "$CONFIG_FILE"
else
# Append new entry
{
echo ""
-
echo "[$subdomain]"
+
echo "[$tunnel_name]"
echo "port = $port"
+
if [ "$protocol" != "http" ]; then
+
echo "protocol = \"$protocol\""
+
fi
if [ -n "$label" ]; then
echo "label = \"$label\""
fi
···
config_file=$(${pkgs.coreutils}/bin/mktemp)
trap "${pkgs.coreutils}/bin/rm -f $config_file" EXIT
-
# Encode label into proxy name if provided (format: subdomain[label1,label2])
-
proxy_name="$subdomain"
+
# Encode label into proxy name if provided (format: tunnel_name[label1,label2])
+
proxy_name="$tunnel_name"
if [ -n "$label" ]; then
-
proxy_name="''${subdomain}[''${label}]"
+
proxy_name="''${tunnel_name}[''${label}]"
fi
-
${pkgs.coreutils}/bin/cat > $config_file <<EOF
+
# Build proxy configuration based on protocol
+
if [ "$protocol" = "http" ]; then
+
${pkgs.coreutils}/bin/cat > $config_file <<EOF
serverAddr = "${cfg.serverAddr}"
serverPort = ${toString cfg.serverPort}
···
type = "http"
localIP = "127.0.0.1"
localPort = $port
-
subdomain = "$subdomain"
+
subdomain = "$tunnel_name"
EOF
+
elif [ "$protocol" = "tcp" ] || [ "$protocol" = "udp" ]; then
+
# For TCP/UDP, enable admin API to query allocated port
+
# Use Python to find a free port (cross-platform and guaranteed to work)
+
admin_port=$(${pkgs.python3}/bin/python3 -c 'import socket; s=socket.socket(); s.bind(("", 0)); print(s.getsockname()[1]); s.close()')
+
+
${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}"
+
+
webServer.addr = "127.0.0.1"
+
webServer.port = $admin_port
+
+
[[proxies]]
+
name = "$proxy_name"
+
type = "$protocol"
+
localIP = "127.0.0.1"
+
localPort = $port
+
remotePort = 0
+
EOF
+
else
+
${pkgs.gum}/bin/gum style --foreground 196 "Invalid protocol: $protocol (must be http, tcp, or udp)"
+
exit 1
+
fi
# Start tunnel
-
public_url="https://$subdomain.${cfg.domain}"
echo
${pkgs.gum}/bin/gum style --foreground 35 "✓ Tunnel configured"
-
${pkgs.gum}/bin/gum style --foreground 117 " Local: localhost:$port"
-
${pkgs.gum}/bin/gum style --foreground 117 " Public: $public_url"
+
${pkgs.gum}/bin/gum style --foreground 117 " Local: localhost:$port"
+
if [ "$protocol" = "http" ]; then
+
public_url="https://$tunnel_name.${cfg.domain}"
+
${pkgs.gum}/bin/gum style --foreground 117 " Public: $public_url"
+
else
+
${pkgs.gum}/bin/gum style --foreground 117 " Protocol: $protocol"
+
${pkgs.gum}/bin/gum style --foreground 214 " Waiting for server to allocate port..."
+
fi
echo
${pkgs.gum}/bin/gum style --foreground 214 "Connecting to ${cfg.serverAddr}:${toString cfg.serverPort}..."
+
echo
-
exec ${pkgs.frp}/bin/frpc -c $config_file
+
# For TCP/UDP, capture output to parse allocated port
+
if [ "$protocol" = "tcp" ] || [ "$protocol" = "udp" ]; then
+
# Start frpc in background and capture its PID
+
${pkgs.frp}/bin/frpc -c $config_file 2>&1 | while IFS= read -r line; do
+
echo "$line"
+
+
# Look for successful proxy start
+
if echo "$line" | ${pkgs.gnugrep}/bin/grep -q "start proxy success"; then
+
# Wait a moment for the proxy to fully initialize
+
sleep 1
+
+
# Query the frpc admin API for proxy status
+
proxy_status=$(${pkgs.curl}/bin/curl -s http://127.0.0.1:$admin_port/api/status 2>/dev/null || echo "{}")
+
+
# Try to extract remote port from JSON response
+
# Format: "remote_addr":"bore.dunkirk.sh:20097"
+
remote_addr=$(echo "$proxy_status" | ${pkgs.jq}/bin/jq -r ".tcp[]? | select(.name == \"$proxy_name\") | .remote_addr" 2>/dev/null)
+
if [ -z "$remote_addr" ] || [ "$remote_addr" = "null" ]; then
+
remote_addr=$(echo "$proxy_status" | ${pkgs.jq}/bin/jq -r ".udp[]? | select(.name == \"$proxy_name\") | .remote_addr" 2>/dev/null)
+
fi
+
+
# Extract just the port number
+
remote_port=$(echo "$remote_addr" | ${pkgs.gnugrep}/bin/grep -oP ':\K[0-9]+$')
+
+
if [ -n "$remote_port" ] && [ "$remote_port" != "null" ]; then
+
echo
+
${pkgs.gum}/bin/gum style --foreground 35 "✓ Tunnel established"
+
${pkgs.gum}/bin/gum style --foreground 117 " Local: localhost:$port"
+
${pkgs.gum}/bin/gum style --foreground 117 " Remote: ${cfg.serverAddr}:$remote_port"
+
${pkgs.gum}/bin/gum style --foreground 117 " Type: $protocol"
+
echo
+
fi
+
fi
+
done
+
else
+
exec ${pkgs.frp}/bin/frpc -c $config_file
+
fi
'';
bore = pkgs.stdenv.mkDerivation {
+48
modules/nixos/services/bore/README.md
···
+
# Bore
+
+
![screenshot](https://hc-cdn.hel1.your-objectstorage.com/s/v3/7652f29dacb8f76d_screenshot_2025-12-09_at_16.57.47.png)
+
+
Bore is a lightweight wrapper around `frp` which provides a dashboard and a nice `gum` based cli. It supports HTTP, TCP, and UDP tunneling. If you would like to run this in your own nix flake then simplify vendor this folder and `./modules/home/bore` and import the folders into the appropriate home manager and nixos configurations.
+
+
## Client Configuration
+
+
```nix
+
atelier = {
+
bore = {
+
enable = true;
+
authTokenFile = osConfig.age.secrets.bore.path
+
};
+
}
+
```
+
+
and be sure to have a definition for your agenix secret in the osConfig as well:
+
+
```nix
+
age = {
+
identityPaths = [
+
"path/to/ssh/key"
+
];
+
secrets = {
+
bore = {
+
file = ./path/to/bore.age;
+
owner = "username";
+
};
+
};
+
}
+
```
+
+
## Server Configuration
+
+
For TCP and UDP tunneling support, configure the server with allowed port ranges:
+
+
```nix
+
atelier.services.frps = {
+
enable = true;
+
domain = "bore.dunkirk.sh";
+
authTokenFile = config.age.secrets.bore.path;
+
allowedTCPPorts = [ 20000 20001 20002 20003 20004 ];
+
allowedUDPPorts = [ 20000 20001 20002 20003 20004 ];
+
};
+
```
+
+
The secret file is just a oneline file with the key in it. If you do end up deploying this feel free to email me and let me know! I would love to hear about your setup!
+23 -2
modules/nixos/services/bore/bore.nix
···
description = "Port for HTTP virtual host traffic";
};
+
allowedTCPPorts = lib.mkOption {
+
type = lib.types.listOf lib.types.port;
+
default = lib.lists.range 20000 20099;
+
example = [ 20000 20001 20002 20003 20004 ];
+
description = "TCP port range to allow for TCP tunnels (default: 20000-20099)";
+
};
+
+
allowedUDPPorts = lib.mkOption {
+
type = lib.types.listOf lib.types.port;
+
default = lib.lists.range 20000 20099;
+
example = [ 20000 20001 20002 20003 20004 ];
+
description = "UDP port range to allow for UDP tunnels (default: 20000-20099)";
+
};
+
authToken = lib.mkOption {
type = lib.types.nullOr lib.types.str;
default = null;
···
}
];
-
# Open firewall port for frp control connection
-
networking.firewall.allowedTCPPorts = [ cfg.bindPort ];
+
# Open firewall ports for frp control connection and TCP/UDP tunnels
+
networking.firewall.allowedTCPPorts = [ cfg.bindPort ] ++ cfg.allowedTCPPorts;
+
networking.firewall.allowedUDPPorts = cfg.allowedUDPPorts;
# frp server service
systemd.services.frps =
···
# Subdomain support for *.${cfg.domain}
subDomainHost = "${cfg.domain}"
+
+
# Allow port ranges for TCP/UDP tunnels
+
# Format: [[{"start": 20000, "end": 20099}]]
+
allowPorts = [
+
{ start = 20000, end = 20099 }
+
]
# Custom 404 page
custom404Page = "${./404.html}"
+11
modules/nixos/services/bore/bore.toml.example
···
[api]
port = 3000
+
protocol = "http"
label = "dev"
[frontend]
port = 5173
label = "local"
+
+
[database]
+
port = 5432
+
protocol = "tcp"
+
label = "postgres"
+
+
[game-server]
+
port = 27015
+
protocol = "udp"
+
label = "game"