Help us shape the future of Go! Take the 2026 Go Developer Survey

dmsgscp

package
v1.3.104 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 31, 2026 License: AGPL-3.0 Imports: 19 Imported by: 0

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

View Source
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.

View Source
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.

View Source
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.

View Source
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.

View Source
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

View Source
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

func ReadAck(r *bufio.Reader) ([]byte, error)

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

func ResolveSafePath(rootDir, name string) (string, error)

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 WriteAck

func WriteAck(w io.Writer) error

WriteAck writes a single ack byte (\x00) to the peer.

func WriteFatal

func WriteFatal(w io.Writer, msg string) error

WriteFatal writes a fatal-prefix error line to the peer and returns any I/O error. Format: \x02<msg>\n.

func WriteFileHeader

func WriteFileHeader(w io.Writer, mode os.FileMode, size int64, name string) error

WriteFileHeader emits a `C<mode> <size> <name>\n` line.

func WriteWarn

func WriteWarn(w io.Writer, msg string) error

WriteWarn writes a warning-prefix error line to the peer. Format: \x01<msg>\n. The peer is expected to surface this but continue.

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

func Dial(ctx context.Context, dmsgC *dmsg.Client, rPK cipher.PubKey, port uint16) (*Client, error)

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

func NewClient(conn net.Conn, remote cipher.PubKey) *Client

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) Close

func (c *Client) Close() error

Close releases the underlying stream.

func (*Client) Download

func (c *Client) Download(remotePath, localPath string) error

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

func (c *Client) SetIdleTimeout(d time.Duration)

SetIdleTimeout overrides the payload copy's no-progress deadline. A value <= 0 is ignored (the default stands). Call before Upload/Download.

func (*Client) Upload

func (c *Client) Upload(localPath, remotePath string) error

Upload pushes localPath to the remote host at remotePath. remotePath is interpreted relative to the host's rootDir; the basename of remotePath becomes the C-record's name.

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 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

func ReadHeader(r *bufio.Reader) (Header, error)

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

func NewHost(dmsgC *dmsg.Client, wl Whitelist, rootDir string) (*Host, error)

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

func (h *Host) ListenAndServe(ctx context.Context, port uint16) error

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) RootDir

func (h *Host) RootDir() string

RootDir returns the filesystem root the host serves from.

func (*Host) ServeListener

func (h *Host) ServeListener(ctx context.Context, lis net.Listener) error

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.

type Whitelist

type Whitelist = pty.Whitelist

Whitelist is an alias for pty.Whitelist so operators can pass the same whitelist instance to both daemons without re-importing. We deliberately don't define a new interface here — the contract is identical and the indirection would just confuse callers.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL