Files
cgrates/agents/librad.go
ionutboangiu 9991b29cae Implement DisconnectSession API for RADIUS Agent
Updated radigo library to latest version.

Updated RadiusAgent to satisfy the birpc client interface.

Added *radDAdiscMsg OrderedNavigableMap type field within AgentRequest.
This one is similar to *diamreq, as it is used for building RADIUS
server-initiated Disconnect Requests.

radReplyAppendAttributes: refactored to reflect that it can now be
used to also append attributes to request packets, not only reply.

Added bidirectional support for session related RadiusAgent methods.

For Dynamic Authorization to be possible, a new field was added within RadiusAgent
that holds dicts and secrets only for the clients that support it. They are used
to create the DA Client sending Disconnect Requests.

Added a new cache partition to store Access-Request packets with the purpose
of using them to build the Disconnect Requests. They are identified by sessionID.
It defaults to the value of 'Acct-Session-id'.

Added a predefined '*dmr' template as well as a 'dmr_template' config option within
the 'radius_agent' config section. This will map to a custom or to the predefined
template and will be used to build the Disconnect Request. By default, it doesn't
point to any template (this also means that the Access-Request packets will not be
cached).

Another option added to 'radius_agent' is 'client_da_addresses', which lists the
RADIUS clients supporting Dynamic Authorization. The key represents the host of
the client, while the value represents the address to which we will send the
Disconnect Request.

Added integration test.
2024-02-07 18:28:17 +01:00

167 lines
5.5 KiB
Go

/*
Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments
Copyright (C) ITsysCOM GmbH
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>
*/
package agents
import (
"bytes"
"github.com/cgrates/cgrates/utils"
"github.com/cgrates/radigo"
)
// radAppendAttributes appends attributes to a RADIUS packet based on predefined template
func radAppendAttributes(packet *radigo.Packet, nm *utils.OrderedNavigableMap) error {
for el := nm.GetFirstElement(); el != nil; el = el.Next() {
path := el.Value
cfgItm, _ := nm.Field(path)
path = path[:len(path)-1] // remove the last index
if path[0] == MetaRadReplyCode { // Special case used to control the reply code of RADIUS reply
if err := packet.SetCodeWithName(utils.IfaceAsString(cfgItm.Data)); err != nil {
return err
}
continue
}
var attrName, vendorName string
if len(path) > 1 {
vendorName, attrName = path[0], path[1]
} else {
attrName = path[0]
}
if err := packet.AddAVPWithName(attrName, utils.IfaceAsString(cfgItm.Data), vendorName); err != nil {
return err
}
}
return nil
}
// newRADataProvider constructs a DataProvider
func newRADataProvider(req *radigo.Packet) (dP utils.DataProvider) {
dP = &radiusDP{req: req, cache: utils.MapStorage{}}
return
}
// radiusDP implements utils.DataProvider, serving as radigo.Packet data decoder
// decoded data is only searched once and cached
type radiusDP struct {
req *radigo.Packet
cache utils.MapStorage
}
// String is part of utils.DataProvider interface
// when called, it will display the already parsed values out of cache
func (pk *radiusDP) String() string {
return utils.ToIJSON(pk.req) // return ToJSON because Packet don't have a string method
}
// FieldAsInterface is part of utils.DataProvider interface
func (pk *radiusDP) FieldAsInterface(fldPath []string) (data any, err error) {
if len(fldPath) != 1 {
return nil, utils.ErrNotFound
}
if data, err = pk.cache.FieldAsInterface(fldPath); err != nil {
if err != utils.ErrNotFound { // item found in cache
return
}
err = nil // cancel previous err
} else {
return // data found in cache
}
if len(pk.req.AttributesWithName(fldPath[0], "")) != 0 {
data = pk.req.AttributesWithName(fldPath[0], "")[0].GetStringValue()
}
pk.cache.Set(fldPath, data)
return
}
// FieldAsString is part of utils.DataProvider interface
func (pk *radiusDP) FieldAsString(fldPath []string) (data string, err error) {
var valIface any
valIface, err = pk.FieldAsInterface(fldPath)
if err != nil {
return
}
return utils.IfaceAsString(valIface), nil
}
// radauthReq is used to authorize a request based on flags
func radauthReq(flags utils.FlagsWithParams, req *radigo.Packet, aReq *AgentRequest, rpl *radigo.Packet) (bool, error) {
// try to get UserPassword from Vars as slice of NMItems
nmItems, has := aReq.Vars.Map[utils.UserPassword]
if !has {
return false, utils.ErrNotFound
}
pass := nmItems.Slice[0].Value.String()
switch {
case flags.Has(utils.MetaPAP):
userPassAvps := req.AttributesWithName(UserPasswordAVP, utils.EmptyString)
if len(userPassAvps) == 0 {
return false, utils.NewErrMandatoryIeMissing(UserPasswordAVP)
}
return userPassAvps[0].StringValue == pass, nil
case flags.Has(utils.MetaCHAP):
chapAVPs := req.AttributesWithName(CHAPPasswordAVP, utils.EmptyString)
if len(chapAVPs) == 0 {
return false, utils.NewErrMandatoryIeMissing(CHAPPasswordAVP)
}
return radigo.AuthenticateCHAP([]byte(pass),
req.Authenticator[:], chapAVPs[0].RawValue), nil
case flags.Has(utils.MetaMSCHAPV2):
msChallenge := req.AttributesWithName(MSCHAPChallengeAVP, MicrosoftVendor)
if len(msChallenge) == 0 {
return false, utils.NewErrMandatoryIeMissing(MSCHAPChallengeAVP)
}
msResponse := req.AttributesWithName(MSCHAPResponseAVP, MicrosoftVendor)
if len(msResponse) == 0 {
return false, utils.NewErrMandatoryIeMissing(MSCHAPResponseAVP)
}
vsaMSResponde := msResponse[0].Value.(*radigo.VSA).RawValue
vsaMSChallange := msChallenge[0].Value.(*radigo.VSA).RawValue
userName := req.AttributesWithName("User-Name", utils.EmptyString)[0].StringValue
if len(vsaMSChallange) != 16 || len(vsaMSResponde) != 50 {
return false, nil
}
ident := vsaMSResponde[0]
peerChallenge := vsaMSResponde[2:18]
peerResponse := vsaMSResponde[26:50]
ntResponse, err := radigo.GenerateNTResponse(vsaMSChallange,
peerChallenge, userName, pass)
if err != nil || !bytes.Equal(ntResponse, peerResponse) {
return false, err
}
authenticatorResponse, err := radigo.GenerateAuthenticatorResponse(vsaMSChallange, peerChallenge,
ntResponse, userName, pass)
if err != nil {
return false, err
}
success := make([]byte, 43)
success[0] = ident
copy(success[1:], authenticatorResponse)
// this AVP need to be added to be verified on the client side
rpl.AddAVPWithName(MSCHAP2SuccessAVP, string(success), MicrosoftVendor)
return true, nil
default:
return false, utils.NewErrMandatoryIeMissing(utils.Flags)
}
}