radius

package
v0.4.0 Latest Latest
Warning

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

Go to latest
Published: Jul 18, 2026 License: Apache-2.0 Imports: 19 Imported by: 0

Documentation

Overview

Package radius implements just enough of RFC 2865/3579 for an outside-in diagnostic client: build an Access-Request, hide a PAP password, add a Message-Authenticator, and verify the Response Authenticator on the reply.

The probe acts as a NAS (network access server) talking to a RADIUS server, so it only ever *sends* Access-Requests and *reads* the replies.

Index

Constants

View Source
const (
	EAPRequest  byte = 1
	EAPResponse byte = 2
	EAPSuccess  byte = 3
	EAPFailure  byte = 4
)

EAP (RFC 3748) codes and types used by the probe.

View Source
const (
	EAPTypeIdentity byte = 1
	EAPTypeNak      byte = 3
	EAPTypeTLS      byte = 13
	EAPTypeTTLS     byte = 21
	EAPTypePEAP     byte = 25
	EAPTypeMSCHAPv2 byte = 26
	EAPTypeTLV      byte = 33 // PEAP Result-TLV (extensions)
)
View Source
const (
	NASPortEthernet      = 15
	NASPortWireless80211 = 19
	NASPortVirtual       = 5
)

NAS-Port-Type values (RFC 2865). Wireless-802.11 is what real APs send; including it makes the probe's request match the same network policies a real 802.1X client would.

Variables

View Source
var ErrTimeout = errors.New("no reply before timeout")

ErrTimeout means no reply arrived before the deadline — the server is unreachable, not listening, or (very commonly) does not have this probe whitelisted as a RADIUS client, in which case it silently drops the request.

Functions

func DecodeMSCHAPError

func DecodeMSCHAPError(failureMsg string) (code int, cause string)

DecodeMSCHAPError turns the "E=<code>" in an MSCHAPv2 Failure message into a plain-English cause. The message looks like:

E=691 R=1 C=<hex> V=3 M=Authentication failed

func GenerateAuthenticatorResponse

func GenerateAuthenticatorResponse(authChallenge, peerChallenge [16]byte, ntResponse []byte, userName, password string) string

GenerateAuthenticatorResponse reproduces the server's expected "S=" value (RFC 2759 §8.7), letting the probe verify the server proved knowledge of the password — i.e. it's the real RADIUS server, not an impostor.

func GenerateNTResponse

func GenerateNTResponse(authChallenge, peerChallenge [16]byte, userName, password string) []byte

GenerateNTResponse produces the 24-byte NT-Response for an MSCHAPv2 exchange.

func MTUReachable

func MTUReachable(addr, secret string, target int, attrs []Attribute, timeout time.Duration, localAddr net.Addr) (ok bool, replied bool, err error)

MTUReachable sends an Access-Request padded to ~target bytes and reports whether a reply came back — i.e. whether the network path carries RADIUS packets of that size in both directions. Any reply (even Access-Reject) counts; a timeout means the packet (or its reply) was dropped.

func NewPeerChallenge

func NewPeerChallenge() ([16]byte, error)

NewPeerChallenge returns 16 random bytes for the client challenge.

func VerifyMessageAuthenticator added in v0.3.0

func VerifyMessageAuthenticator(raw []byte, reqAuth [16]byte, secret string) (present, valid bool)

VerifyMessageAuthenticator reports whether a reply we received carries a Message-Authenticator attribute (RFC 3579 §3.2) and, if so, whether it is valid for the request we sent. This is the observation behind the BlastRADIUS (CVE-2024-3596) posture check: a server that signs its replies closes off the RADIUS/UDP reply-forgery class for this client entry.

  • present=false: the reply has no Message-Authenticator at all — the server accepted our (signed) request but did not sign its answer.
  • present=true, valid=false: a Message-Authenticator is there but does not verify with this shared secret (a wrong secret, or a malformed/tampered attribute).
  • present=true, valid=true: the server signed its reply correctly.

raw is the exact bytes received; reqAuth is the Request Authenticator we sent (RFC 3579 keys the reply HMAC off the request's authenticator, not the reply's). This never mutates raw and never surfaces the secret.

