|
package main |
|
|
|
import ( |
|
"fmt" |
|
"os" |
|
"os/exec" |
|
"slices" |
|
"strings" |
|
"syscall" |
|
"unsafe" |
|
|
|
"github.com/buildkite/shellwords" |
|
) |
|
|
|
func main() { |
|
if err := os.Chdir("/"); err != nil { |
|
fmt.Fprintf(os.Stderr, "%s: %v\n", os.Args[0], err) |
|
os.Exit(126) |
|
} |
|
var st syscall.Stat_t |
|
if err := syscall.Stat("/proc/self/exe", &st); err != nil { |
|
fmt.Fprintf(os.Stderr, "%s: failed to stat wrapper: %v\n", os.Args[0], err) |
|
os.Exit(126) |
|
} |
|
if st.Mode&^0o0100550 != 0 { // S_IFREG | S_IWUSR | S_IRUSR | S_IXUSR | S_IRGRP | S_IXGRP |
|
fmt.Fprintf(os.Stderr, "%s: wrapper permissions are too open: must not be greater than -r-xr-x--- (550)\n", os.Args[0]) |
|
os.Exit(126) |
|
} |
|
if err := usecap(2); err != nil { // CAP_DAC_READ_SEARCH |
|
fmt.Fprintf(os.Stderr, "%s: failed to use capability CAP_DAC_READ_SEARCH: %v\n", os.Args[0], err) |
|
os.Exit(126) |
|
} |
|
if cmd := os.Getenv("SSH_ORIGINAL_COMMAND"); cmd == "" { |
|
fmt.Fprintf(os.Stderr, "%s: must be run over ssh\n", os.Args[0]) |
|
os.Exit(126) |
|
} else if err := rrsync(cmd); err != nil { |
|
fmt.Fprintf(os.Stderr, "%s: %v (SSH_ORIGINAL_COMMAND: %q)\n", os.Args[0], err, cmd) |
|
os.Exit(126) |
|
} |
|
} |
|
|
|
func rrsync(cmd string) error { |
|
// based on https://download.samba.org/pub/rsync/rrsync.1 |
|
argv, err := shellwords.SplitPosix(cmd) |
|
if err != nil { |
|
return err |
|
} |
|
if len(argv) <= 0 || argv[0] != "rsync" { |
|
return fmt.Errorf("only rsync is allowed") |
|
} |
|
if len(argv) <= 1 || argv[1] != "--server" { |
|
return fmt.Errorf("only rsync server is allowed") |
|
} |
|
if len(argv) <= 2 || argv[2] != "--sender" { |
|
return fmt.Errorf("sending to read-only server is not allowed") |
|
} |
|
if dotarg := slices.Index(argv, "."); dotarg >= 0 { |
|
argv = slices.Insert(argv, dotarg, "--") // forcefully terminate option parsing |
|
} else { |
|
return fmt.Errorf("rsync server command line missing dot argument") |
|
} |
|
for _, arg := range argv[3:] { |
|
if arg == "--" { |
|
break |
|
} |
|
if arg[0] != '-' { |
|
continue |
|
} |
|
if arg[1] != '-' { |
|
const ( |
|
stateOption = iota |
|
stateNumber |
|
stateE |
|
) |
|
var state int |
|
opt: |
|
for i, opt := range []byte(arg) { |
|
if i == 0 { |
|
continue |
|
} |
|
switch state { |
|
case stateOption: |
|
switch { |
|
case slices.Contains([]byte("ACDEHIJKLNORSUWXbcdgklmnopqrstuvxyz"), opt): // short options without args |
|
if opt != 's' { |
|
continue opt |
|
} |
|
case slices.Contains([]byte("@B"), opt): // short options with number |
|
state = stateNumber |
|
continue opt |
|
case opt == 'e': // short e option (value: [0-9]*[.].*) |
|
state = stateE |
|
continue opt |
|
} |
|
return fmt.Errorf("option -%c is not allowed", opt) |
|
case stateNumber: |
|
if opt >= '0' && opt <= '9' { |
|
continue opt |
|
} |
|
case stateE: |
|
if opt >= '0' && opt <= '9' { |
|
continue opt |
|
} |
|
if opt == '.' { |
|
break opt |
|
} |
|
} |
|
return fmt.Errorf("invalid argument %s", arg) |
|
} |
|
continue |
|
} |
|
switch tmp, _, _ := strings.Cut(arg[2:], "="); tmp { |
|
case "append", "backup-dir", "block-size", "bwlimit", "checksum-choice", |
|
"checksum-seed", "compare-dest", "compress-choice", "compress-level", |
|
"copy-dest", "copy-unsafe-links", "debug", "delay-updates", "delete", |
|
"delete-after", "delete-before", "delete-delay", "delete-during", |
|
"delete-excluded", "delete-missing-args", "existing", "fake-super", |
|
"files-from", "force", "from0", "fsync", "fuzzy", "group", "groupmap", |
|
"hard-links", "iconv", "ignore-errors", "ignore-existing", |
|
"ignore-missing-args", "ignore-times", "info", "inplace", "link-dest", |
|
"links", "list-only", "log-file", "log-format", "max-alloc", "max-delete", |
|
"max-size", "min-size", "mkpath", "modify-window", "msgs2stderr", |
|
"munge-links", "new-compress", "no-W", "no-implied-dirs", "no-msgs2stderr", |
|
"no-r", "no-relative", "no-specials", "numeric-ids", "old-compress", |
|
"one-file-system", "only-write-batch", "open-noatime", "owner", |
|
"partial", "partial-dir", "perms", "preallocate", "recursive", |
|
"remove-sent-files", "remove-source-files", "safe-links", "sender", |
|
"server", "size-only", "skip-compress", "specials", "stats", "stderr", |
|
"suffix", "super", "temp-dir", "timeout", "times", "use-qsort", "usermap": |
|
default: |
|
return fmt.Errorf("option %s is not allowed", arg) |
|
} |
|
if arg == "--log-file" || strings.HasPrefix(arg, "--delete") || strings.HasPrefix(arg, "--remove") { |
|
return fmt.Errorf("option %s is not allowed on read-only server", arg) |
|
} |
|
} |
|
rsync, err := exec.LookPath("rsync") |
|
if err != nil { |
|
return err |
|
} |
|
return syscall.Exec(rsync, argv, os.Environ()) |
|
} |
|
|
|
func usecap(cap uint) error { |
|
// https://man7.org/linux/man-pages/man2/capset.2.html |
|
var caps struct { |
|
hdr struct { |
|
version uint32 |
|
pid int |
|
} |
|
data [2]struct { |
|
effective uint32 |
|
permitted uint32 |
|
inheritable uint32 |
|
} |
|
} |
|
caps.hdr.version = 0x20080522 // _LINUX_CAPABILITY_VERSION_3 |
|
if _, _, errno := syscall.Syscall(syscall.SYS_CAPGET, uintptr(unsafe.Pointer(&caps)), uintptr(unsafe.Pointer(&caps.data[0])), 0); errno != 0 { |
|
return fmt.Errorf("capget: %w", errno) |
|
} |
|
if caps.data[0].permitted&(1<<cap) == 0 { |
|
return fmt.Errorf("capability %d is not permitted (try setcap %d+p ...)", cap, cap) |
|
} |
|
caps.data[0].effective |= 1 << cap |
|
caps.data[0].inheritable |= 1 << cap |
|
if _, _, errno := syscall.Syscall(syscall.SYS_CAPSET, uintptr(unsafe.Pointer(&caps.hdr)), uintptr(unsafe.Pointer(&caps.data[0])), 0); errno != 0 { |
|
return fmt.Errorf("capset: %w", errno) |
|
} |
|
if _, _, errno := syscall.Syscall(syscall.SYS_PRCTL, 47, 2, uintptr(cap)); errno != 0 { |
|
return fmt.Errorf("prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_RAISE, %d): %w", cap, errno) |
|
} |
|
return nil |
|
} |