@@ -165,27 +165,42 @@ func printRemoteCapabilityPreflight(ctx context.Context, w io.Writer, cfg Config
165165 if len (tools ) == 0 {
166166 return
167167 }
168- var out string
169- var err error
170- if isWindowsNativeTarget (target ) {
171- out , err = runWindowsRemoteCapabilityPreflight (ctx , target , workdir , env , envFiles , tools )
172- } else if isWindowsWSL2Target (target ) {
173- out , err = runWSL2RemoteCapabilityPreflight (ctx , target , workdir , env , envFiles , tools )
174- } else {
175- out , err = runSSHCombinedOutput (ctx , target , remoteCapabilityPreflightCommand (workdir , env , envFiles , tools ))
176- }
177- if err != nil {
178- fmt .Fprintf (w , "remote preflight failed: %v\n " , err )
179- if strings .TrimSpace (out ) != "" {
180- fmt .Fprintf (w , "remote preflight output: %s\n " , strings .TrimSpace (out ))
168+ baseTools := make ([]string , 0 , len (tools ))
169+ rawSocketRequested := false
170+ for _ , tool := range tools {
171+ if tool == rawSocketPreflightTool {
172+ rawSocketRequested = true
173+ continue
181174 }
182- return
183- }
184- for _ , line := range strings .Split (strings .TrimSpace (out ), "\n " ) {
185- if strings .TrimSpace (line ) != "" {
186- fmt .Fprintf (w , "remote preflight %s\n " , strings .TrimSpace (line ))
175+ baseTools = append (baseTools , tool )
176+ }
177+ if len (baseTools ) > 0 {
178+ var out string
179+ var err error
180+ if isWindowsNativeTarget (target ) {
181+ out , err = runWindowsRemoteCapabilityPreflight (ctx , target , workdir , env , envFiles , baseTools )
182+ } else if isWindowsWSL2Target (target ) {
183+ out , err = runWSL2RemoteCapabilityPreflight (ctx , target , workdir , env , envFiles , baseTools )
184+ } else {
185+ out , err = runSSHCombinedOutput (ctx , target , remoteCapabilityPreflightCommand (workdir , env , envFiles , baseTools ))
186+ }
187+ if err != nil {
188+ fmt .Fprintf (w , "remote preflight failed: %v\n " , err )
189+ if strings .TrimSpace (out ) != "" {
190+ fmt .Fprintf (w , "remote preflight output: %s\n " , strings .TrimSpace (out ))
191+ }
192+ } else {
193+ for _ , line := range strings .Split (strings .TrimSpace (out ), "\n " ) {
194+ if strings .TrimSpace (line ) != "" {
195+ fmt .Fprintf (w , "remote preflight %s\n " , strings .TrimSpace (line ))
196+ }
197+ }
187198 }
188199 }
200+ if rawSocketRequested {
201+ state := runRawSocketCapabilityPreflight (ctx , target , workdir , env , envFiles )
202+ fmt .Fprintf (w , "remote preflight %s=%s\n " , rawSocketPreflightTool , state )
203+ }
189204}
190205
191206func printDelegatedPreflightUnsupported (w io.Writer , provider string ) {
@@ -501,31 +516,175 @@ type preflightToolSpec struct {
501516 OS map [string ]bool
502517}
503518
519+ const (
520+ rawSocketPreflightTool = "raw_socket"
521+ rawSocketPreflightTimeout = 30 * time .Second
522+ rawSocketProbePrefix = "__crabbox_raw_socket_v1__:"
523+ rawSocketProbeDirect = rawSocketProbePrefix + "direct"
524+ rawSocketProbeSudo = rawSocketProbePrefix + "sudo"
525+ rawSocketProbeUnavailable = rawSocketProbePrefix + "unavailable"
526+ rawSocketProbeMissing = rawSocketProbePrefix + "probe_missing"
527+ )
528+
529+ const rawSocketPythonProbe = `import sys
530+ sys.path = [entry for entry in sys.path if entry not in ("", ".")]
531+ import errno
532+ import socket
533+ try:
534+ probe = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_RAW)
535+ probe.close()
536+ except socket.error as exc:
537+ if getattr(exc, "errno", None) in (errno.EPERM, errno.EACCES):
538+ sys.exit(77)
539+ sys.exit(78)
540+ except BaseException:
541+ sys.exit(78)`
542+
504543var preflightToolRegistry = map [string ]preflightToolSpec {
505- "apt" : {Posix : []string {"apt-get" , "--version" }, OS : map [string ]bool {"linux" : true }},
506- "bubblewrap" : {Posix : []string {"bwrap" , "--version" }, OS : map [string ]bool {"linux" : true }},
507- "bun" : {Posix : []string {"bun" , "--version" }, Windows : []string {"bun" , "--version" }},
508- "bwrap" : {Posix : []string {"bwrap" , "--version" }, OS : map [string ]bool {"linux" : true }},
509- "cargo" : {Posix : []string {"cargo" , "--version" }, Windows : []string {"cargo" , "--version" }},
510- "corepack" : {Posix : []string {"corepack" , "--version" }, Windows : []string {"corepack" , "--version" }},
511- "docker" : {Posix : []string {"docker" , "--version" }, Windows : []string {"docker" , "--version" }},
512- "execution_policy" : {Windows : []string {"Get-ExecutionPolicy -Scope Process" }, OS : map [string ]bool {"windows" : true }},
513- "git" : {Posix : []string {"git" , "--version" }, Windows : []string {"git" , "--version" }},
514- "go" : {Posix : []string {"go" , "version" }, Windows : []string {"go" , "version" }},
515- "longpaths" : {Windows : []string {"git config --global --get core.longpaths" }, OS : map [string ]bool {"windows" : true }},
516- "make" : {Posix : []string {"make" , "--version" }},
517- "node" : {Posix : []string {"node" , "--version" }, Windows : []string {"node" , "--version" }},
518- "npm" : {Posix : []string {"npm" , "--version" }, Windows : []string {"npm" , "--version" }},
519- "pnpm" : {Posix : []string {"pnpm" , "--version" }, Windows : []string {"pnpm" , "--version" }},
520- "powershell" : {Windows : []string {"$PSVersionTable.PSVersion.ToString()" }, OS : map [string ]bool {"windows" : true }},
521- "python" : {Posix : []string {"python" , "--version" }, Windows : []string {"python" , "--version" }},
522- "python3" : {Posix : []string {"python3" , "--version" }, Windows : []string {"python3" , "--version" }},
523- "pwsh" : {Windows : []string {"pwsh" , "--version" }, OS : map [string ]bool {"windows" : true }},
524- "sudo" : {OS : map [string ]bool {"linux" : true , "macos" : true }},
525- "tar" : {Posix : []string {"tar" , "--version" }, Windows : []string {"tar" , "--version" }},
526- "temp" : {Windows : []string {"$env:TEMP" }, OS : map [string ]bool {"windows" : true }},
527- "uv" : {Posix : []string {"uv" , "--version" }, Windows : []string {"uv" , "--version" }},
528- "yarn" : {Posix : []string {"yarn" , "--version" }, Windows : []string {"yarn" , "--version" }},
544+ "apt" : {Posix : []string {"apt-get" , "--version" }, OS : map [string ]bool {"linux" : true }},
545+ "bubblewrap" : {Posix : []string {"bwrap" , "--version" }, OS : map [string ]bool {"linux" : true }},
546+ "bun" : {Posix : []string {"bun" , "--version" }, Windows : []string {"bun" , "--version" }},
547+ "bwrap" : {Posix : []string {"bwrap" , "--version" }, OS : map [string ]bool {"linux" : true }},
548+ "cargo" : {Posix : []string {"cargo" , "--version" }, Windows : []string {"cargo" , "--version" }},
549+ "corepack" : {Posix : []string {"corepack" , "--version" }, Windows : []string {"corepack" , "--version" }},
550+ "docker" : {Posix : []string {"docker" , "--version" }, Windows : []string {"docker" , "--version" }},
551+ "execution_policy" : {Windows : []string {"Get-ExecutionPolicy -Scope Process" }, OS : map [string ]bool {"windows" : true }},
552+ "git" : {Posix : []string {"git" , "--version" }, Windows : []string {"git" , "--version" }},
553+ "go" : {Posix : []string {"go" , "version" }, Windows : []string {"go" , "version" }},
554+ "longpaths" : {Windows : []string {"git config --global --get core.longpaths" }, OS : map [string ]bool {"windows" : true }},
555+ "make" : {Posix : []string {"make" , "--version" }},
556+ "node" : {Posix : []string {"node" , "--version" }, Windows : []string {"node" , "--version" }},
557+ "npm" : {Posix : []string {"npm" , "--version" }, Windows : []string {"npm" , "--version" }},
558+ "pnpm" : {Posix : []string {"pnpm" , "--version" }, Windows : []string {"pnpm" , "--version" }},
559+ "powershell" : {Windows : []string {"$PSVersionTable.PSVersion.ToString()" }, OS : map [string ]bool {"windows" : true }},
560+ "python" : {Posix : []string {"python" , "--version" }, Windows : []string {"python" , "--version" }},
561+ "python3" : {Posix : []string {"python3" , "--version" }, Windows : []string {"python3" , "--version" }},
562+ "pwsh" : {Windows : []string {"pwsh" , "--version" }, OS : map [string ]bool {"windows" : true }},
563+ rawSocketPreflightTool : {OS : map [string ]bool {"linux" : true }},
564+ "sudo" : {OS : map [string ]bool {"linux" : true , "macos" : true }},
565+ "tar" : {Posix : []string {"tar" , "--version" }, Windows : []string {"tar" , "--version" }},
566+ "temp" : {Windows : []string {"$env:TEMP" }, OS : map [string ]bool {"windows" : true }},
567+ "uv" : {Posix : []string {"uv" , "--version" }, Windows : []string {"uv" , "--version" }},
568+ "yarn" : {Posix : []string {"yarn" , "--version" }, Windows : []string {"yarn" , "--version" }},
569+ }
570+
571+ const rawSocketSudoPATH = "/usr/local/bin:/usr/bin:/bin:/run/current-system/sw/bin:/nix/var/nix/profiles/default/bin:/run/current-system/profile/bin"
572+
573+ func rawSocketPreflightScript () string {
574+ return rawSocketPreflightScriptWithSudoEnvironment ([]string {
575+ "/usr/bin/sudo" ,
576+ "/bin/sudo" ,
577+ "/usr/local/bin/sudo" ,
578+ "/run/wrappers/bin/sudo" ,
579+ "/run/setuid-programs/sudo" ,
580+ }, rawSocketSudoPATH )
581+ }
582+
583+ func rawSocketPreflightScriptWithSudoEnvironment (sudoExecutables []string , sudoPath string ) string {
584+ probe := shellQuote (rawSocketPythonProbe )
585+ sudoCandidates := make ([]string , 0 , len (sudoExecutables ))
586+ for _ , sudo := range sudoExecutables {
587+ sudoCandidates = append (sudoCandidates , shellQuote (sudo ))
588+ }
589+ if len (sudoCandidates ) == 0 {
590+ sudoCandidates = append (sudoCandidates , "__crabbox_no_trusted_sudo__" )
591+ }
592+ sudoProbe := shellQuote (`PATH=` + shellQuote (sudoPath ) + `
593+ export PATH
594+ case "$1" in
595+ python3|python) ;;
596+ *) exit 79 ;;
597+ esac
598+ command -v "$1" >/dev/null 2>&1 || exit 79
599+ exec "$1" -B -E -S -c "$2"` )
600+ return `found_interpreter=0
601+ for interpreter_name in python3 python; do
602+ interpreter="$(command -v "$interpreter_name" 2>/dev/null || true)"
603+ if [ -z "$interpreter" ] || [ ! -x "$interpreter" ]; then
604+ continue
605+ fi
606+ found_interpreter=1
607+ direct_status=0
608+ "$interpreter" -B -E -S -c ` + probe + ` >/dev/null 2>&1 || direct_status=$?
609+ if [ "$direct_status" -eq 0 ]; then
610+ printf '` + rawSocketProbeDirect + `\n'
611+ exit 0
612+ fi
613+ if [ "$direct_status" -ne 77 ]; then
614+ continue
615+ fi
616+ # Resolve the same interpreter name only inside a fixed root-side PATH, never the workload PATH.
617+ for sudo_executable in ` + strings .Join (sudoCandidates , " " ) + `; do
618+ if [ ! -x "$sudo_executable" ]; then
619+ continue
620+ fi
621+ if "$sudo_executable" -n -- /bin/sh -c ` + sudoProbe + ` crabbox-raw-socket "$interpreter_name" ` + probe + ` >/dev/null 2>&1; then
622+ printf '` + rawSocketProbeSudo + `\n'
623+ exit 0
624+ fi
625+ done
626+ done
627+ if [ "$found_interpreter" -eq 0 ]; then
628+ printf '` + rawSocketProbeMissing + `\n'
629+ else
630+ printf '` + rawSocketProbeUnavailable + `\n'
631+ fi
632+ `
633+ }
634+
635+ func runRawSocketCapabilityPreflight (ctx context.Context , target SSHTarget , workdir string , env map [string ]string , envFiles []string ) string {
636+ command := rawSocketCapabilityPreflightCommand (workdir , env , envFiles )
637+ return runRawSocketCapabilityPreflightWithRunner (ctx , rawSocketPreflightTimeout , func (probeCtx context.Context ) (string , error ) {
638+ if isWindowsWSL2Target (target ) {
639+ return runWSL2ControlCombinedOutput (probeCtx , target , command )
640+ }
641+ return runSSHCombinedOutput (probeCtx , target , command )
642+ })
643+ }
644+
645+ func rawSocketCapabilityPreflightCommand (workdir string , env map [string ]string , envFiles []string ) string {
646+ return remoteShellCommandWithEnvFiles (workdir , env , envFiles , rawSocketPreflightScript ())
647+ }
648+
649+ func runRawSocketCapabilityPreflightWithRunner (ctx context.Context , timeout time.Duration , runner func (context.Context ) (string , error )) string {
650+ probeCtx , cancel := context .WithTimeout (ctx , timeout )
651+ defer cancel ()
652+ out , err := runner (probeCtx )
653+ if err != nil || probeCtx .Err () != nil {
654+ return "unavailable"
655+ }
656+ return parseRawSocketProbeOutput (out )
657+ }
658+
659+ func parseRawSocketProbeOutput (out string ) string {
660+ state := ""
661+ for _ , line := range strings .Split (out , "\n " ) {
662+ line = strings .TrimSpace (line )
663+ if ! strings .HasPrefix (line , rawSocketProbePrefix ) {
664+ continue
665+ }
666+ var next string
667+ switch line {
668+ case rawSocketProbeDirect :
669+ next = "direct"
670+ case rawSocketProbeSudo :
671+ next = "sudo"
672+ case rawSocketProbeMissing :
673+ next = "probe_missing"
674+ case rawSocketProbeUnavailable :
675+ next = "unavailable"
676+ default :
677+ return "unavailable"
678+ }
679+ if state != "" {
680+ return "unavailable"
681+ }
682+ state = next
683+ }
684+ if state == "" {
685+ return "unavailable"
686+ }
687+ return state
529688}
530689
531690var defaultPreflightToolNames = []string {"git" , "tar" , "node" , "npm" , "corepack" , "pnpm" , "yarn" , "bun" , "docker" , "sudo" , "apt" , "bubblewrap" , "powershell" , "execution_policy" , "longpaths" , "temp" , "pwsh" }
0 commit comments