package main import ( "crypto/tls" "flag" "fmt" "net" "os" "strings" "time" ) // cmdListen runs clonetool in listen mode: it waits for inbound TLS // connections (see nettransport.go), authenticates each with the shared // password, and serves whatever role the peer requests (control for the // manager's stat calls, source-stream/sink for the data transfer). This is the // no-ssh path for a host — typically a Windows source — that this machine and // the destination connect *to* instead of being reached over ssh. func cmdListen(args []string) error { fs := flag.NewFlagSet("listen", flag.ContinueOnError) bind := fs.String("bind", ":9000", "address to listen on, e.g. :9000 or 0.0.0.0:9000") password := fs.String("password", "", "shared password (or set CLONETOOL_PASSWORD, or --password-file)") passwordFile := fs.String("password-file", "", "read the shared password from this file") if err := fs.Parse(args); err != nil { return err } pw, err := resolvePassword(*password, *passwordFile) if err != nil { return err } if pw == "" { return fmt.Errorf("listen: a password is required (--password, --password-file, or CLONETOOL_PASSWORD)") } tlsCfg, err := newServerTLSConfig() if err != nil { return fmt.Errorf("listen: tls setup: %w", err) } ln, err := net.Listen("tcp", *bind) if err != nil { return fmt.Errorf("listen on %s: %w", *bind, err) } defer ln.Close() fmt.Fprintf(os.Stderr, "clonetool listening on %s (%s); press Ctrl-C to stop\n", ln.Addr(), versionString()) for { raw, err := ln.Accept() if err != nil { return fmt.Errorf("accept: %w", err) } go serveListenConn(raw, tlsCfg, pw) } } // serveListenConn handles one accepted connection: TLS + password auth, then a // single role request that it dispatches. Each connection is one role; the // manager and the destination open separate connections. func serveListenConn(raw net.Conn, tlsCfg *tls.Config, password string) { remote := raw.RemoteAddr().String() defer raw.Close() conn := tls.Server(raw, tlsCfg) const handshakeTimeout = 30 * time.Second _ = conn.SetDeadline(time.Now().Add(handshakeTimeout)) if err := conn.Handshake(); err != nil { fmt.Fprintf(os.Stderr, "listen: %s: tls handshake: %v\n", remote, err) return } fr, fw, err := serverAuth(conn, password, handshakeTimeout) if err != nil { fmt.Fprintf(os.Stderr, "listen: %s: %v\n", remote, err) return } _ = conn.SetDeadline(time.Time{}) // transfers set their own pacing req, err := readCtrlFrame(fr) if err != nil { fmt.Fprintf(os.Stderr, "listen: %s: read role request: %v\n", remote, err) return } if req.Type != msgRole { _ = fw.WriteJSON(CtrlMsg{Type: msgRoleErr, Message: fmt.Sprintf("expected a role request, got %q", req.Type)}) return } if err := dispatchRole(req, fr, fw, remote); err != nil { fmt.Fprintf(os.Stderr, "listen: %s: role %s: %v\n", remote, req.Role, err) } } func dispatchRole(req CtrlMsg, fr *FrameReader, fw *FrameWriter, remote string) error { switch req.Role { case roleControl: fmt.Fprintf(os.Stderr, "listen: %s: control session\n", remote) return runControlAgent(fr, fw) case roleSourceStream: fmt.Fprintf(os.Stderr, "listen: %s: source-stream %s\n", remote, req.Path) return runSourceStreamRole(fr, fw, req.Path, req.Base, req.Size, req.BlockSize) case roleSink: fmt.Fprintf(os.Stderr, "listen: %s: sink %s\n", remote, req.Path) return runSinkRole(fr, fw, req.Path, req.Base, req.Size, req.BlockSize) default: _ = fw.WriteJSON(CtrlMsg{Type: msgRoleErr, Message: fmt.Sprintf("unknown role %q", req.Role)}) return fmt.Errorf("unknown role %q", req.Role) } } // resolvePassword picks the shared password from, in order: --password-file, an // inline --password, then the CLONETOOL_PASSWORD environment variable. A // trailing newline in a password file is stripped. func resolvePassword(inline, file string) (string, error) { if file != "" { b, err := os.ReadFile(file) if err != nil { return "", fmt.Errorf("read password file: %w", err) } return strings.TrimRight(string(b), "\r\n"), nil } if inline != "" { return inline, nil } return os.Getenv("CLONETOOL_PASSWORD"), nil }