func VerifyResponse

func VerifyResponse(raw []byte, reqAuth [16]byte, secret string) bool

VerifyResponse checks the Response Authenticator of a reply against the request authenticator and shared secret (RFC 2865 §3). A correct result proves the server holds the same shared secret we do. `raw` is the exact bytes received; `reqAuth` is the authenticator we sent.

Types

type AttrType

type AttrType byte

Attribute types used by the probe (RFC 2865 / 3579).

const (
	AttrServiceType          AttrType = 6
	AttrFramedProtocol       AttrType = 7
	AttrFilterID             AttrType = 11
	AttrVendorSpecific       AttrType = 26
	AttrSessionTimeout       AttrType = 27
	AttrIdleTimeout          AttrType = 28
	AttrTunnelType           AttrType = 64
	AttrTunnelMediumType     AttrType = 65
	AttrTunnelPrivateGroupID AttrType = 81
)

Authorization attributes an Access-Accept can carry to decide *which* VLAN/policy the authenticated session lands in (RFC 2865 / RFC 2868). A huge share of real 802.1X tickets are "auth works, but into the wrong VLAN" — the answer is in these attributes, so the probe surfaces them.

const (
	AttrUserName             AttrType = 1
	AttrUserPassword         AttrType = 2
	AttrNASIPAddress         AttrType = 4
	AttrNASPort              AttrType = 5
	AttrReplyMessage         AttrType = 18
	AttrState                AttrType = 24
	AttrProxyState           AttrType = 33 // echoed unchanged by the server (RFC 2865)
	AttrCalledStationID      AttrType = 30
	AttrCallingStationID     AttrType = 31
	AttrNASIdentifier        AttrType = 32
	AttrNASPortType          AttrType = 61
	AttrEAPMessage           AttrType = 79
	AttrMessageAuthenticator AttrType = 80
)

type Attribute

type Attribute struct {
	Type  AttrType
	Value []byte
}

type AuthAttr added in v0.3.0

type AuthAttr struct {
	Name   string `json:"name"`
	Value  string `json:"value"`
	Raw    string `json:"raw,omitempty"`    // hex, for VSAs / undecodable values
	Vendor int    `json:"vendor,omitempty"` // vendor id, for vendor-specific attributes
}

AuthAttr is one decoded authorization attribute from an Access-Accept, in a form always safe to print: these attributes never carry secrets. Value is the human-readable decode; Raw (hex) and Vendor are set only for vendor-specific or otherwise opaque values so nothing is lost.

type CapturedCert

type CapturedCert struct {
	Chain       []*x509.Certificate
	TLSVersion  uint16
	CipherSuite uint16
}

CapturedCert holds what the probe learned from the outer TLS handshake.

type Code

type Code byte
const (
	AccessRequest   Code = 1
	AccessAccept    Code = 2
	AccessReject    Code = 3
	AccessChallenge Code = 11
	StatusServer    Code = 12 // RFC 5997 liveness query; reply is an Access-Accept
)

func (Code) String

func (c Code) String() string

type EAPPacket

type EAPPacket struct {
	Code byte
	ID   byte
	Type byte
	Data []byte
}

EAPPacket is a parsed EAP message. Type is 0 for Success/Failure (which carry no type byte).

func ParseEAP

func ParseEAP(b []byte) (*EAPPacket, error)

func (*EAPPacket) Marshal

func (e *EAPPacket) Marshal() []byte

type EAPSession

type EAPSession struct {
	Addr      string
	Secret    string
	Timeout   time.Duration
	Identity  string
	Attrs     []Attribute // common NAS attributes added to every request
	LocalAddr net.Addr    // source address to bind (--bind); nil = OS default
	// contains filtered or unexported fields
}

EAPSession drives an EAP conversation over RADIUS: it acts as the NAS, sending Access-Requests carrying EAP-Message attributes and reading the Access-Challenge replies, tracking the RADIUS State attribute and EAP identifiers across round trips.

