// Copyright (c) 2026 Petr BalvĂ­n (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) }