@jaspermayone.com's dotfiles
1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.atelier.bore;
9
10 boreScript = pkgs.writeShellScript "bore" ''
11 CONFIG_FILE="bore.toml"
12
13 # Trap exit signals to ensure cleanup and exit immediately
14 trap 'exit 130' INT
15 trap 'exit 143' TERM
16 trap 'exit 129' HUP
17
18 # Enable immediate exit on error or pipe failure
19 set -e
20 set -o pipefail
21
22 # Check for flags
23 if [ "$1" = "--list" ] || [ "$1" = "-l" ]; then
24 ${pkgs.gum}/bin/gum style --bold --foreground 212 "Active tunnels"
25 echo
26
27 tunnels=$(${pkgs.curl}/bin/curl -s https://${cfg.domain}/api/proxy/http)
28
29 if ! echo "$tunnels" | ${pkgs.jq}/bin/jq -e '.proxies | length > 0' >/dev/null 2>&1; then
30 ${pkgs.gum}/bin/gum style --foreground 117 "No active tunnels"
31 exit 0
32 fi
33
34 # Filter only online tunnels with valid conf
35 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
36 ${pkgs.gum}/bin/gum style --foreground 35 "✓ $line"
37 done
38 exit 0
39 fi
40
41 if [ "$1" = "--saved" ] || [ "$1" = "-s" ]; then
42 if [ ! -f "$CONFIG_FILE" ]; then
43 ${pkgs.gum}/bin/gum style --foreground 117 "No bore.toml found in current directory"
44 exit 0
45 fi
46
47 ${pkgs.gum}/bin/gum style --bold --foreground 212 "Saved tunnels in bore.toml"
48 echo
49
50 # Parse TOML and show tunnels
51 while IFS= read -r line; do
52 if [[ "$line" =~ ^\[([^]]+)\] ]]; then
53 current_tunnel="''${BASH_REMATCH[1]}"
54 elif [[ "$line" =~ ^port[[:space:]]*=[[:space:]]*([0-9]+) ]]; then
55 port="''${BASH_REMATCH[1]}"
56 elif [[ "$line" =~ ^protocol[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
57 protocol="''${BASH_REMATCH[1]}"
58 elif [[ "$line" =~ ^label[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
59 label="''${BASH_REMATCH[1]}"
60 proto_display="''${protocol:-http}"
61 ${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port [$proto_display] [$label]"
62 label=""
63 protocol=""
64 elif [[ -z "$line" ]] && [[ -n "$current_tunnel" ]] && [[ -n "$port" ]]; then
65 proto_display="''${protocol:-http}"
66 ${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port [$proto_display]"
67 current_tunnel=""
68 port=""
69 protocol=""
70 fi
71 done < "$CONFIG_FILE"
72
73 # Handle last entry if file doesn't end with blank line
74 if [[ -n "$current_tunnel" ]] && [[ -n "$port" ]]; then
75 proto_display="''${protocol:-http}"
76 if [[ -n "$label" ]]; then
77 ${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port [$proto_display] [$label]"
78 else
79 ${pkgs.gum}/bin/gum style --foreground 35 "✓ $current_tunnel → localhost:$port [$proto_display]"
80 fi
81 fi
82 exit 0
83 fi
84
85 # Get tunnel name/subdomain
86 if [ -n "$1" ]; then
87 tunnel_name="$1"
88 else
89 # Check if we have a bore.toml in current directory
90 if [ -f "$CONFIG_FILE" ]; then
91 # Count tunnels in TOML
92 tunnel_count=$(${pkgs.gnugrep}/bin/grep -c '^\[' "$CONFIG_FILE" 2>/dev/null || echo "0")
93
94 if [ "$tunnel_count" -gt 0 ]; then
95 ${pkgs.gum}/bin/gum style --bold --foreground 212 "Creating bore tunnel"
96 echo
97
98 # Show choice between new or saved
99 choice=$(${pkgs.gum}/bin/gum choose "New tunnel" "Use saved tunnel")
100
101 if [ "$choice" = "Use saved tunnel" ]; then
102 # Extract tunnel names from TOML
103 saved_names=$(${pkgs.gnugrep}/bin/grep '^\[' "$CONFIG_FILE" | ${pkgs.gnused}/bin/sed 's/^\[\(.*\)\]$/\1/')
104 tunnel_name=$(echo "$saved_names" | ${pkgs.gum}/bin/gum choose)
105
106 if [ -z "$tunnel_name" ]; then
107 ${pkgs.gum}/bin/gum style --foreground 196 "No tunnel selected"
108 exit 1
109 fi
110
111 # Parse TOML for this tunnel's config
112 in_section=false
113 while IFS= read -r line; do
114 if [[ "$line" =~ ^\[([^]]+)\] ]]; then
115 if [[ "''${BASH_REMATCH[1]}" = "$tunnel_name" ]]; then
116 in_section=true
117 else
118 in_section=false
119 fi
120 elif [[ "$in_section" = true ]]; then
121 if [[ "$line" =~ ^port[[:space:]]*=[[:space:]]*([0-9]+) ]]; then
122 port="''${BASH_REMATCH[1]}"
123 elif [[ "$line" =~ ^protocol[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
124 protocol="''${BASH_REMATCH[1]}"
125 elif [[ "$line" =~ ^label[[:space:]]*=[[:space:]]*\"([^\"]+)\" ]]; then
126 label="''${BASH_REMATCH[1]}"
127 fi
128 fi
129 done < "$CONFIG_FILE"
130
131 proto_display="''${protocol:-http}"
132 ${pkgs.gum}/bin/gum style --foreground 35 "✓ Loaded from bore.toml: $tunnel_name → localhost:$port [$proto_display]''${label:+ [$label]}"
133 else
134 # New tunnel - prompt for protocol first to determine what to ask for
135 protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp")
136 if [ -z "$protocol" ]; then
137 protocol="http"
138 fi
139
140 if [ "$protocol" = "http" ]; then
141 tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ")
142 else
143 tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "my-tunnel" --prompt "Tunnel name: ")
144 fi
145
146 if [ -z "$tunnel_name" ]; then
147 ${pkgs.gum}/bin/gum style --foreground 196 "No name provided"
148 exit 1
149 fi
150 fi
151 else
152 ${pkgs.gum}/bin/gum style --bold --foreground 212 "Creating bore tunnel"
153 echo
154 # Prompt for protocol first
155 protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp")
156 if [ -z "$protocol" ]; then
157 protocol="http"
158 fi
159
160 if [ "$protocol" = "http" ]; then
161 tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ")
162 else
163 tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "my-tunnel" --prompt "Tunnel name: ")
164 fi
165
166 if [ -z "$tunnel_name" ]; then
167 ${pkgs.gum}/bin/gum style --foreground 196 "No name provided"
168 exit 1
169 fi
170 fi
171 else
172 ${pkgs.gum}/bin/gum style --bold --foreground 212 "Creating bore tunnel"
173 echo
174 # Prompt for protocol first
175 protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp")
176 if [ -z "$protocol" ]; then
177 protocol="http"
178 fi
179
180 if [ "$protocol" = "http" ]; then
181 tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "myapp" --prompt "Subdomain: ")
182 else
183 tunnel_name=$(${pkgs.gum}/bin/gum input --placeholder "my-tunnel" --prompt "Tunnel name: ")
184 fi
185
186 if [ -z "$tunnel_name" ]; then
187 ${pkgs.gum}/bin/gum style --foreground 196 "No name provided"
188 exit 1
189 fi
190 fi
191 fi
192
193 # Validate tunnel name (only for http subdomains)
194 if [ "$protocol" = "http" ]; then
195 if ! echo "$tunnel_name" | ${pkgs.gnugrep}/bin/grep -qE '^[a-z0-9-]+$'; then
196 ${pkgs.gum}/bin/gum style --foreground 196 "Invalid subdomain (use only lowercase letters, numbers, and hyphens)"
197 exit 1
198 fi
199 fi
200
201 # Get port (skip if loaded from saved config)
202 if [ -z "$port" ]; then
203 if [ -n "$2" ]; then
204 port="$2"
205 else
206 port=$(${pkgs.gum}/bin/gum input --placeholder "8000" --prompt "Local port: ")
207 if [ -z "$port" ]; then
208 ${pkgs.gum}/bin/gum style --foreground 196 "No port provided"
209 exit 1
210 fi
211 fi
212 fi
213
214 # Validate port
215 if ! echo "$port" | ${pkgs.gnugrep}/bin/grep -qE '^[0-9]+$'; then
216 ${pkgs.gum}/bin/gum style --foreground 196 "Invalid port (must be a number)"
217 exit 1
218 fi
219
220 # Get optional protocol, label and save flag (skip if loaded from saved config)
221 save_config=false
222 if [ -z "$label" ]; then
223 shift 2 2>/dev/null || true
224 while [[ $# -gt 0 ]]; do
225 case "$1" in
226 --protocol|-p)
227 protocol="$2"
228 shift 2
229 ;;
230 --label|-l)
231 label="$2"
232 shift 2
233 ;;
234 --save)
235 save_config=true
236 shift
237 ;;
238 *)
239 shift
240 ;;
241 esac
242 done
243
244 # Prompt for protocol if not provided via flag and not loaded from saved config and not already set
245 if [ -z "$protocol" ]; then
246 protocol=$(${pkgs.gum}/bin/gum choose --header "Protocol:" "http" "tcp" "udp")
247 if [ -z "$protocol" ]; then
248 protocol="http"
249 fi
250 fi
251
252 # Prompt for label if not provided via flag and not loaded from saved config
253 if [ -z "$label" ]; then
254 # Allow multiple labels selection
255 labels=$(${pkgs.gum}/bin/gum choose --no-limit --header "Labels (select multiple):" "dev" "prod" "custom")
256
257 if [ -n "$labels" ]; then
258 # Check if custom was selected
259 if echo "$labels" | ${pkgs.gnugrep}/bin/grep -q "custom"; then
260 custom_label=$(${pkgs.gum}/bin/gum input --placeholder "my-label" --prompt "Custom label: ")
261 if [ -z "$custom_label" ]; then
262 ${pkgs.gum}/bin/gum style --foreground 196 "No custom label provided"
263 exit 1
264 fi
265 # Replace 'custom' with the actual custom label
266 labels=$(echo "$labels" | ${pkgs.gnused}/bin/sed "s/custom/$custom_label/")
267 fi
268 # Join labels with comma
269 label=$(echo "$labels" | ${pkgs.coreutils}/bin/tr '\n' ',' | ${pkgs.gnused}/bin/sed 's/,$//')
270 fi
271 fi
272 fi
273
274 # Default protocol to http if still not set
275 if [ -z "$protocol" ]; then
276 protocol="http"
277 fi
278
279 # Check if local port is accessible
280 if ! ${pkgs.netcat}/bin/nc -z 127.0.0.1 "$port" 2>/dev/null; then
281 ${pkgs.gum}/bin/gum style --foreground 214 "! Warning: Nothing listening on localhost:$port"
282 fi
283
284 # Save configuration if requested
285 if [ "$save_config" = true ]; then
286 # Check if tunnel already exists in TOML
287 if [ -f "$CONFIG_FILE" ] && ${pkgs.gnugrep}/bin/grep -q "^\[$tunnel_name\]" "$CONFIG_FILE"; then
288 # Update existing entry
289 ${pkgs.gnused}/bin/sed -i "/^\[$tunnel_name\]/,/^\[/{
290 s/^port[[:space:]]*=.*/port = $port/
291 s/^protocol[[:space:]]*=.*/protocol = \"$protocol\"/
292 ''${label:+s/^label[[:space:]]*=.*/label = \"$label\"/}
293 }" "$CONFIG_FILE"
294 else
295 # Append new entry
296 {
297 echo ""
298 echo "[$tunnel_name]"
299 echo "port = $port"
300 if [ "$protocol" != "http" ]; then
301 echo "protocol = \"$protocol\""
302 fi
303 if [ -n "$label" ]; then
304 echo "label = \"$label\""
305 fi
306 } >> "$CONFIG_FILE"
307 fi
308
309 ${pkgs.gum}/bin/gum style --foreground 35 "✓ Configuration saved to bore.toml"
310 echo
311 fi
312
313 # Create config file
314 config_file=$(${pkgs.coreutils}/bin/mktemp)
315 trap "${pkgs.coreutils}/bin/rm -f $config_file" EXIT
316
317 # Encode label into proxy name if provided (format: tunnel_name[label1,label2])
318 proxy_name="$tunnel_name"
319 if [ -n "$label" ]; then
320 proxy_name="''${tunnel_name}[''${label}]"
321 fi
322
323 # Build proxy configuration based on protocol
324 if [ "$protocol" = "http" ]; then
325 ${pkgs.coreutils}/bin/cat > $config_file <<EOF
326 serverAddr = "${cfg.serverAddr}"
327 serverPort = ${toString cfg.serverPort}
328
329 auth.method = "token"
330 auth.tokenSource.type = "file"
331 auth.tokenSource.file.path = "${cfg.authTokenFile}"
332
333 [[proxies]]
334 name = "$proxy_name"
335 type = "http"
336 localIP = "127.0.0.1"
337 localPort = $port
338 subdomain = "$tunnel_name"
339 EOF
340 elif [ "$protocol" = "tcp" ] || [ "$protocol" = "udp" ]; then
341 # For TCP/UDP, enable admin API to query allocated port
342 admin_port=$(${pkgs.python3}/bin/python3 -c 'import socket; s=socket.socket(); s.bind(("", 0)); print(s.getsockname()[1]); s.close()')
343
344 ${pkgs.coreutils}/bin/cat > $config_file <<EOF
345 serverAddr = "${cfg.serverAddr}"
346 serverPort = ${toString cfg.serverPort}
347
348 auth.method = "token"
349 auth.tokenSource.type = "file"
350 auth.tokenSource.file.path = "${cfg.authTokenFile}"
351
352 webServer.addr = "127.0.0.1"
353 webServer.port = $admin_port
354
355 [[proxies]]
356 name = "$proxy_name"
357 type = "$protocol"
358 localIP = "127.0.0.1"
359 localPort = $port
360 remotePort = 0
361 EOF
362 else
363 ${pkgs.gum}/bin/gum style --foreground 196 "Invalid protocol: $protocol (must be http, tcp, or udp)"
364 exit 1
365 fi
366
367 # Start tunnel
368 echo
369 ${pkgs.gum}/bin/gum style --foreground 35 "✓ Tunnel configured"
370 ${pkgs.gum}/bin/gum style --foreground 117 " Local: localhost:$port"
371 if [ "$protocol" = "http" ]; then
372 public_url="https://$tunnel_name.${cfg.domain}"
373 ${pkgs.gum}/bin/gum style --foreground 117 " Public: $public_url"
374 else
375 ${pkgs.gum}/bin/gum style --foreground 117 " Protocol: $protocol"
376 ${pkgs.gum}/bin/gum style --foreground 214 " Waiting for server to allocate port..."
377 fi
378 echo
379 ${pkgs.gum}/bin/gum style --foreground 214 "Connecting to ${cfg.serverAddr}:${toString cfg.serverPort}..."
380 echo
381
382 # For TCP/UDP, capture output to parse allocated port
383 if [ "$protocol" = "tcp" ] || [ "$protocol" = "udp" ]; then
384 ${pkgs.frp}/bin/frpc -c $config_file 2>&1 | while IFS= read -r line; do
385 echo "$line"
386
387 # Look for successful proxy start
388 if echo "$line" | ${pkgs.gnugrep}/bin/grep -q "start proxy success"; then
389 sleep 1
390
391 proxy_status=$(${pkgs.curl}/bin/curl -s http://127.0.0.1:$admin_port/api/status 2>/dev/null || echo "{}")
392
393 remote_addr=$(echo "$proxy_status" | ${pkgs.jq}/bin/jq -r ".tcp[]? | select(.name == \"$proxy_name\") | .remote_addr" 2>/dev/null)
394 if [ -z "$remote_addr" ] || [ "$remote_addr" = "null" ]; then
395 remote_addr=$(echo "$proxy_status" | ${pkgs.jq}/bin/jq -r ".udp[]? | select(.name == \"$proxy_name\") | .remote_addr" 2>/dev/null)
396 fi
397
398 remote_port=$(echo "$remote_addr" | ${pkgs.gnugrep}/bin/grep -oP ':\K[0-9]+$')
399
400 if [ -n "$remote_port" ] && [ "$remote_port" != "null" ]; then
401 echo
402 ${pkgs.gum}/bin/gum style --foreground 35 "✓ Tunnel established"
403 ${pkgs.gum}/bin/gum style --foreground 117 " Local: localhost:$port"
404 ${pkgs.gum}/bin/gum style --foreground 117 " Remote: ${cfg.serverAddr}:$remote_port"
405 ${pkgs.gum}/bin/gum style --foreground 117 " Type: $protocol"
406 echo
407 fi
408 fi
409 done
410 else
411 exec ${pkgs.frp}/bin/frpc -c $config_file
412 fi
413 '';
414
415 bore = pkgs.stdenv.mkDerivation {
416 pname = "bore";
417 version = "1.0";
418
419 dontUnpack = true;
420
421 nativeBuildInputs = with pkgs; [
422 pandoc
423 installShellFiles
424 ];
425
426 manPageSrc = ./bore.1.md;
427 zshCompletionSrc = ./completions/bore.zsh;
428
429 buildPhase = ''
430 ${pkgs.pandoc}/bin/pandoc -s -t man $manPageSrc -o bore.1
431 '';
432
433 installPhase = ''
434 mkdir -p $out/bin
435
436 # Install binary
437 cp ${boreScript} $out/bin/bore
438 chmod +x $out/bin/bore
439
440 # Install man page
441 installManPage bore.1
442
443 # Install completions
444 installShellCompletion --zsh --name _bore $zshCompletionSrc
445 '';
446
447 meta = with lib; {
448 description = "Secure tunneling service CLI";
449 homepage = "https://tun.hogwarts.channel";
450 license = licenses.mit;
451 maintainers = [ ];
452 };
453 };
454in
455{
456 options.atelier.bore = {
457 enable = lib.mkEnableOption "bore tunneling service";
458
459 serverAddr = lib.mkOption {
460 type = lib.types.str;
461 default = "tun.hogwarts.channel";
462 description = "bore server address";
463 };
464
465 serverPort = lib.mkOption {
466 type = lib.types.port;
467 default = 7000;
468 description = "bore server port";
469 };
470
471 domain = lib.mkOption {
472 type = lib.types.str;
473 default = "tun.hogwarts.channel";
474 description = "Domain for public tunnel URLs";
475 };
476
477 authTokenFile = lib.mkOption {
478 type = lib.types.nullOr lib.types.path;
479 default = null;
480 description = "Path to file containing authentication token";
481 };
482 };
483
484 config = lib.mkIf cfg.enable {
485 home.packages = [
486 pkgs.frp
487 bore
488 ];
489 };
490}