It is used to establish the outer TLS tunnel of PEAP/EAP-TLS so the server's certificate can be inspected. Inner authentication (PEAP-MSCHAPv2, EAP-TLS client cert) builds on the same session and is a later milestone.

func (*EAPSession) AuthEAPTLS

func (s *EAPSession) AuthEAPTLS(ctx context.Context, clientCert tls.Certificate, serverName string) (*EAPTLSResult, error)

AuthEAPTLS runs a real EAP-TLS authentication, presenting the given client certificate. In EAP-TLS the TLS handshake *is* the authentication: the server validates the client certificate during the handshake. We complete the handshake, then drive the final EAP exchange to read the server's verdict (Access-Accept vs Access-Reject) — so we can tell "certificate untrusted" apart from "certificate fine, but the server's policy rejected this identity".

The private key is used only to complete the handshake and never leaves the host.

func (*EAPSession) AuthEAPTTLS

func (s *EAPSession) AuthEAPTTLS(ctx context.Context, userName, password, serverName string) (*TTLSResult, error)

AuthEAPTTLS completes the EAP-TTLS tunnel and authenticates with inner PAP. The password is sent as a Diameter AVP *inside* the TLS tunnel (never over the wire in the clear) and is never logged.

func (*EAPSession) AuthPEAPMSCHAPv2

func (s *EAPSession) AuthPEAPMSCHAPv2(ctx context.Context, userName, password, serverName string) (*PEAPResult, error)

AuthPEAPMSCHAPv2 completes the PEAP tunnel and runs a real inner EAP-MSCHAPv2 authentication with the given credentials. It reports success/failure, decodes the MSCHAPv2 error code on rejection, and verifies the server's authenticator response (mutual proof). The password is used only to build the response and is never transmitted or logged.

func (*EAPSession) InspectServerCert

func (s *EAPSession) InspectServerCert(ctx context.Context, serverName string) (*CapturedCert, error)

InspectServerCert establishes the PEAP outer TLS tunnel far enough to receive and record the RADIUS server's certificate chain, then aborts (it never sends inner credentials or a client certificate). serverName sets SNI; empty is allowed. This is read-only: no authentication is attempted or completed.

type EAPTLSResult

type EAPTLSResult struct {
	Success bool
	Reason  string // plain-English cause when Success is false
	Cert    *CapturedCert
	Accept  *Packet // final Access-Accept, for authorization attributes; may be nil
}

EAPTLSResult reports the outcome of an EAP-TLS authentication attempt.

type PEAPResult

type PEAPResult struct {
	Success      bool
	ServerProved bool // the server's MSCHAPv2 authenticator response verified
	ErrorCode    int  // MSCHAPv2 error code on failure (e.g. 691), else 0
	ErrorCause   string
	Cert         *CapturedCert
	// Accept is the final Access-Accept, when the probe was able to drive the
	// exchange to it, so its authorization attributes (VLAN/Filter-Id/…) can be
	// read. Nil if the accept could not be captured — auth still succeeded.
	Accept *Packet
}

PEAPResult reports the outcome of a PEAP-MSCHAPv2 authentication attempt.

type Packet

type Packet struct {
	Code          Code
	Identifier    byte
	Authenticator [16]byte
	Attributes    []Attribute
	// contains filtered or unexported fields
}

func Exchange

func Exchange(addr string, secret string, p *Packet, timeout time.Duration, localAddr net.Addr) (reply *Packet, raw []byte, rtt time.Duration, err error)

Exchange sends one Access-Request and waits for a reply. It returns the decoded reply, the raw reply bytes (for Response-Authenticator verification), and the round-trip time. A Message-Authenticator is always included, which is both good hygiene and required by servers hardened against BlastRADIUS (CVE-2024-3596).

localAddr, when non-nil, is the source address the UDP socket binds to (the --bind flag) — the way to pin the outgoing interface on a multi-homed host. The chosen source IP is what the RADIUS server sees, and it is exactly what TimeoutError.LocalIP reports back so the registration hint stays correct.

func NewAccessRequest

func NewAccessRequest(id byte) (*Packet, error)

