feat: full NFSv4.2 server and client in pure Go
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
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
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -0,0 +1,375 @@
|
||||
// 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)
|
||||
}
|
||||
Reference in New Issue
Block a user