Files
nfs/internal/rpc/message.go
petrbalvin a9b8039ef7
Test / test (push) Successful in 2m4s
Release / gates (push) Successful in 2m5s
Release / build (amd64, freebsd) (push) Successful in 1m27s
Release / build (amd64, linux) (push) Successful in 1m22s
Release / build (amd64, netbsd) (push) Successful in 1m19s
Release / build (amd64, openbsd) (push) Successful in 1m20s
Release / build (arm64, darwin) (push) Successful in 1m21s
Release / build (arm64, freebsd) (push) Successful in 1m26s
Release / build (arm64, linux) (push) Successful in 1m25s
Release / build (arm64, netbsd) (push) Successful in 1m31s
Release / build (arm64, openbsd) (push) Successful in 1m27s
Release / build (loong64, linux) (push) Successful in 1m37s
Release / build (riscv64, linux) (push) Successful in 1m21s
Release / release (push) Successful in 40s
feat: full NFSv4.2 server and client in pure Go
Assisted-by: GLM 5.3 Flash
2026-09-21 18:51:17 +02:00

376 lines
10 KiB
Go

// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
package rpc
import (
"errors"
"fmt"
"sourcedock.dev/petrbalvin/nfs/internal/xdr"
)
// Version is the ONC RPC protocol version, fixed by RFC 5531.
const Version = 2
// Message types.
const (
MsgCall = 0
MsgReply = 1
)
// ProcedureNull is the NULL procedure every program reserves for
// control exchanges such as the RPCSEC_GSS context procedures of
// RFC 2203 section 5.1.3 and the STARTTLS probe of RFC 9289.
const ProcedureNull = 0
// Authentication flavors.
const (
FlavorNone = 0
FlavorSys = 1
FlavorShort = 2
)
// maxAuthBody is the largest opaque credential or verifier body the
// standard allows.
const maxAuthBody = 400
// ErrBadMessage is returned for a record that is not a well formed ONC RPC
// message.
var ErrBadMessage = errors.New("rpc: malformed message")
// An Auth is an opaque_auth: a flavor and its flavor defined body.
type Auth struct {
Flavor uint32
Body []byte
}
// AuthNull is the credential and verifier that carry nothing.
var AuthNull = Auth{}
func appendAuth(b []byte, a Auth) ([]byte, error) {
if len(a.Body) > maxAuthBody {
return nil, fmt.Errorf("rpc: auth body of %d bytes exceeds %d", len(a.Body), maxAuthBody)
}
b = xdr.AppendUint32(b, a.Flavor)
b = xdr.AppendVarOpaque(b, a.Body)
return b, nil
}
func decodeAuth(d *xdr.Decoder) (Auth, error) {
flavor, err := d.Uint32()
if err != nil {
return Auth{}, err
}
body, err := d.VarOpaque()
if err != nil {
return Auth{}, err
}
if len(body) > maxAuthBody {
return Auth{}, fmt.Errorf("rpc: auth body of %d bytes exceeds %d", len(body), maxAuthBody)
}
return Auth{Flavor: flavor, Body: body}, nil
}
// A Call is the header of an ONC RPC call. The procedure arguments follow
// the header in the same record.
type Call struct {
XID uint32
Program, Version, Procedure uint32
Cred, Verifier Auth
}
// AppendCall appends the call header to b. The caller appends the
// procedure arguments afterwards. Call.Version carries the program version
// the call targets; the ONC RPC protocol version is fixed at 2.
func AppendCall(b []byte, c Call) ([]byte, error) {
b = xdr.AppendUint32(b, c.XID)
b = xdr.AppendUint32(b, MsgCall)
b = xdr.AppendUint32(b, Version)
b = xdr.AppendUint32(b, c.Program)
b = xdr.AppendUint32(b, c.Version)
b = xdr.AppendUint32(b, c.Procedure)
var err error
if b, err = appendAuth(b, c.Cred); err != nil {
return nil, fmt.Errorf("rpc: credential: %w", err)
}
if b, err = appendAuth(b, c.Verifier); err != nil {
return nil, fmt.Errorf("rpc: verifier: %w", err)
}
return b, nil
}
// DecodeCall splits a record into its call header and the bytes that hold
// the procedure arguments.
func DecodeCall(record []byte) (Call, []byte, error) {
d := xdr.NewDecoder(record)
xid, err := d.Uint32()
if err != nil {
return Call{}, nil, ErrBadMessage
}
mtype, err := d.Uint32()
if err != nil {
return Call{}, nil, ErrBadMessage
}
if mtype != MsgCall {
return Call{}, nil, fmt.Errorf("%w: message type %d is not a call", ErrBadMessage, mtype)
}
var c Call
c.XID = xid
rpcvers, err := d.Uint32()
if err != nil {
return Call{}, nil, ErrBadMessage
}
if rpcvers != Version {
return Call{}, nil, fmt.Errorf("%w: rpc version %d, want %d", ErrBadMessage, rpcvers, Version)
}
if c.Program, err = d.Uint32(); err != nil {
return Call{}, nil, ErrBadMessage
}
if c.Version, err = d.Uint32(); err != nil {
return Call{}, nil, ErrBadMessage
}
if c.Procedure, err = d.Uint32(); err != nil {
return Call{}, nil, ErrBadMessage
}
if c.Cred, err = decodeAuth(d); err != nil {
return Call{}, nil, ErrBadMessage
}
if c.Verifier, err = decodeAuth(d); err != nil {
return Call{}, nil, ErrBadMessage
}
return c, record[len(record)-d.Remaining():], nil
}
// Accept statuses carried by an accepted reply.
const (
AcceptSuccess = 0
AcceptProgUnavail = 1
AcceptProgMismatch = 2
AcceptProcUnavail = 3
AcceptGarbageArgs = 4
AcceptSystemErr = 5
)
// Reject statuses carried by a rejected reply.
const (
RejectRPCMismatch = 0
RejectAuthError = 1
)
// Auth statistics of a rejected reply, RFC 5531 section 9 and
// RFC 2203 section 5.2.3.
const (
AuthBadCred = 1
AuthRejectedCred = 2
AuthBadVerf = 3
AuthRejectedVerf = 4
AuthTooWeak = 5
AuthInvalidResp = 6
AuthFailed = 7
AuthDenied = 8
AuthGSSCredProb = 14
AuthGSSCtxProb = 15
)
// A Mismatch carries the program version range a server accepts, sent when
// a call names a version the server does not.
type Mismatch struct {
Low, High uint32
}
// AppendAcceptedReply appends an accepted reply header. On success the
// caller appends the procedure results afterwards; on any other status the
// header carries the whole reply, and mismatch is read only when the status
// is AcceptProgMismatch.
func AppendAcceptedReply(b []byte, xid uint32, verifier Auth, status uint32, mismatch Mismatch) ([]byte, error) {
b = xdr.AppendUint32(b, xid)
b = xdr.AppendUint32(b, MsgReply)
b = xdr.AppendUint32(b, 0) // accepted
var err error
if b, err = appendAuth(b, verifier); err != nil {
return nil, fmt.Errorf("rpc: verifier: %w", err)
}
b = xdr.AppendUint32(b, status)
switch status {
case AcceptSuccess, AcceptProgUnavail, AcceptProcUnavail, AcceptGarbageArgs, AcceptSystemErr:
case AcceptProgMismatch:
b = xdr.AppendUint32(b, mismatch.Low)
b = xdr.AppendUint32(b, mismatch.High)
default:
return nil, fmt.Errorf("rpc: unknown accept status %d", status)
}
return b, nil
}
// A Reply is a decoded reply header. Body holds the procedure results
// when the status is AcceptSuccess. A reply the server rejected, with
// MSG_DENIED, carries Rejected set: Status stays zero and is
// meaningless there, AuthStat holds the auth error when the rejection
// is one, and Mismatch holds the version range on an RPC_MISMATCH.
type Reply struct {
XID uint32
Rejected bool
Status uint32
AuthStat uint32
Mismatch Mismatch
Verifier Auth
Body []byte
}
// DecodeReply splits a record into its reply header and the result bytes.
func DecodeReply(record []byte) (Reply, error) {
d := xdr.NewDecoder(record)
xid, err := d.Uint32()
if err != nil {
return Reply{}, ErrBadMessage
}
mtype, err := d.Uint32()
if err != nil {
return Reply{}, ErrBadMessage
}
if mtype != MsgReply {
return Reply{}, fmt.Errorf("%w: message type %d is not a reply", ErrBadMessage, mtype)
}
stat, err := d.Uint32()
if err != nil {
return Reply{}, ErrBadMessage
}
switch stat {
case 0: // accepted
r := Reply{XID: xid}
if r.Verifier, err = decodeAuth(d); err != nil {
return Reply{}, ErrBadMessage
}
if r.Status, err = d.Uint32(); err != nil {
return Reply{}, ErrBadMessage
}
switch r.Status {
case AcceptSuccess:
r.Body = record[len(record)-d.Remaining():]
return r, nil
case AcceptProgMismatch:
if r.Mismatch.Low, err = d.Uint32(); err != nil {
return Reply{}, ErrBadMessage
}
if r.Mismatch.High, err = d.Uint32(); err != nil {
return Reply{}, ErrBadMessage
}
return r, nil
default:
return r, nil
}
case 1: // rejected
r := Reply{XID: xid, Rejected: true}
kind, err := d.Uint32()
if err != nil {
return Reply{}, ErrBadMessage
}
switch kind {
case RejectRPCMismatch:
if r.Mismatch.Low, err = d.Uint32(); err != nil {
return Reply{}, ErrBadMessage
}
if r.Mismatch.High, err = d.Uint32(); err != nil {
return Reply{}, ErrBadMessage
}
case RejectAuthError:
if r.AuthStat, err = d.Uint32(); err != nil {
return Reply{}, ErrBadMessage
}
default:
return Reply{}, ErrBadMessage
}
return r, nil
default:
return Reply{}, fmt.Errorf("%w: unknown reply stat %d", ErrBadMessage, stat)
}
}
// An AuthSys is an AUTH_SYS credential body: the identity the client
// asserts for every request.
type AuthSys struct {
Stamp uint32
Machine string
UID uint32
GID uint32
GIDs []uint32
}
// Body encodes the credential in the AUTH_SYS layout.
func (a AuthSys) Body() ([]byte, error) {
if len(a.GIDs) > 16 {
return nil, fmt.Errorf("rpc: %d supplementary groups exceeds 16", len(a.GIDs))
}
b := xdr.AppendUint32(nil, a.Stamp)
b = xdr.AppendString(b, a.Machine)
b = xdr.AppendUint32(b, a.UID)
b = xdr.AppendUint32(b, a.GID)
b = xdr.AppendUint32(b, uint32(len(a.GIDs)))
for _, g := range a.GIDs {
b = xdr.AppendUint32(b, g)
}
return b, nil
}
// DecodeAuthSysBody decodes an AUTH_SYS credential body.
func DecodeAuthSysBody(body []byte) (AuthSys, error) {
d := xdr.NewDecoder(body)
var a AuthSys
var err error
if a.Stamp, err = d.Uint32(); err != nil {
return a, ErrBadMessage
}
if a.Machine, err = d.String(); err != nil {
return a, ErrBadMessage
}
if a.UID, err = d.Uint32(); err != nil {
return a, ErrBadMessage
}
if a.GID, err = d.Uint32(); err != nil {
return a, ErrBadMessage
}
n, err := d.Uint32()
if err != nil {
return a, ErrBadMessage
}
if n > 16 {
return a, fmt.Errorf("rpc: %d supplementary groups exceeds 16", n)
}
for range n {
g, err := d.Uint32()
if err != nil {
return a, ErrBadMessage
}
a.GIDs = append(a.GIDs, g)
}
return a, nil
}
// PeekHeader reads the XID and the message type of a record without fully
// decoding it. It is the demultiplexer's tool: a connection that carries
// both directions distinguishes a reply to its own call from a call the
// peer issued by the message type alone.
func PeekHeader(record []byte) (xid uint32, mtype uint32, err error) {
d := xdr.NewDecoder(record)
if xid, err = d.Uint32(); err != nil {
return 0, 0, ErrBadMessage
}
if mtype, err = d.Uint32(); err != nil {
return 0, 0, ErrBadMessage
}
return xid, mtype, nil
}
// MsgDenied is the reply_stat of a rejected reply, RFC 5531 section 8.
const MsgDenied = 1
// AppendRejectedReply encodes a MSG_DENIED reply with an auth error.
func AppendRejectedReply(b []byte, xid, authStat uint32) []byte {
b = xdr.AppendUint32(b, xid)
b = xdr.AppendUint32(b, MsgReply)
b = xdr.AppendUint32(b, MsgDenied)
b = xdr.AppendUint32(b, RejectAuthError)
return xdr.AppendUint32(b, authStat)
}