NewAccessRequest creates an Access-Request with a fresh request authenticator.

func NewStatusServer added in v0.3.0

func NewStatusServer(id byte) (*Packet, error)

NewStatusServer creates a Status-Server (RFC 5997) liveness query with a fresh authenticator. It carries no User-Name or password — it is a pure "are you alive?" ping that a server answers with an Access-Accept without ever consuming an authentication attempt. Exchange always appends the Message-Authenticator that RFC 5997 requires.

func (*Packet) Add

func (p *Packet) Add(t AttrType, v []byte)

func (*Packet) AddEAP

func (p *Packet) AddEAP(eap []byte)

AddEAP splits an EAP packet across as many EAP-Message attributes as needed (each attribute value is capped at 253 bytes). RFC 3579: a receiver concatenates them in order to reconstruct the EAP packet.

func (*Packet) AddString

func (p *Packet) AddString(t AttrType, v string)

func (*Packet) AuthorizationAttributes added in v0.3.0

func (p *Packet) AuthorizationAttributes() []AuthAttr

AuthorizationAttributes decodes the authorization-relevant attributes the server returned, in the order they appear on the wire. Transport plumbing (EAP-Message, Message-Authenticator, State, Proxy-State) is skipped — those say nothing about the granted VLAN/policy. Unknown vendor attributes are kept as vendor id + raw hex rather than dropped.

func (*Packet) ConcatEAP

func (p *Packet) ConcatEAP() []byte

ConcatEAP reassembles the EAP packet from all EAP-Message attributes, in order.

func (*Packet) Get

func (p *Packet) Get(t AttrType) []byte

Get returns the first attribute of type t, or nil.

func (*Packet) GetAllString

func (p *Packet) GetAllString(t AttrType) string

GetAllString returns the concatenated string values of every attribute of type t (used for Reply-Message, which servers may split across attributes).

func (*Packet) SetUserPassword

func (p *Packet) SetUserPassword(password, secret string)

SetUserPassword hides the password per RFC 2865 §5.2 and adds it as User-Password. Must be called after the authenticator is set.

func (*Packet) WireSize

func (p *Packet) WireSize(withMsgAuth bool) int

WireSize returns the encoded byte length of the packet. When withMsgAuth is true it includes the 18-byte Message-Authenticator that Exchange always adds.

type RadSecResult

type RadSecResult struct {
	Connected     bool // TCP connection established
	TLSOK         bool // TLS handshake completed
	TLSVersion    uint16
	Cert          []*x509.Certificate
	RADIUSReplyOK bool   // a RADIUS request over the tunnel got a reply
	Reason        string // failure detail when a stage didn't pass
}

RadSecResult reports what the probe learned about a RadSec (RADIUS/TLS, RFC 6614) endpoint on TCP/2083.

func DialRadSec

func DialRadSec(ctx context.Context, addr string, clientCert *tls.Certificate, serverName string, timeout time.Duration) *RadSecResult

DialRadSec connects to a RadSec endpoint, completes the TLS handshake (presenting clientCert if given), captures the server certificate, and — if the tunnel comes up — sends one RADIUS Access-Request over it to confirm the RADIUS layer answers. Read-only: no state is changed on the server.

type TTLSResult

type TTLSResult struct {
	Success bool
	Reason  string
	Cert    *CapturedCert
	Accept  *Packet // final Access-Accept, for authorization attributes; may be nil
}

TTLSResult reports the outcome of an EAP-TTLS (inner PAP) authentication.

type TimeoutError added in v0.3.0

type TimeoutError struct{ LocalIP string }

TimeoutError is the concrete error Exchange returns on timeout. It carries the local source IP the OS chose for the (already-dialed) socket, so callers can tell the admin exactly which address to register as a RADIUS client. errors.Is(err, ErrTimeout) matches it, so existing checks need no change.

func (*TimeoutError) Error added in v0.3.0

func (e *TimeoutError) Error() string

func (*TimeoutError) Is added in v0.3.0

func (e *TimeoutError) Is(target error) bool

Jump to

Keyboard shortcuts

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