clonetool/listen.go
2026-09-25 10:32:17 +02:00

123 lines
4.1 KiB
Go

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
}