release: 0.7.2 — crate replicate (ZFS storage replication via ssh)#140
Merged
release: 0.7.2 — crate replicate (ZFS storage replication via ssh)#140
Conversation
`crate replicate <jail> --to [user@]host --dest-dataset <ds>`
takes a fresh snapshot and streams `zfs send | ssh ... 'zfs recv'`.
Supports incremental sends (--since / --auto-incremental). All SSH
transport options are operator-controllable.
crate replicate myjail \
--to backup@dr.example.com \
--dest-dataset tank/jails/myjail \
--auto-incremental \
--ssh-port 2222 \
--ssh-key /root/.ssh/id_ed25519 \
--ssh-opt StrictHostKeyChecking=accept-new \
--ssh-opt ConnectTimeout=30
SSH knobs:
- --ssh-port N non-default port (passed via -p)
- --ssh-key PATH identity file (passed via -i)
- --ssh-config PATH custom ssh_config (passed via -F)
- --ssh-opt KEY=VAL repeatable; passed verbatim as `-o KEY=VAL`
`--ssh-opt` is the deliberate escape hatch — we don't enumerate
every OpenSSH option in the CLI. StrictHostKeyChecking,
UserKnownHostsFile, ProxyJump, ConnectTimeout, ... all thread
through.
Defaults set automatically: BatchMode=yes (no password prompts in
cron), ServerAliveInterval=30 (keeps long sends alive across
firewalls). Both can be overridden via --ssh-opt.
Defense in depth: every input passes through pure validators
before any filesystem/process touchpoint:
- --to: [user@]host — user alnum+._-, host IPv4 (with octet
bounds — 256.0.0.1 rejected) or RFC 1123 hostname
- --dest-dataset: alnum+._-/, no //, no .|.., ZFS alphabet
- --ssh-opt KEY=VAL: KEY alnum (<=64), VAL rejects whitespace,
control chars, shell metas (values are passed unquoted to ssh)
- --ssh-key / --ssh-config: absolute, no .., no metas
Even with a compromised spec, worst case is a validate-style
error — never `ssh host '...; rm -rf /'`.
Implementation:
- lib/replicate_pure.{h,cpp}: pure SSH-side helpers with the
IPv4-shape fix from 0.6.10 reused; argv builders thread the
--ssh-* flags into the right ssh positions; buildReplicationPipeline
returns the two-stage argv-list for Util::execPipeline.
- lib/replicate.cpp: jail -> dataset, latest backup-* via
zfs list (auto-incremental), fresh snapshot, run pipeline.
Reuses BackupPure::choosePlan + snapshotSuffix from 0.7.0.
- cli/args.cpp + main.cpp: CmdReplicate + usageReplicate, all
--ssh-* flags wired.
- lib/audit*.cpp: recorded; target is
<jail>-><[user@]host>:<dest-dataset>.
Tests: +15 ATF cases in replicate_pure_test.cpp covering remote
parse incl. all the edge cases (empty user/host, IPv4 octet
bounds, shell metas, underscore-hostname rejection), ssh-opt
validation incl. control chars and shell metas in VALUE,
ssh-key path validation, dest-dataset alphabet incl. //, .|..
rejection, ssh-argv shape with defaults present, full options
stitched in correct positions, pipeline two-stage shape with
correct -i prev curr in incremental case. 15/15 verified locally.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
crate replicate <jail> --to [user@]host --dest-dataset <ds>— takes a fresh snapshot of a jail's dataset and streams it to a remote host viazfs send | ssh ... 'zfs recv'. Supports incremental sends (--since/--auto-incremental).crate replicate myjail \ --to backup@dr.example.com \ --dest-dataset tank/jails/myjail \ --auto-incremental \ --ssh-port 2222 \ --ssh-key /root/.ssh/id_ed25519 \ --ssh-opt StrictHostKeyChecking=accept-new \ --ssh-opt ConnectTimeout=30SSH transport — what operators can drive
--ssh-port N--ssh-key PATH-i)--ssh-config PATHssh_config(-F)--ssh-opt KEY=VAL-o KEY=VAL--ssh-optis the deliberate escape hatch —StrictHostKeyChecking,UserKnownHostsFile,ProxyJump,ConnectTimeout, etc. all thread through.Defaults:
BatchMode=yes(no password prompts in cron),ServerAliveInterval=30(long sends stay alive across firewalls). Both overridable via--ssh-opt.Defense in depth
Every input passes through pure validators before any filesystem/process touchpoint:
--to— user must be alnum +._-; host must be IPv4 with octet bounds (256.0.0.1rejected) or RFC 1123 hostname; shell metacharacters rejected--dest-dataset— ZFS alphabet only (alnum +._-/), no//, no./..segments--ssh-opt KEY=VAL— KEY alnum (≤64), VAL rejects whitespace, control chars, and shell metacharacters (values are passed unquoted to ssh)--ssh-key/--ssh-config— absolute paths, no.., no metasEven with a compromised spec, worst case is a validate-style error — never
ssh host '...; rm -rf /'.Implementation
lib/replicate_pure.{h,cpp}— pure SSH-side helpers (with the IPv4-shape fix from 0.6.10 reused), argv builders threading--ssh-*flags into correct ssh positions,buildReplicationPipelineforUtil::execPipeline.lib/replicate.cpp— runtime: jail → dataset, latestbackup-*for--auto-incremental, fresh snapshot, run pipeline. ReusesBackupPure::choosePlan+snapshotSuffixfrom 0.7.0.cli/args.cpp+main.cpp—CmdReplicate,usageReplicate(), all flags wired.lib/audit*.cpp— recorded; target<jail>-><[user@]host>:<dest-dataset>.Tests
tests/unit/replicate_pure_test.cpp— 15 ATF cases:validateSshRemote(typical/invalid incl. octet bounds, underscore-hostname, shell metas)parseSshRemoteuser/host splitvalidateSshOpt(typical incl. real-world ssh options; invalid incl. ws, ctrl chars, metas)validateSshKey(typical/invalid)validateDestDataset(typical/invalid://,./..,:, metas)buildSshArgv(minimal w/ defaults, full options, no-user)buildRemoteRecvCommandbuildReplicationPipeline(full + incremental-i prev curr)Verified locally: 15/15.
Test plan
kyua test replicate_pure_test— 15/15TODO— replicate moved to Donehttps://claude.ai/code/session_01X6t6tzVypHye5bDGLxzmZK
Generated by Claude Code