Documentation
¶
Overview ¶
Package dmsgscp pkg/dmsg/dmsgscp/client.go c1-net-dmsg scp-over-dmsg. A Client wraps a single dmsg.Stream and exposes Download / Upload methods that drive the same wire framing the Host serves.
A Client is single-use by design: each transfer establishes a fresh dmsg stream, runs the framing end-to-end, and closes. This keeps the failure model simple — half-finished transfers can't pollute a long-lived stream — and matches how the CLI invokes it.
Package dmsgscp pkg/dmsg/dmsgscp/const.go c1-net-dmsg scp-over-dmsg utility — wire-protocol framing bytes, default dmsg port, and the configurable safety caps the host enforces on every transfer.
Package dmsgscp pkg/dmsg/dmsgscp/copyidle.go c1-net-dmsg
Package dmsgscp pkg/dmsg/dmsgscp/host.go c1-net-dmsg scp-over-dmsg. Host accepts dmsg streams on its configured port, authorizes the remote PK against an injected Whitelist, reads the first line to discover the role the caller wants the host to play (SOURCE = caller downloads, SINK = caller uploads), then drives the framing accordingly.
The structure deliberately mirrors pkg/dmsg/dmsgpty/host.go — same accept loop, same temporary-error retry, same close hooks. The big difference is the dispatch: dmsgpty multiplexes RPC endpoints over a single yamux-ish mux on each stream; dmsgscp uses each stream for a single file transfer end-to-end.
Package dmsgscp pkg/dmsg/dmsgscp/protocol.go c1-net-dmsg header parsing + serialization, plus the tiny ack helpers that drive the framing. The protocol mirrors OpenSSH's scp on-wire format — a single header line per record, a single ack byte after every header and every payload, and an in-band error prefix (\x01 warning, \x02 fatal) for human-readable failures.
We only implement records the dmsgscp Host/Client actually use:
C<octal-mode> <size> <name>\n - file record (followed by `size`
bytes of payload + ack)
D<octal-mode> 0 <dirname>\n - start of recursion (v1 rejects)
E\n - end of recursion (v1 rejects)
Path safety: the parser rejects any `..` component, absolute paths, embedded NUL/newline, and names beyond MaxNameLen — these are all explicit traversal/DoS guards rather than passive checks. Callers still resolve the final path against the host's rootDir via ResolveSafePath before touching the filesystem.
Index ¶
- Constants
- Variables
- func ReadAck(r *bufio.Reader) ([]byte, error)
- func ResolveSafePath(rootDir, name string) (string, error)
- func WriteAck(w io.Writer) error
- func WriteFatal(w io.Writer, msg string) error
- func WriteFileHeader(w io.Writer, mode os.FileMode, size int64, name string) error
- func WriteWarn(w io.Writer, msg string) error
- type Client
- type FatalError
- type Header
- type Host
- type RecordType
- type Whitelist
Constants ¶
const ( // AckByte is the single-byte acknowledgement the receiver sends // after a valid header or completed payload. Zero by design so a // raw read of \x00 from the peer means "go ahead". AckByte byte = 0x00 // WarnPrefix marks a non-fatal warning line — sender or receiver // may continue after consuming the line. WarnPrefix byte = 0x01 // FatalPrefix marks a fatal error line — the recipient must abort // the transfer once it consumes the line. FatalPrefix byte = 0x02 )
Wire-protocol framing bytes. The protocol is a stripped-down re-implementation of OpenSSH's scp wire format (the "rcp" subset) — a single ack byte after every header and every payload, and an error prefix for warnings vs fatal aborts.
const ( // RoleSource is sent by a client requesting a download — the // host plays the "source" role (reads file from disk, writes // to wire). RoleSource = "SOURCE" // RoleSink is sent by a client requesting an upload — the host // plays the "sink" role (reads payload from wire, writes to disk). RoleSink = "SINK" )
Role markers — the first line written on a freshly-dialed stream tells the Host whether the caller wants to pull (SOURCE) or push (SINK). SSH's classic role flag (`-f` / `-t` on the remote scp binary) doesn't map cleanly to a non-shell daemon, so we use an explicit line. The format is `<ROLE> <path>\n` with ROLE in `SOURCE` or `SINK` and `path` a relative path under the host's rootDir.
const ( // MaxNameLen caps the basename portion of a `C` or `D` header. // Long enough for any reasonable filename, short enough that a // malicious peer can't exhaust memory with a multi-megabyte // header line. MaxNameLen = 255 // MaxHeaderLen caps the entire header line (`C<mode> <size> <name>\n`). // 1 KiB is well above MaxNameLen + the fixed fields. MaxHeaderLen = 1024 )
Safety caps. Apply to the header parser only — there is no per-transfer file-size cap. The protocol streams payloads of any length the underlying transport accepts. MaxNameLen / MaxHeaderLen guard against memory exhaustion from a malicious peer sending a multi-megabyte header line; they have no effect on the payload.
const DefaultIdleTimeout = 60 * time.Second
DefaultIdleTimeout is the no-progress deadline used when a caller does not supply one: a transfer aborts only after this long with zero bytes moving. It bounds a genuine stall without capping the total duration of a healthy transfer of any size.
const ( // DefaultPort is the default dmsg port the dmsgscp Host listens // on. Operators may override via config. DefaultPort = uint16(23) )
Dmsg-port constants. We pick port 23 deliberately — adjacent to dmsgpty's port 22 — so the two services occupy a contiguous, easily-recognized pair on every visor that runs them.
Variables ¶
var ( // ErrShortHeader signals a header line shorter than the minimum // valid form (`C0 0 x`). ErrShortHeader = errors.New("dmsgscp: header too short") // ErrHeaderTooLong signals a header line above MaxHeaderLen. ErrHeaderTooLong = errors.New("dmsgscp: header line exceeds max length") // ErrUnknownRecord signals a leading byte we don't recognize. ErrUnknownRecord = errors.New("dmsgscp: unknown record type") // ErrBadMode signals an unparseable octal mode field. ErrBadMode = errors.New("dmsgscp: bad octal mode") // ErrBadSize signals an unparseable or out-of-range size field. ErrBadSize = errors.New("dmsgscp: bad size") // ErrEmptyName signals a missing or whitespace-only name. ErrEmptyName = errors.New("dmsgscp: empty name") // ErrNameTooLong signals a name above MaxNameLen. ErrNameTooLong = errors.New("dmsgscp: name exceeds max length") // ErrPathTraversal signals a name containing `..` or absolute paths. ErrPathTraversal = errors.New("dmsgscp: name contains path traversal") // ErrInvalidName signals a name with NUL or newline bytes. ErrInvalidName = errors.New("dmsgscp: name contains invalid characters") // ErrDirNotSupported is returned when a D/E record arrives — v1 // is files-only. ErrDirNotSupported = errors.New("dmsgscp: directory transfer not implemented in v1") )
Sentinel parser errors. Callers can use errors.Is to react to a specific failure mode (e.g. send a fatal-prefix line on the wire after ErrPathTraversal).
Functions ¶
func ReadAck ¶
ReadAck reads a single byte from the peer and returns:
(nil, nil) on a clean ack (\x00)
(warnMsg, nil) on a warning line (\x01 + msg + \n) — the
caller may continue but should surface
the message
(fatalMsg, errFatal) on a fatal line (\x02 + msg + \n) — the
caller must abort
On I/O error it returns (nil, err).
func ResolveSafePath ¶
ResolveSafePath joins rootDir with a wire-provided name and verifies the result stays under rootDir after symlink-unaware cleaning. Callers must NOT pass the result to filepath.EvalSymlinks before using it for I/O — the guard is purely structural.
Returns the cleaned absolute path or ErrPathTraversal.
func WriteFatal ¶
WriteFatal writes a fatal-prefix error line to the peer and returns any I/O error. Format: \x02<msg>\n.
func WriteFileHeader ¶
WriteFileHeader emits a `C<mode> <size> <name>\n` line.
Types ¶
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is a single-transfer dmsgscp client. Build one via Dial (dmsg path) or NewClient (any net.Conn, e.g. a skynet stream dialed inside the visor process where appnet is available).
The wire protocol is transport-agnostic; the Client holds a `net.Conn` so any stream-shaped transport can plug in. The dmsg Dial helper stays for callers that already have a `*dmsg.Client` in hand and don't need to think about transports.
func Dial ¶
Dial opens a dmsg.Stream to (rPK:port) and returns a Client ready to drive Upload/Download. The caller must Close the Client when done — Upload/Download don't close on success so the caller can chain (in practice, every CLI path closes immediately).
func NewClient ¶
NewClient wraps an already-dialed net.Conn into a dmsgscp Client. Used by callers that opened the stream over a non-dmsg transport (notably skynet via appnet.Dial, from inside the visor process). The remote PK is informational — it appears in error messages but the protocol itself doesn't depend on it.
func (*Client) Download ¶
Download pulls a file from the remote host's rootDir at remotePath and writes it to localPath. Existing files at localPath are overwritten.
func (*Client) SetIdleTimeout ¶
SetIdleTimeout overrides the payload copy's no-progress deadline. A value <= 0 is ignored (the default stands). Call before Upload/Download.
type FatalError ¶
type FatalError struct {
Msg string
}
FatalError wraps a fatal-prefix message from the peer. ReadAck returns one as its error so callers using errors.As can extract the original text.
func (*FatalError) Error ¶
func (e *FatalError) Error() string
Error implements the error interface.
type Header ¶
type Header struct {
Type RecordType
Mode os.FileMode // permission bits parsed from the octal field
Size int64 // payload size in bytes (0 for D/E)
Name string // basename, validated against traversal
}
Header is one parsed protocol record. For RecordDirEnd, only Type is meaningful; Mode/Size/Name are zero.
func ReadHeader ¶
ReadHeader reads one header record from r. It consumes up to (and including) the trailing newline. ErrHeaderTooLong is returned if the line would exceed MaxHeaderLen — the caller should treat that as fatal and abort the stream.
io.EOF is returned verbatim when the peer cleanly closes before sending a header — callers distinguish "no more records" from a malformed record by checking for io.EOF.
type Host ¶
type Host struct {
// contains filtered or unexported fields
}
Host is the listening side of dmsgscp.
func NewHost ¶
NewHost creates a new dmsgscp Host with the given dmsg client, whitelist, and filesystem root. rootDir is created with 0700 permissions if it does not already exist — the directory is the effective chroot for every served transfer.
func (*Host) ListenAndServe ¶
ListenAndServe binds the configured dmsg port and serves transfers until ctx is canceled or the listener fails permanently. The signature and accept-loop pattern intentionally mirror pty.Host.ListenAndServe — see that function's comments for the rationale on the temporary-error sleep and the close-pipe classification.
The accept loop body is shared with ServeListener so the same Host can also accept on a skynet listener for dual-transport parity.
func (*Host) ServeListener ¶
ServeListener runs the accept loop against a caller-provided listener. Used directly by the skynet-mirror goroutine in initDmsgpty so the dmsgscp Host serves over BOTH dmsg and the skywire router at the same port number. The listener is closed when ctx is canceled.
Authorization (whitelist check) happens per-stream and works uniformly across listener types because remoteAddrToPK handles dmsg.Addr plus both skynet shapes (appnet.Addr direct-transport + routing.Addr route-group).
type RecordType ¶
type RecordType byte
RecordType identifies a parsed protocol record.
const ( // RecordFile is a `C` (file) record. RecordFile RecordType = 'C' // RecordDirStart is a `D` (start-of-directory) record. v1 rejects. RecordDirStart RecordType = 'D' // RecordDirEnd is an `E` (end-of-directory) record. v1 rejects. RecordDirEnd RecordType = 'E' )
Supported record types.