123 lines
4.1 KiB
Go
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
|
|
}
|