-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfloo
More file actions
executable file
·1175 lines (1122 loc) · 65.9 KB
/
Copy pathfloo
File metadata and controls
executable file
·1175 lines (1122 loc) · 65.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env bash
# Built & run in production by the Agents Deployed team — https://agents-deployed.com
# floo — client-initiated, temporary, recorded support access.
#
# WHAT THIS DOES, IN ONE BREATH:
# You run this only when you want help. It makes YOUR box dial OUT to our relay and
# stand up a throwaway SSH endpoint that ONLY the holder of our published support key
# (the operator) can enter. While it runs you see a "technician connected" line and a
# pairing code to read back to us. The whole session is recorded to YOUR disk. When you
# press Ctrl-C (or close the window) the endpoint dies, the tunnel drops, the temporary
# keys are wiped, and you are shown whether anything on your box changed. Nothing is
# installed, nothing survives a reboot, nothing runs as root.
#
# DON'T TRUST US — READ US. This is one Bash file. Things worth confirming yourself:
# - It NEVER runs as root and asks for no password: grep -n 'sudo\|doas' "$0" -> nothing
# - The ONLY identity allowed in is our published CA public key, pasted in plain sight
# below (FLOO_OPERATOR_CA). Only the matching PRIVATE key — which lives on the
# operator's machine and is in no repo — can mint a login. You can SEE exactly whom
# you are authorizing.
# - Access lasts only while this process is alive. Ctrl-C = revoke. There is no daemon,
# no @reboot, no systemd unit, no entry added to ~/.ssh/authorized_keys.
# - Every change to your SSH keys, services, or scheduled jobs is shown to you on exit.
#
# Usage:
# curl -fsSL https://raw.githubusercontent.com/interkelstar/floo/<commit>/floo | bash
# floo open a support session (default)
# floo --help | --version
#
set -uo pipefail
umask 077 # everything we create (keys, config, recordings) is owner-only from birth
FLOO_VERSION="0.7.2"
# ─── The disclosure: the ONLY key that can enter. Public by design. ──────────────────────
# This is a *public* key. Publishing it is safe and is the whole point: you read it and
# know precisely who you let in. Secrecy of this line buys nothing; its INTEGRITY is what
# matters — if you fetched this script pinned to a commit hash over HTTPS, this line is the
# one the operator published.
FLOO_OPERATOR_CA="${FLOO_OPERATOR_CA:-}"
# The relay's host PUBLIC key — PINNED so a hijacked/MITM'd relay.agents-deployed.com is rejected
# rather than trusted on first contact. Without this pin, a network attacker who answers as the
# relay could feed your registration to themselves. Public by design (host keys are public).
FLOO_RELAY_HOSTKEY="${FLOO_RELAY_HOSTKEY:-}"
# ─── Where to dial out (overridable for testing via env). ────────────────────────────────
FLOO_RELAY_HOST="${FLOO_RELAY_HOST:-}"
FLOO_RELAY_PORT="${FLOO_RELAY_PORT:-443}" # 443 so it survives restrictive outbound firewalls
FLOO_RELAY_USER="${FLOO_RELAY_USER:-gw}" # a powerless, forwarding-only account on the relay
FLOO_RELAY_SOCK_DIR="${FLOO_RELAY_SOCK_DIR:-/run/floo}" # path namespace ON the relay
FLOO_RELAY_PIN="${FLOO_RELAY_PIN:-}" # short relay-hostkey fingerprint (pin-bootstrap)
FLOO_PUBLIC="${FLOO_PUBLIC:-0}" # --public: no-CA "quick" mode; the code IS the credential
# ─── Cosmetic ────────────────────────────────────────────────────────────────────────────
# shellcheck disable=SC2034 # full color palette declared; not every color is used everywhere
if [ -t 1 ]; then B=$'\e[1m'; D=$'\e[2m'; G=$'\e[32m'; Y=$'\e[33m'; R=$'\e[31m'; X=$'\e[0m'
else B=""; D=""; G=""; Y=""; R=""; X=""; fi
say() { printf '%s\n' "$*"; }
info() { printf '%s•%s %s\n' "$D" "$X" "$*"; }
ok() { printf '%s✓%s %s\n' "$G" "$X" "$*"; }
warn() { printf '%s⚠%s %s\n' "$Y" "$X" "$*"; }
codehash() { printf '%s' "$1" | sha256sum | cut -c1-64; } # pairing code never leaves the box in clear
norm() { printf '%s' "$1" | tr 'a-z' 'A-Z'; } # uniform code casing for sha256 + HMAC on both ends
# HMAC-SHA256(key=$1, message=stdin) -> lowercase hex digest. The key is passed to openssl as
# -macopt hexkey:<hex>, never as a plain -hmac argv value — the pairing code IS the credential in
# quick/public mode, and a literal argv value is visible to any other local user via `ps`/procfs
# for as long as the process runs. Hex-encoding doesn't hide it from someone who bothers to decode
# it, but it does remove the credential from being read off a process listing in the clear.
hmac_code() {
local key="$1" hexkey
hexkey="$(printf '%s' "$key" | od -An -tx1 | tr -d ' \n')"
openssl dgst -sha256 -mac hmac -macopt "hexkey:$hexkey" | awk '{print $NF}'
}
# ─── Resolve this box's name (our natural key: the box's unique label). ───────────────────
# Order: --name / FLOO_NAME -> FLOO_IDENTITY_HOOK (a script printing the name; how a deployment
# plugs in its own naming) -> hostname -s -> the Unix username. All cheap, all local.
resolve_name() {
[ -n "${FLOO_NAME:-}" ] && { printf '%s' "$FLOO_NAME"; return; }
if [ -n "${FLOO_IDENTITY_HOOK:-}" ] && [ -x "$FLOO_IDENTITY_HOOK" ]; then
local n; n="$("$FLOO_IDENTITY_HOOK" 2>/dev/null)"; [ -n "$n" ] && { printf '%s' "$n"; return; }
fi
local h; h="$(hostname -s 2>/dev/null)"; [ -n "$h" ] && { printf '%s' "$h"; return; }
id -un
}
# stable, private, tmpfs-backed home for this session's state (so teardown can wipe it cleanly)
session_dir() {
local base="${XDG_RUNTIME_DIR:-}"
# Fall back OFF world-writable /dev/shm: sshd's StrictModes refuses to read an
# AuthorizedPrincipalsFile whose parent dir is group/other-writable, which breaks cert auth for
# any client without a per-user runtime dir (cron/non-login). $HOME/.local/state is private.
[ -n "$base" ] && [ -d "$base" ] || base="${HOME:-/tmp}/.local/state"
printf '%s/floo/%s' "$base" "$1"
}
# ─── Attack-surface snapshot — the honest part of the trust promise. ─────────────────────
# We snapshot the three things a departing technician could use to get back in — SSH keys,
# enabled services, scheduled jobs — before and after, and show you the diff on exit. This
# is a *disclosure*, not a guarantee of prevention: a shell can change a system; what this
# promises is that any such change to these surfaces is surfaced to you, not hidden.
snapshot_surface() {
local out="$1"
{
echo "## authorized_keys"
local h ak
for h in "$HOME" /root /home/*; do
ak="$h/.ssh/authorized_keys"
[ -r "$ak" ] && { echo "# $ak"; cat "$ak"; }
done 2>/dev/null
echo "## enabled systemd user units"
systemctl --user list-unit-files --state=enabled --no-legend 2>/dev/null | awk '{print $1}' | sort
echo "## enabled systemd system units (readable subset)"
systemctl list-unit-files --state=enabled --no-legend 2>/dev/null | awk '{print $1}' | sort
echo "## user crontab"
crontab -l 2>/dev/null
echo "## /etc/crontab + /etc/cron.d + cron.* (readable)"
cat /etc/crontab /etc/cron.d/* /etc/cron.hourly/* /etc/cron.daily/* \
/etc/cron.weekly/* /etc/cron.monthly/* /etc/cron.yearly/* 2>/dev/null
} > "$out" 2>/dev/null
}
# ─── Build the throwaway SSH endpoint (in tmpfs; nothing on durable disk). ────────────────
WORKDIR=""; SSHD_PID=""; TUNNEL_PID=""; MONITOR_PID=""; BINDWATCH_PID=""; WATCHDOG_PID=""; NAME=""; SID=""; SSHD_PORT=""
PAIRCODE=""; BEFORE=""; AFTER=""; MARK_NONCE=""
build_endpoint() {
mkdir -p "$WORKDIR/recording"
# pre-pin the relay's host key. ssh's accept-new still adds a genuinely-new host, but a
# MISMATCH against this pin (a MITM presenting a different key) is refused.
if [ -n "${FLOO_RELAY_HOSTKEY:-}" ]; then
local rt; if [ "$FLOO_RELAY_PORT" = 22 ]; then rt="$FLOO_RELAY_HOST"; else rt="[$FLOO_RELAY_HOST]:$FLOO_RELAY_PORT"; fi
printf '%s %s\n' "$rt" "$FLOO_RELAY_HOSTKEY" > "$WORKDIR/relay_known_hosts"
elif [ -n "${FLOO_ALLOW_TOFU:-}" ]; then
warn "relay host key not pinned; FLOO_ALLOW_TOFU set -> trust-on-first-use (MITM-able on the first dial)."
else
warn "relay host key not pinned. Pass --relay-hostkey, import an operator config, or set FLOO_ALLOW_TOFU=1 to accept trust-on-first-use. Refusing."
exit 1
fi
SID="$(head -c8 /dev/urandom | od -An -tx1 | tr -d ' \n')" # random, non-secret session id: routing + cert principal
printf '%s\n' "$SID" > "$WORKDIR/principals" # the cert principal we will accept (the sid, not the name)
# per-session marker nonce: SECRET (unlike SID), baked into the shell hooks + exec recorder and
# handed to the renderer out-of-band, so operator-controlled command OUTPUT cannot forge the
# live command-log without first reading this file off the box. 128-bit, hex (OSC-safe).
MARK_NONCE="$(head -c16 /dev/urandom | od -An -tx1 | tr -d ' \n')"
printf '%s' "$MARK_NONCE" > "$WORKDIR/marknonce"; chmod 600 "$WORKDIR/marknonce"
ssh-keygen -t ed25519 -f "$WORKDIR/hostkey" -N '' -q -C "floo-$NAME"
# Choose the box's auth gate by mode. CA mode: only the operator's CA-signed cert (principal=sid) gets in.
# NO-CERT mode: no CA at all — authorized_keys starts EMPTY and the bind_watcher writes the single operator
# key only after it verifies HMAC(code, opkey). The code is the credential; the long code is the entropy.
if [ "$FLOO_PUBLIC" = 1 ]; then
: > "$WORKDIR/authorized_keys"; chmod 600 "$WORKDIR/authorized_keys"
AUTH_BLOCK="PubkeyAuthentication yes
AuthenticationMethods publickey
AuthorizedKeysFile $WORKDIR/authorized_keys"
else
printf '%s\n' "$FLOO_OPERATOR_CA" > "$WORKDIR/operator_ca.pub"
AUTH_BLOCK="PubkeyAuthentication yes
AuthenticationMethods publickey
TrustedUserCAKeys $WORKDIR/operator_ca.pub
AuthorizedPrincipalsFile $WORKDIR/principals
AuthorizedKeysFile /nonexistent"
fi
# the recorder: every command the operator runs, and its output, is teed to your disk.
cat > "$WORKDIR/record-session" <<'REC'
#!/usr/bin/env bash
# ForceCommand wrapper: this runs ONLY after a successful operator-cert login — so its presence
# IS a real technician session, never a liveness probe (probes never authenticate). It records
# the session to the client's disk and holds a marker file that drives the "● connected" display.
set -u
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}" # so systemctl/journalctl --user work over exec
DIR="$(dirname "$0")"; RECDIR="$DIR/recording"; ADIR="$DIR/active"; mkdir -p "$RECDIR" "$ADIR"
# the operator's quick-mode liveness probe (it polls until the client authorizes its key): succeed
# silently and record nothing — no marker, no "connected" flicker, no recorded "$ floo-probe".
[ "${SSH_ORIGINAL_COMMAND:-}" = floo-probe ] && exit 0
marker="$ADIR/$$"; : > "$marker"; trap 'rm -f "$marker"' EXIT
# one durable line per authenticated session — lets the watcher notice even a sub-second
# command (whose marker may appear+vanish between its 1s polls), so nothing is silently invisible
printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "${SSH_ORIGINAL_COMMAND:-<interactive shell>}" >> "$DIR/sessions.log"
stamp="$(date -u +%Y%m%dT%H%M%SZ)-$$"
# ONE fixed-name raw stream per session dir (all operator connections append to it). Fixed name so
# the live console's `tail -F` can follow it even though render_console starts BEFORE the operator
# connects (a glob expanded at that moment matches nothing → tail would follow a literal that never
# appears). The raw .raw is the complete record; teardown renders a readable .log beside it.
log="$RECDIR/session.raw"
{ echo "=== floo session $stamp ==="; echo "from: ${SSH_CONNECTION:-?}"; echo "cmd : ${SSH_ORIGINAL_COMMAND:-<interactive shell>}"; echo; } >> "$log"
if [ -n "${SSH_ORIGINAL_COMMAND:-}" ]; then
# non-interactive (an audit/upgrade script piped in, OR a direct `ssh host '<cmd>'`): put BOTH the
# commands AND their output in the ONE saved log, stamped with nonce markers so the live console can
# render them. Markers are defined HERE — before the transfer fast-path — so even a binary transfer's
# disclosure goes through the nonce channel and can never be swallowed by the renderer's suppression.
NONCE="$(cat "$DIR/marknonce" 2>/dev/null)"
__floo_osc() { printf '\033]1337;floo;%s;%s\007' "$NONCE" "$1" >> "$log"; }
__floo_mark_cmd() { # emit a nonce-stamped `cmd` marker for label $1, TRUNCATED so a giant label
# can't exceed the renderer's OSC scan bound and render as base64 garbage
local lbl="$1" max=4096
[ "${#lbl}" -gt "$max" ] && lbl="${lbl:0:$max}… [$(( ${#1} - max )) more bytes — see the raw recording]"
__floo_osc "cmd;$(printf '%s' "$lbl" | base64 | tr -d '\n')"
}
case "$SSH_ORIGINAL_COMMAND" in
rsync\ --server*|scp\ -*|*sftp-server*)
# binary, bidirectional file transfer: buffering stdin + tee would DEADLOCK and corrupt the
# protocol (the "null byte" warning is the tell). Record the FACT through the marker channel and
# pass it a clean channel. BUT only for a GENUINE transfer: a command that (a) chains/substitutes
# shell metacharacters (;, &, |, backtick, $, <, >, newline), OR (b) carries a command-EXECUTING
# option of the transfer binary itself (scp -S/-o a program, rsync -e/--rsh/--rsync-path a program)
# is NOT a plain transfer — it can run code whose output we MUST record — so it falls through to
# the normal teed+marked path below instead of this unrecorded fast path. (`-e.iLsfxC`, the benign
# capability token a real `rsync --server` sends, is deliberately NOT matched: the guard keys on a
# program-bearing ` -e ` / ` -e/`, never ` -e.`.)
case "$SSH_ORIGINAL_COMMAND" in
*';'*|*'&'*|*'|'*|*'`'*|*'$'*|*'<'*|*'>'*|*'
'*) : ;;
*' -S'*|*' -o'*|*' --rsh'*|*' --rsync-path'*|*' -e '*|*' -e/'*) : ;;
*)
__floo_mark_cmd "file transfer (binary — recorded as the command only): $SSH_ORIGINAL_COMMAND"
bash -c "$SSH_ORIGINAL_COMMAND"; rc=$?; __floo_osc "end;$rc"; exit "$rc" ;;
esac
;;
esac
# the cmd marker must carry the ACTUAL command. Two operator styles, classified EXACTLY:
# - the `floo-powder exec` shuttle: SSH_ORIGINAL_COMMAND is the EXACT literal `bash -s` and the real
# commands are the piped SCRIPT -> label = the script (slurp stdin to get it).
# - any direct `ssh host '<command>'` (the stack-update / agent flow, INCLUDING `bash -s <args>`):
# the command IS SSH_ORIGINAL_COMMAND -> label = it, and stdin (if any) is just its data, so we
# STREAM it to the child rather than slurp (slurping strips trailing newlines and would buffer a
# whole multi-GB transfer in memory). The old `bash\ -s\ *` wildcard misclassified `bash -s <args>`
# as the shuttle, giving it an empty label -> a blank `$` (the bug this fix exists to kill).
if [ "$SSH_ORIGINAL_COMMAND" = 'bash -s' ]; then
in="$(cat)"
__floo_mark_cmd "${in:-bash -s}"; __floo_osc "out"
printf '%s' "$in" | { bash -c "$SSH_ORIGINAL_COMMAND"; } 2>&1 | tee -a "$log"
rc="${PIPESTATUS[1]}"
else
__floo_mark_cmd "$SSH_ORIGINAL_COMMAND"; __floo_osc "out"
{ bash -c "$SSH_ORIGINAL_COMMAND"; } 2>&1 | tee -a "$log"
rc="${PIPESTATUS[0]}"
fi
__floo_osc "end;$rc"
exit "$rc"
fi
# interactive shell. A support session must NEVER attach the operator's or client's existing
# tmux: many shell rc files do `[[ -n $SSH_CONNECTION ]] && exec tmux ...` on login, which would
# hijack a shared session (and a teardown kill could then take it — and anything in it — down).
# Strip the SSH markers so that guard can't fire, then run the login shell on ssh's OWN pty
# (native size, keys, and Ctrl-C). Run it as a CHILD (not exec) so the EXIT trap clears the marker.
trap '' INT # Ctrl-C interrupts the shell's command, never this wrapper
unset SSH_CONNECTION SSH_CLIENT SSH_TTY # defeat SSH-auto-tmux in the user's shell rc
# choose how to start the shell: hooked bash/zsh emits invisible command markers; other shells
# start plainly and the live renderer falls back to cleaned stream output.
ushell="${SHELL:-/bin/bash}"; sh_base="$(basename "$ushell")"
if [ "$sh_base" = bash ] && [ -r "$DIR/hook.bash" ]; then
set -- "$ushell" --rcfile "$DIR/hook.bash" -i
elif [ "$sh_base" = zsh ] && [ -r "$DIR/zdot/.zshrc" ]; then
export __FLOO_REAL_ZDOTDIR="${ZDOTDIR:-$HOME}" ZDOTDIR="$DIR/zdot"
set -- "$ushell" -i
else
set -- "$ushell" -l
fi
if command -v script >/dev/null 2>&1; then
script -q -a "$log" -c "$(printf '%q ' "$@")" # keystroke-recorded where util-linux `script` exists
else
# IMPORTANT: write the relay to a file and run it. `python3 - <<'PY'` would make the heredoc
# Python's STDIN, so the relay could not read the operator's keystrokes (instant EOF → it broke,
# then blocked in waitpid for a shell starved of input = the "stuck on opening a shell" hang).
cat > "$DIR/relay.py" <<'PY'
# Record the interactive session by relaying ssh's pty through an inner pty we can tee. Safe now
# that SSH-auto-tmux is disabled (the earlier hang was tmux on a mis-sized pty, not the relay).
# Match the terminal size + forward SIGWINCH so the size is right; ignore INT/QUIT so Ctrl-C is
# passed to the shell as a byte (raw mode delivers ^C, the shell handles it) and never crashes us.
import os, pty, sys, tty, termios, fcntl, signal, select, struct
log = open(sys.argv[1], "ab", buffering=0); shell_argv = sys.argv[2:]
for s in (signal.SIGINT, signal.SIGQUIT):
try: signal.signal(s, signal.SIG_IGN)
except Exception: pass
def winsz(fd):
try: return fcntl.ioctl(fd, termios.TIOCGWINSZ, b"\0" * 8)
except Exception: return struct.pack("HHHH", 24, 80, 0, 0)
pid, fd = pty.fork()
if pid == 0:
os.execvp(shell_argv[0], shell_argv); os._exit(127)
def sync(*_):
try: fcntl.ioctl(fd, termios.TIOCSWINSZ, winsz(0))
except Exception: pass
sync()
try: signal.signal(signal.SIGWINCH, sync)
except Exception: pass
old = None
try: old = termios.tcgetattr(0); tty.setraw(0)
except Exception: pass
try:
while True:
try: r, _, _ = select.select([0, fd], [], [])
except (InterruptedError, OSError): continue
if 0 in r:
try: d = os.read(0, 65536)
except OSError: d = b""
if not d: break
try: os.write(fd, d)
except OSError: break
if fd in r:
try: d = os.read(fd, 65536)
except OSError: break
if not d: break
os.write(1, d)
try: log.write(d)
except Exception: pass
finally:
if old is not None:
try: termios.tcsetattr(0, termios.TCSADRAIN, old)
except Exception: pass
try: os.waitpid(pid, 0)
except Exception: pass
PY
python3 "$DIR/relay.py" "$log" "$@" # stdin stays the operator's terminal
fi
REC
chmod 700 "$WORKDIR/record-session"
floo_hook_rc bash "$MARK_NONCE" > "$WORKDIR/hook.bash" 2>/dev/null || true
mkdir -p "$WORKDIR/zdot"; floo_hook_rc zsh "$MARK_NONCE" > "$WORKDIR/zdot/.zshrc" 2>/dev/null || true
chmod 600 "$WORKDIR/hook.bash" "$WORKDIR/zdot/.zshrc" 2>/dev/null || true
# pick a free localhost port for the throwaway sshd
SSHD_PORT="$(python3 -c 'import socket;s=socket.socket();s.bind(("127.0.0.1",0));print(s.getsockname()[1]);s.close()' 2>/dev/null || echo $(( (RANDOM % 20000) + 20000 )))"
cat > "$WORKDIR/sshd_config" <<CFG
Port $SSHD_PORT
ListenAddress 127.0.0.1
HostKey $WORKDIR/hostkey
PidFile $WORKDIR/sshd.pid
# the box's auth rests ENTIRELY on the operator CA below — no passwords, no PAM, no aging
UsePAM no
PasswordAuthentication no
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
$AUTH_BLOCK
AllowUsers $(id -un)
# operator gets a shell to help you, nothing more — no forwarding back through your box
AllowTcpForwarding no
AllowStreamLocalForwarding no
AllowAgentForwarding no
X11Forwarding no
PermitTunnel no
PermitTTY yes
ForceCommand $WORKDIR/record-session
LogLevel VERBOSE
PermitRootLogin no
CFG
}
# Resolve the sshd binary — it lives in sbin and is usually NOT on a normal user's PATH, so probe the
# common locations. The relay installer resolves it the same way; keep the two halves in agreement.
resolve_sshd() {
local b; b="$(command -v sshd 2>/dev/null)"; [ -n "$b" ] && { printf '%s' "$b"; return; }
for b in /usr/sbin/sshd /usr/bin/sshd /sbin/sshd /usr/local/sbin/sshd; do
[ -x "$b" ] && { printf '%s' "$b"; return; }
done
printf '%s' /usr/sbin/sshd # canonical fallback (the preflight reports if it's genuinely absent)
}
SSHD_BIN="$(resolve_sshd)"
start_sshd() {
# own process group (setsid) so teardown can kill the listener AND every connection it forks
setsid "$SSHD_BIN" -D -f "$WORKDIR/sshd_config" -E "$WORKDIR/sshd.log" &
SSHD_PID=$!
sleep 0.4
kill -0 "$SSHD_PID" 2>/dev/null || { warn "could not start the local endpoint"; cat "$WORKDIR/sshd.log" 2>/dev/null; return 1; }
start_sshd_watchdog "$$" "$SSHD_PID"
}
# The endpoint must not outlive the controller (this floo process). Normally a dropped connection
# delivers SIGHUP and `teardown` runs on EXIT — but a hard crash or SIGKILL of the controller skips
# all of that, leaving the throwaway sshd (and the relay's reverse tunnel into it) reachable to
# anyone who already knows the sid. This background watchdog polls the controller's liveness and,
# the moment it's gone, kills the sshd's whole process group itself (TERM, then KILL after a grace).
start_sshd_watchdog() {
local ctrl_pid="$1" sshd_pid="$2" poll="${FLOO_WATCHDOG_POLL_INTERVAL:-2}"
(
while kill -0 "$ctrl_pid" 2>/dev/null; do sleep "$poll"; done
kill -TERM "-$sshd_pid" 2>/dev/null
sleep 1
kill -KILL "-$sshd_pid" 2>/dev/null
) &
WATCHDOG_PID=$!
disown "$WATCHDOG_PID" 2>/dev/null || true
}
pairing_code() {
if [ "$FLOO_PUBLIC" = 1 ]; then
# NO-CERT MODE: the code IS the credential against an untrusted relay, so it must be high-entropy
# (>=64 bits). 13 base32 chars ~= 65 bits; fall back to 16 hex (64 bits) where base32 is absent.
local raw
if command -v base32 >/dev/null 2>&1; then
raw="$(head -c9 /dev/urandom | base32 | tr -dc 'A-Z2-7' | cut -c1-13)"
else
raw="$(head -c8 /dev/urandom | od -An -tx1 | tr -d ' \n' | tr 'a-f' 'A-F')"
fi
PAIRCODE="$(printf '%s' "$raw" | sed 's/.\{4\}/&-/g; s/-$//')" # group every 4 chars
else
# CA MODE: the cert is the gate, so a short 32-bit pairing token is fine.
PAIRCODE="$(head -c4 /dev/urandom | od -An -tx1 | tr -d ' \n' | tr 'a-f' 'A-F')"
PAIRCODE="${PAIRCODE:0:4}-${PAIRCODE:4:4}"
fi
}
relay_ssh() { # helper: run a one-shot command on the relay's gw account
ssh -p "$FLOO_RELAY_PORT" \
-o BatchMode=yes -o ConnectTimeout=12 \
-o ServerAliveInterval=5 -o ServerAliveCountMax=2 \
-o StrictHostKeyChecking=accept-new \
-o UserKnownHostsFile="$WORKDIR/relay_known_hosts" \
-i "$WORKDIR/clientkey" \
"$FLOO_RELAY_USER@$FLOO_RELAY_HOST" "$@"
}
register_and_tunnel() {
ssh-keygen -t ed25519 -f "$WORKDIR/clientkey" -N '' -q -C "floo-tunnel-$NAME"
local hostpub; hostpub="$(cat "$WORKDIR/hostkey.pub")"
info "registering the session with the relay…"
local qflag=""; [ "$FLOO_PUBLIC" = 1 ] && qflag="quick=1"
if ! relay_ssh register "$SID" "$(codehash "$(norm "$PAIRCODE")")" "$(id -un)" "$NAME" $qflag "$hostpub"; then
warn "could not reach the relay at $FLOO_RELAY_HOST:$FLOO_RELAY_PORT — is the network up?"
return 1
fi
# the dial-out itself: a reverse unix socket on the relay → our throwaway sshd. This
# foreground-less process IS the access grant; killing it removes the relay socket
# (relay sets StreamLocalBindUnlink) and ends all access.
setsid ssh -p "$FLOO_RELAY_PORT" -N \
-o BatchMode=yes -o ExitOnForwardFailure=yes \
-o ServerAliveInterval=15 -o ServerAliveCountMax=3 \
-o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="$WORKDIR/relay_known_hosts" \
-i "$WORKDIR/clientkey" \
-R "$FLOO_RELAY_SOCK_DIR/$SID.sock:127.0.0.1:$SSHD_PORT" \
"$FLOO_RELAY_USER@$FLOO_RELAY_HOST" &
TUNNEL_PID=$!
printf '%s\n' "$TUNNEL_PID" > "$WORKDIR/tunnel.pid" # so `floo --stop` can reap an orphaned tunnel
sleep 1.2
kill -0 "$TUNNEL_PID" 2>/dev/null || { warn "the outbound tunnel did not stay up"; return 1; }
}
# ─── Live status: shared connect/idle detection (used by the python console AND no-python monitor). ──
# Reap dead active-markers under $1/active, echo the current sessions.log line count, and return 0 if a
# session is live RIGHT NOW (a live PID marker, OR a new sessions.log line since $2) else 1. The recorder
# writes BOTH signals ONLY after a cert login (so a liveness probe never trips them): a durable per-session
# line in sessions.log (catches even a sub-second command) + a live PID marker under active/.
_session_active() {
local dir="$1" seen="$2" m p live now
for m in "$dir"/active/*; do
[ -e "$m" ] || continue; p="$(basename "$m")"; kill -0 "$p" 2>/dev/null || rm -f "$m"
done
live="$(ls "$dir/active" 2>/dev/null | wc -l)"
now="$( [ -f "$dir/sessions.log" ] && wc -l < "$dir/sessions.log" || echo 0 )"
echo "${now:-0}"
[ "${now:-0}" -gt "${seen:-0}" ] || [ "${live:-0}" -gt 0 ]
}
# ─── The client's own window when python3 is ABSENT: plain connect/idle lines (no pinned frame). ──
monitor() {
mkdir -p "$WORKDIR/active"
local shown=0 idle=0 seen now active
seen="$( [ -f "$WORKDIR/sessions.log" ] && wc -l < "$WORKDIR/sessions.log" || echo 0 )"
while kill -0 "$SSHD_PID" 2>/dev/null && kill -0 "$TUNNEL_PID" 2>/dev/null; do
now="$(_session_active "$WORKDIR" "$seen")"; active=$?
if [ "$active" = 0 ]; then
seen="$now"; idle=0
[ "$shown" = 0 ] && { printf '%s● your helper is connected · Ctrl-C ends the session%s\n' "$G" "$X"; shown=1; }
elif [ "$shown" = 1 ]; then
# stepped away — never say "finished" (it nudges the client to close mid-operation); 30s avoids flap.
idle=$((idle + 1))
[ "$idle" -ge 30 ] && { printf '%s○ support session open · your helper stepped away (still recording) — Ctrl-C only when you are done%s\n' "$D" "$X"; shown=0; idle=0; }
fi
sleep 1
done
}
# ─── No-cert bind: authorize the operator's ephemeral key after verifying the code-proof. ────────
# PUBLIC MODE only. The operator binds (at the relay) an ephemeral key + auth=HMAC(code,opkey). We poll
# getop, verify the HMAC ourselves (only the code-holder can forge it), and authorize the FIRST bind that
# verifies — skipping inert griefer binds. sshd reads authorized_keys per-connection, so no reload needed.
bind_watcher() {
local got auth opkey calc authorized=0
while kill -0 "$TUNNEL_PID" 2>/dev/null; do
if [ "$authorized" = 0 ]; then
got="$(relay_ssh getop "$SID" 2>/dev/null)" || { sleep 2; continue; }
while IFS=' ' read -r auth opkey; do
[ -n "$auth" ] && [ -n "$opkey" ] || continue
calc="$(printf '%s' "$opkey" | hmac_code "$(norm "$PAIRCODE")")"
if [ "$calc" = "$auth" ]; then
printf '%s\n' "$opkey" > "$WORKDIR/authorized_keys.tmp"; chmod 600 "$WORKDIR/authorized_keys.tmp"
mv "$WORKDIR/authorized_keys.tmp" "$WORKDIR/authorized_keys" # atomic swap; live per-connection
ok "the person you gave the code to is verified — they can connect now."
authorized=1; break
fi
done <<<"$got"
fi
sleep 2
done
}
# ─── Terminal frame: a scrolling pane above a pinned status row. ────────────────────────
CON_ROWS=""
_con_rows() {
local rows
if rows="$(tput lines 2>/dev/null)" && [ -n "$rows" ]; then echo "$rows"; return; fi
rows="$(stty size 2>/dev/null | awk '{print $1}')"
echo "${rows:-24}"
}
_con_setup() {
CON_ROWS="$(_con_rows)"
[ "${CON_ROWS:-0}" -gt 1 ] || CON_ROWS=24
printf '\033[1;%dr\033[%d;1H' $((CON_ROWS-1)) $((CON_ROWS-1))
}
_con_status() { printf '\033[s\033[%d;1H\033[2K%s\033[u' "$CON_ROWS" "$1"; }
_con_teardown() { printf '\033[r\033[%d;1H\033[2K\033[?25h\n' "${CON_ROWS:-24}"; }
_fmt_elapsed() { local s="$1"; printf '%dm%02ds' $((s/60)) $((s%60)); }
# ONE client-facing word for the person on the other end — "your helper" — used everywhere a
# non-technical user reads it (the warm term the README + --public prompt already use). "operator" is
# reserved for the operator's own CLI (floo-powder) and code comments. Idle uses a HOLLOW glyph (○) in
# both this frame and the no-python monitor() so "paused/away" reads consistently across environments.
_con_line() {
case "$1" in
waiting) printf '%s◷ waiting for your helper to connect — Ctrl-C cancels%s' "$D" "$X";;
connected) printf '%s● your helper is connected · %s · Ctrl-C cuts the connection%s' "$G" "$2" "$X";;
idle) printf '%s○ support session open · %s · your helper stepped away (still recording) — Ctrl-C only when you are done%s' "$D" "$2" "$X";;
esac
}
render_console() {
local dir="$WORKDIR" rpid start="" shown=0 idle=0 seen now active last_status="" nonce
mkdir -p "$dir/active"
command -v python3 >/dev/null 2>&1 || { monitor; return; } # no python: plain status lines, no frame
nonce="$(cat "$dir/marknonce" 2>/dev/null)"
trap '_con_setup; [ -n "${last_status:-}" ] && _con_status "$last_status"' WINCH
# reap THIS pane's pipeline (its own subshell children — NOT a system-wide pattern), restore the terminal
trap 'pkill -P "$rpid" 2>/dev/null; kill "$rpid" 2>/dev/null; _con_teardown; exit 0' INT TERM HUP
printf '\033[?25l'; _con_setup
# tail a FIXED name (`tail -F` retries it until the recorder creates it on the first connect).
( tail -n +1 -F "$dir/recording/session.raw" 2>/dev/null | FLOO_MARK_NONCE="$nonce" render_stream ) &
rpid=$!
seen="$( [ -f "$dir/sessions.log" ] && wc -l < "$dir/sessions.log" || echo 0 )"
last_status="$(_con_line waiting)"; _con_status "$last_status"
while kill -0 "$rpid" 2>/dev/null; do
now="$(_session_active "$dir" "$seen")"; active=$?
if [ "$active" = 0 ]; then
seen="$now"; idle=0; [ -z "$start" ] && start="$SECONDS"; shown=1
last_status="$(_con_line connected "$(_fmt_elapsed $((SECONDS-start)))")"; _con_status "$last_status"
elif [ "$shown" = 1 ]; then
# stepped away (a bot runs many brief execs with gaps) — never "finished"; 30s avoids flapping.
idle=$((idle + 1))
[ "$idle" -ge 30 ] && { last_status="$(_con_line idle "$(_fmt_elapsed $((SECONDS-start)))")"; _con_status "$last_status"; shown=0; idle=0; }
fi
sleep 1
done
pkill -P "$rpid" 2>/dev/null # reap THIS pane's tail if the pipeline ended on its own
_con_teardown
}
# ─── Live renderer: raw recording stream (stdin) → clean command-log lines (stdout). ─────────
# Written to a file because it must read the operator stream on stdin.
RENDER_PY=""
write_render_py() {
[ -n "$RENDER_PY" ] && return 0
# inside a live session, place it in WORKDIR so teardown's `rm -rf "$WORKDIR"` reaps it; the
# standalone modes (--render/--clean-dir) fall back to a temp file they self-clean.
if [ -n "${WORKDIR:-}" ] && [ -d "$WORKDIR" ]; then RENDER_PY="$WORKDIR/render.py"
else RENDER_PY="$(mktemp "${TMPDIR:-/tmp}/floo-render.XXXXXX")" || return 1; fi
cat > "$RENDER_PY" <<'PY'
import sys, os, re, base64, codecs
# A floo command marker is ONLY honored when it carries the per-session nonce (FLOO_MARK_NONCE),
# which the client bakes into the injected shell hooks + the exec recorder and passes to this
# renderer out-of-band. Command OUTPUT (operator-controlled) therefore cannot forge a marker
# without first exfiltrating the session nonce from the box's own files — closing the trivial
# "print a fake $ command into the live pane" vector. Empty nonce => NO markers honored (safe).
NONCE = os.environ.get('FLOO_MARK_NONCE', '')
MARK_RE = re.compile(r'1337;floo;' + re.escape(NONCE) + r';(\w+)(?:;(.*))?$', re.S) if NONCE else None
MAXSEQ = 1 << 16 # abandon an unterminated escape/OSC longer than this (anti-DoS, avoids O(n^2))
MAXCOL = 2000 # a real terminal clamps the cursor to its width; we clamp too, so a single
# operator-controlled cursor-forward/absolute-column escape (or a newline-less
# line) can never balloon the line buffer / the saved recording. Long lines wrap.
out = sys.stdout
cur = []
col = 0
# SUP: in a hooked session, suppress the inter-command region (the shell prompt + the echo of the
# typed command) in the RENDERED view — the command is already shown cleanly via its `$ <cmd>` marker
# and its output between out/end. Starts off (pre-first-marker + marker-less non-hooked sessions render
# fully). ONLY a `prompt` marker turns it on, and it SELF-LIMITS at the next newline (the prompt+echo is
# a single line). Crucially `end` does NOT latch it: every operator connection appends to the one shared
# session.raw that this single renderer concatenates, so a latched flag would let one connection's
# end/prompt swallow ANOTHER connection's real output (and the file-transfer disclosure). Self-limiting
# + prompt-only bounds any cross-connection bleed to at most one line; the raw `.raw` keeps every byte.
SUP = {"on": False}
decoder = codecs.getincrementaldecoder('utf-8')('replace')
def emit(line):
out.write(line.rstrip() + "\n"); out.flush()
def put(ch):
global col
if col >= MAXCOL: flush_line() # wrap a pathologically long line instead of growing cur
while len(cur) <= col: cur.append(' ')
cur[col] = ch; col += 1
def flush_line():
global col
emit(''.join(cur)); cur[:] = []; col = 0
def sanitize_label(s):
# The command label is display-only: never let it carry raw escapes to the client terminal
# (those could overwrite the pinned status line / forge the connection indicator). Strip CSI/
# OSC sequences, C0 AND 8-bit C1 controls; keep newlines so a multi-line exec script renders as lines.
o = []; i = 0; n = len(s)
while i < n:
c = s[i]
if c == '\x1b':
if i+1 < n and s[i+1] == '[':
j = i+2
while j < n and s[j] in '0123456789;?': j += 1
while j < n and ' ' <= s[j] <= '/': j += 1
i = j+1 if j < n else n; continue
if i+1 < n and s[i+1] == ']':
j = i+2
while j < n and s[j] not in '\x07\x1b': j += 1
i = j+1; continue
i += 2; continue
if c == '\n':
if len(o) < MAXSEQ: o.append('\n') # bound a pathological label too
i += 1; continue
if c == '\t': o.append(' '); i += 1; continue
if ord(c) < 32 or 0x80 <= ord(c) <= 0x9f: i += 1; continue # drop C0 + 8-bit C1
if len(o) < MAXSEQ: o.append(c)
i += 1
return ''.join(o)
def handle_marker(kind, arg):
if kind == "cmd":
SUP["on"] = False # show this command + its output
try: raw = base64.b64decode(arg or "").decode('utf-8', 'replace')
except Exception: raw = arg or ""
cmd = sanitize_label(raw)
lines = cmd.split('\n')
emit("$ " + lines[0])
for extra in lines[1:]:
if extra.strip(): emit(" " + extra)
elif kind == "out":
pass
elif kind == "prompt":
if cur: flush_line()
SUP["on"] = True # suppress the prompt + the typed-command echo
elif kind == "end":
try: code = int(arg or "0")
except ValueError: code = 0
if code != 0: emit(" ↳ exit %d" % code)
# NB: `end` deliberately does NOT set SUP — only `prompt` (the interactive precmd) does. An exec
# connection emits cmd/out/end with no prompt, so after it SUP stays off and the NEXT connection's
# output renders instead of being suppressed.
def feed(text):
global col
i = 0; n = len(text)
while i < n:
ch = text[i]
if ch == '\x1b':
if i+1 >= n: return text[i:]
nxt = text[i+1]
if nxt == '[': # CSI — applied via the line emulator
j = i+2
while j < n and text[j] in '0123456789;?': j += 1
while j < n and ' ' <= text[j] <= '/': j += 1
if j >= n:
if n - i > MAXSEQ: i = n; break # runaway: abandon
return text[i:]
f = text[j]; p = re.sub(r'\?', '', text[i+2:j])
nums = [int(x) for x in p.split(';') if x.isdigit()] or [0]
if f == 'K':
m = nums[0]
if m == 0: del cur[col:]
elif m == 1:
for k in range(min(col+1, len(cur))): cur[k] = ' '
else: cur[:] = []
elif f == 'C': col = min(MAXCOL, col + (nums[0] or 1)) # clamp: no balloon
elif f == 'D': col = max(0, col-(nums[0] or 1))
elif f == 'G': col = min(MAXCOL, max(0, (nums[0] or 1)-1)) # clamp: no balloon
elif f in ('H', 'f'): col = 0
# NB: full-screen apps (alt-screen ?1049h/l, absolute row moves) are NOT collapsed or
# suppressed — suppression keyed on operator-controlled output bytes could hide real
# output from the watching client and from the saved recording. We render everything;
# the command's own "$ <cmd>" marker already tells the client what opened.
i = j+1; continue
if nxt == ']': # OSC — only NONCE-valid floo markers act
j = i+2
while j < n and text[j] not in '\x07\x1b': j += 1
if j >= n:
if n - i > MAXSEQ: i = n; break # runaway unterminated OSC: abandon
return text[i:]
body = text[i+2:j]
if text[j] == '\x07':
end = j+1
else: # ESC: real ST (ESC \) or a fresh escape
if j+1 >= n: return text[i:]
end = j+2 if text[j+1] == '\\' else j # bare ESC -> drop malformed OSC, resume at it
if MARK_RE:
m = MARK_RE.match(body)
if m:
if cur: flush_line()
handle_marker(m.group(1), m.group(2))
i = end; continue
i += 2; continue # other ESC x -> drop
if SUP["on"]: # inter-command prompt/echo — suppressed
if ch == '\n': SUP["on"] = False # self-limit: the prompt+echo is ONE line
i += 1; continue
if ch == '\r': col = 0; i += 1; continue
if ch == '\n': flush_line(); i += 1; continue
if ch == '\b': col = max(0, col-1); i += 1; continue
if ch == '\t': put(' '); i += 1; continue
if ord(ch) < 32 or 0x80 <= ord(ch) <= 0x9f: i += 1; continue # drop C0 AND 8-bit C1 controls
put(ch); i += 1
return ""
pending = ""
while True:
try: chunk = os.read(0, 65536)
except OSError: break
if not chunk: break
pending = feed(pending + decoder.decode(chunk, final=False))
if len(pending) > MAXSEQ * 2: pending = pending[-MAXSEQ:] # never let a partial grow unbounded
tail = decoder.decode(b"", final=True)
if tail:
pending = feed(pending + tail)
if cur: flush_line()
PY
}
render_stream() {
write_render_py || return 1
python3 "$RENDER_PY"; local rc=$?
# standalone callers (no live WORKDIR to be wiped) self-clean the temp renderer script
{ [ -z "${WORKDIR:-}" ] || [ ! -d "${WORKDIR:-}" ]; } && [ -n "$RENDER_PY" ] && rm -f "$RENDER_PY" 2>/dev/null
return $rc
}
# ─── Shell hooks: make the operator's shell announce each command via invisible private-OSC. ──
# Markers carry the per-session NONCE (arg 2) so command output cannot forge them. The capture is a
# distilled bash-preexec: arm ONLY between the end of PROMPT_COMMAND and the next command, so neither
# PROMPT_COMMAND's own commands (any shape — string, array, function, multi-statement) nor later
# pipeline stages are mistaken for the user's command. The command text is the full typed line from
# history; if history did not advance (HISTCONTROL=ignorespace / history off) we fall back to
# BASH_COMMAND so a command can never be silently mislabeled as a prior one.
floo_hook_rc() {
local nonce="${2:-}"
# nonce is embedded verbatim into the generated shell-hook script (below) and, via --emit-hook, is
# directly attacker/argv-controlled — a nonce containing a quote or shell metacharacter could break
# out of the printf format string it's spliced into and inject code into a hook the user's shell
# then sources. Reject anything outside a safe, fixed character set before it's ever used.
if [ -n "$nonce" ] && ! [[ "$nonce" =~ ^[A-Za-z0-9_-]{1,64}$ ]]; then
warn "bad nonce for --emit-hook (must match ^[A-Za-z0-9_-]{1,64}\$)"; return 1
fi
case "$1" in
bash) cat <<RC
# floo command-boundary hook (bash). Load the user's normal interactive env first.
[ -r /etc/bash.bashrc ] && . /etc/bash.bashrc
[ -r "\$HOME/.bashrc" ] && . "\$HOME/.bashrc"
__floo_osc() { printf '\033]1337;floo;${nonce};%s\007' "\$1"; }
__floo_on=; __floo_active=; __floo_hist=
__floo_preexec() { # DEBUG: fires before every simple command
[ -n "\${COMP_LINE:-}" ] && return # readline completion, not a command
[ -z "\$__floo_on" ] && return # inside PROMPT_COMMAND / a later pipeline stage / startup
case "\$BASH_COMMAND" in __floo_preexec|__floo_precmd|__floo_arm) return;; esac
__floo_on=; __floo_active=1 # consume the arm for the rest of THIS command line
local h n cmd
h=\$(HISTTIMEFORMAT= builtin history 1 2>/dev/null)
h=\${h#"\${h%%[0-9]*}"}; n=\${h%%[![:digit:]]*}; cmd=\${h#"\$n"}
cmd=\${cmd#"\${cmd%%[![:space:]]*}"} # the full typed command line
if [ -n "\$n" ] && [ "\$n" != "\$__floo_hist" ]; then __floo_hist=\$n; else cmd=\$BASH_COMMAND; fi
__floo_osc "cmd;\$(printf '%s' "\$cmd" | base64 | tr -d '\n')"
__floo_osc "out"
}
__floo_precmd() { # FIRST in PROMPT_COMMAND: close prior cmd + disarm
local ec=\$?
__floo_on= # so the rest of PROMPT_COMMAND is never seen as a cmd
[ -n "\$__floo_active" ] && { __floo_osc "end;\$ec"; __floo_active=; }
__floo_osc "prompt" # mark the prompt so the renderer drops it + the echo
return \$ec
}
__floo_arm() { local e=\$?; __floo_on=1; return \$e; } # LAST in PROMPT_COMMAND: arm for the user's next line
# Install precmd-first / arm-last around the user's PROMPT_COMMAND. For an ARRAY PROMPT_COMMAND each
# element runs independently. For a STRING we run it via eval inside a wrapper, so a user string that
# ends in a separator (;, &&, ||, |, &), starts with ;, or is whitespace-only can NEVER turn the
# install into an empty-statement syntax error (which would silently kill all capture for the session).
if [[ "\$(declare -p PROMPT_COMMAND 2>/dev/null)" == "declare -a"* ]]; then
PROMPT_COMMAND=(__floo_precmd "\${PROMPT_COMMAND[@]}" __floo_arm)
else
# eval the user's string at PROMPT_COMMAND top level: the surrounding parse can never break (eval
# takes its body as one string argument), and the body runs in the shell context so the user's
# positional params / shell state are preserved.
__floo_user_pc=\$PROMPT_COMMAND
PROMPT_COMMAND='__floo_precmd; eval "\$__floo_user_pc"; __floo_arm'
fi
trap '__floo_preexec' DEBUG
RC
;;
zsh) cat <<RC
# floo command-boundary hook (zsh). Load the user's normal env from their real ZDOTDIR/HOME.
[ -r "\${__FLOO_REAL_ZDOTDIR:-\$HOME}/.zshrc" ] && source "\${__FLOO_REAL_ZDOTDIR:-\$HOME}/.zshrc"
__floo_osc() { printf '\033]1337;floo;${nonce};%s\007' "\$1" }
__floo_ran=
__floo_preexec() { __floo_ran=1; __floo_osc "cmd;\$(print -rn -- "\$1" | base64 | tr -d '\n')"; __floo_osc "out" }
__floo_precmd() { local ec=\$?; [[ -n \$__floo_ran ]] && { __floo_osc "end;\$ec"; __floo_ran= }; __floo_osc "prompt" }
if autoload -Uz add-zsh-hook 2>/dev/null; then
add-zsh-hook preexec __floo_preexec
add-zsh-hook precmd __floo_precmd
else
typeset -ga preexec_functions precmd_functions
preexec_functions+=(__floo_preexec)
precmd_functions+=(__floo_precmd)
fi
RC
;;
*) return 1;;
esac
}
# ─── Write a readable command-log ALONGSIDE the raw recording. ───────────────────────────
# For each raw `session.raw` (the exact tee'd pty bytes) we write a readable `session.log` rendered
# through the SAME renderer the live console uses (markers -> "$ command", escapes collapsed). The
# raw `.raw` is left UNTOUCHED: it is the complete, tamper-evident record. The readable `.log` (and
# the live pane) render the session the way a terminal would (control sequences, incl. OSC bodies,
# are not shown — exactly as a terminal wouldn't show them), so a determined operator can obscure
# the *rendered* view; the raw `.raw` still holds every byte for after-the-fact review.
clean_recordings() {
local d="$1"; [ -d "$d" ] || return 0
command -v python3 >/dev/null 2>&1 || return 0
write_render_py || return 0
local f nonce; nonce="${FLOO_MARK_NONCE:-$MARK_NONCE}"
for f in "$d"/*.raw; do
[ -f "$f" ] || continue
FLOO_MARK_NONCE="$nonce" python3 "$RENDER_PY" < "$f" > "${f%.raw}.log" 2>/dev/null || rm -f "${f%.raw}.log"
done
}
# Is the throwaway endpoint port still bound? Prefer `ss`; else a bash /dev/tcp connect probe (no extra
# binary). Echoes "yes" (a listener is there), "no" (confirmed free), or "unknown" (couldn't check) — so
# teardown never CLAIMS a revoke it cannot actually confirm (the one overclaim a support tool must avoid).
port_bound() {
local p="$1"
[ -n "$p" ] || { echo unknown; return; }
if command -v ss >/dev/null 2>&1; then
ss -tlnH "( sport = :$p )" 2>/dev/null | grep -q . && echo yes || echo no
return
fi
# A successful /dev/tcp connect to 127.0.0.1:p PROVES a listener is still up. A failure is ambiguous
# (connection refused = free, but /dev/tcp may also be disabled in this bash), so we report "unknown"
# on failure rather than assert "free" — keeping the teardown message honest.
if (exec 3<>"/dev/tcp/127.0.0.1/$p") 2>/dev/null; then exec 3>&- 3<&-; echo yes; return; fi
echo unknown
}
# ─── Teardown: the revoke. Runs on Ctrl-C, on close, on any exit. ────────────────────────
teardown() {
trap - EXIT INT TERM HUP
printf '\033[r\033[?25h' 2>/dev/null # release any scroll region + show cursor
echo
info "closing the support session…"
# kill whole process groups (negative pid) so no forked sshd/ssh child is orphaned. Killing
# render_console (MONITOR_PID) fires its own signal trap, which scope-reaps ITS pipeline (tail +
# renderer) by child PID — so we never need a system-wide pattern kill that could hit another pane.
[ -n "$MONITOR_PID" ] && { kill "$MONITOR_PID" 2>/dev/null; wait "$MONITOR_PID" 2>/dev/null; }
[ -n "$BINDWATCH_PID" ] && kill "$BINDWATCH_PID" 2>/dev/null
[ -n "$WATCHDOG_PID" ] && kill "$WATCHDOG_PID" 2>/dev/null
[ -n "$TUNNEL_PID" ] && kill -TERM "-$TUNNEL_PID" 2>/dev/null
[ -n "$SSHD_PID" ] && kill -TERM "-$SSHD_PID" 2>/dev/null
sleep 0.5
[ -n "$TUNNEL_PID" ] && kill -KILL "-$TUNNEL_PID" 2>/dev/null
[ -n "$SSHD_PID" ] && kill -KILL "-$SSHD_PID" 2>/dev/null
# tidy up the relay: drop our (now-dead) socket + registration. Best-effort.
local dereg_ok=0
[ -f "$WORKDIR/clientkey" ] && relay_ssh deregister "$SID" >/dev/null 2>&1 && dereg_ok=1
# THE REVOKE is the local endpoint being down — that, not the relay socket, is the access path.
# Confirm it (retrying a harder kill if it is somehow still bound); only claim "revoked" if CONFIRMED.
if [ "$(port_bound "$SSHD_PORT")" = yes ]; then
warn "the local endpoint port $SSHD_PORT is still bound — killing harder"
[ -f "$WORKDIR/sshd.pid" ] && kill -KILL "$(cat "$WORKDIR/sshd.pid" 2>/dev/null)" 2>/dev/null; sleep 0.3
fi
case "$(port_bound "$SSHD_PORT")" in
yes) warn "could NOT confirm the endpoint is down. If unsure, reboot — nothing survives a reboot." ;;
no)
ok "access revoked — the support endpoint is down; only a reboot-proof, no-listener state remains."
[ "$dereg_ok" = 1 ] && info "relay entry removed." || info "the relay entry points to a now-dead endpoint and will be auto-cleaned." ;;
*) # neither `ss` nor a /dev/tcp probe could check — be HONEST, never claim a revoke we can't confirm
ok "support endpoint shut down — its sshd + tunnel were killed. Could NOT independently confirm the port is closed on this box; a reboot makes it certain (nothing survives a reboot)."
[ "$dereg_ok" = 1 ] && info "relay entry removed." || info "the relay entry points to a now-dead endpoint and will be auto-cleaned." ;;
esac
# the disclosure: did anything change?
if [ -n "$BEFORE" ] && [ -f "$BEFORE" ]; then
AFTER="$WORKDIR/after.txt"; snapshot_surface "$AFTER"
if diff -q "$BEFORE" "$AFTER" >/dev/null 2>&1; then
ok "verified: your SSH keys, enabled services, and scheduled jobs are unchanged."
else
warn "something on this box's access surfaces CHANGED while your helper was connected:"
say "${D} (A '+' line is something ADDED. The one that matters most is a new '+' under${X}"
say "${D} '## authorized_keys' — that's a key that could let someone back in later. If you${X}"
say "${D} didn't expect a change, ask your helper what it was; the full record is saved below.)${X}"
diff -u "$BEFORE" "$AFTER" 2>/dev/null | grep -E '^[+-]' | grep -vE '^(\+\+\+|---)' | sed 's/^/ /'
# preserve the evidence (and the recording) outside tmpfs so it survives the wipe
local keep="$HOME/.floo-last-session"; mkdir -p "$keep" 2>/dev/null
cp "$BEFORE" "$AFTER" "$keep/" 2>/dev/null
cp -r "$WORKDIR/recording" "$keep/" 2>/dev/null
warn "details + the full recording saved to: $keep"
fi
fi
if ls "$WORKDIR"/recording/*.raw >/dev/null 2>&1; then
local keep="$HOME/.floo-last-session"; mkdir -p "$keep/recording" 2>/dev/null
cp "$WORKDIR"/recording/*.raw "$keep/recording/" 2>/dev/null # the raw, complete record
clean_recordings "$keep/recording" # renders the readable .log
local rec; rec="$(ls -1t "$keep"/recording/*.log 2>/dev/null | head -1)"
[ -n "$rec" ] && info "readable session log: ${B}$rec${X} ${D}(complete raw recording: $keep/recording/*.raw)${X}"
fi
rm -rf "$WORKDIR" 2>/dev/null # wipe the ephemeral keys + config
[ -n "$RENDER_PY" ] && rm -f "$RENDER_PY" 2>/dev/null # the renderer's temp script
ok "done. nothing is left running; a reboot would change nothing."
}
# ─── Close a session from ANOTHER terminal — the recovery path when the original window/connection ───
# was lost (so there's no Ctrl-C to press). Normally a dropped SSH delivers SIGHUP and floo tears down
# on its own; this handles the cases where it didn't (tmux/screen/nohup, or SIGKILL) and the endpoint
# is still up. Reboot remains the guaranteed backstop — nothing here survives one.
stop_session() {
local dir; dir="$(session_dir "$NAME")"
[ -d "$dir" ] || { info "no support session for '$NAME' is open — nothing to close."; exit 0; }
local mp; mp="$(cat "$dir/floo.pid" 2>/dev/null)"
if [ -n "$mp" ] && kill -0 "$mp" 2>/dev/null; then
# the floo that opened it is still running — signal it so IT runs the full teardown (revoke +
# port-down confirm + access-surface diff + recording), then wait for it to wipe the session dir.
info "closing the running support session for '$NAME'…"
kill -TERM "$mp" 2>/dev/null
local _; for _ in $(seq 1 20); do [ -d "$dir" ] || break; sleep 0.3; done
[ -d "$dir" ] && { warn "it hasn't closed yet — give it a moment, or reboot (nothing survives a reboot)."; exit 1; }
ok "support session closed — access revoked."; exit 0
fi
# orphaned: the owning floo is gone but its sshd/tunnel may still be up. Reconstruct the handles from
# the session dir and run the SAME teardown (kills the endpoint + tunnel, confirms the port is down,
# shows the before/after access-surface diff, saves the recording, wipes the dir).
warn "the floo that opened this session is gone (orphaned) — tearing it down now…"
WORKDIR="$dir"; BEFORE="$dir/before.txt"
SID="$(cat "$dir/principals" 2>/dev/null)"
SSHD_PID="$(cat "$dir/sshd.pid" 2>/dev/null)"
TUNNEL_PID="$(cat "$dir/tunnel.pid" 2>/dev/null)"
teardown
}
run_session() {
command -v ssh >/dev/null && command -v ssh-keygen >/dev/null && [ -x "$SSHD_BIN" ] || {
warn "this box has no SSH server installed, which floo needs to let your helper in. Ask whoever is"
warn "helping you, or install it: 'sudo apt install openssh-server' (Debian/Ubuntu) /"
warn "'sudo dnf install openssh-server' (Fedora/RHEL) / 'sudo pacman -S openssh' (Arch), then re-run."
exit 1; }
[ "$(id -u)" = 0 ] && warn "you are running as root; support is designed to run as your own user."
say ""
say "${B}floo${X} ${D}v$FLOO_VERSION${X} — temporary, recorded help for ${B}$NAME${X}"
say "${D}Access lasts only while this window is open. Ctrl-C ends it. Nothing is installed.${X}"
say ""
WORKDIR="$(session_dir "$NAME")"
# refuse a pre-planted symlink (a local attacker redirecting our key/recording writes), both
# before and after creation (umask 077 already makes the dirs owner-only).
[ -L "$WORKDIR" ] && { warn "session dir $WORKDIR is a symlink — refusing."; exit 1; }
rm -rf "$WORKDIR"; mkdir -p "$WORKDIR/recording" || { warn "cannot create session dir."; exit 1; }
[ -L "$WORKDIR" ] && { warn "session dir became a symlink — refusing."; exit 1; }
chmod 700 "$(dirname "$WORKDIR")" "$WORKDIR"
printf '%s\n' "$$" > "$WORKDIR/floo.pid" # the main process to signal from `floo --stop` (another terminal)
BEFORE="$WORKDIR/before.txt"; snapshot_surface "$BEFORE"
build_endpoint
start_sshd || { teardown; exit 1; }
pairing_code
register_and_tunnel || { teardown; exit 1; }
say ""
if [ "$FLOO_PUBLIC" = 1 ]; then
ok "your box is now reachable — and the code below is the ONLY thing that lets anyone in."
say ""
say " ${B}Give this code ONLY to the person you want to help you:${X} ${B}$PAIRCODE${X}"
say " ${D}Anyone who learns this code can connect. It's long on purpose. Don't post it anywhere;${X}"
say " ${D}read it to one person you trust. Close this window the moment you're done.${X}"
else
ok "your box is now reachable by the person helping you — and ONLY by their published key."
say ""
say " ${B}Read this code back to whoever is helping you:${X} ${B}$PAIRCODE${X}"
say " ${D}They must repeat it before connecting. If their copy doesn't match, do NOT proceed —${X}"
say " ${D}it means someone else is trying to answer. Just close this window.${X}"
fi
say ""
info "your helper's commands will appear below as they work. Ctrl-C ends the session."
say ""
if [ "$FLOO_PUBLIC" = 1 ]; then bind_watcher & BINDWATCH_PID=$!; fi
render_console & MONITOR_PID=$!
# block until the tunnel or endpoint dies, or the user interrupts