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
376 lines
10 KiB
Go
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)
|
|
}
|