Files
cgrates/agents/radagent.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

578 lines
22 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 (
"errors"
"fmt"
"net"
"strings"
"sync"
"github.com/cgrates/birpc"
"github.com/cgrates/birpc/context"
"github.com/cgrates/cgrates/config"
"github.com/cgrates/cgrates/engine"
"github.com/cgrates/cgrates/sessions"
"github.com/cgrates/cgrates/utils"
"github.com/cgrates/radigo"
)
const (
MetaRadReqType = "*radReqType"
MetaRadAuth = "*radAuth"
MetaRadReplyCode = "*radReplyCode"
UserPasswordAVP = "User-Password"
CHAPPasswordAVP = "CHAP-Password"
MSCHAPChallengeAVP = "MS-CHAP-Challenge"
MSCHAPResponseAVP = "MS-CHAP-Response"
MicrosoftVendor = "Microsoft"
MSCHAP2SuccessAVP = "MS-CHAP2-Success"
)
func NewRadiusAgent(cgrCfg *config.CGRConfig, filterS *engine.FilterS,
connMgr *engine.ConnManager) (*RadiusAgent, error) {
radAgent := &RadiusAgent{
cgrCfg: cgrCfg,
filterS: filterS,
connMgr: connMgr,
}
// Register RadiusAgent methods whose names start with "V1" under the "SessionSv1" object name.
srv, err := birpc.NewServiceWithMethodsRename(radAgent, utils.SessionSv1, true, func(oldFn string) (newFn string) {
return strings.TrimPrefix(oldFn, "V1")
})
if err != nil {
return nil, err
}
radAgent.ctx = context.WithClient(context.TODO(), srv)
radAgentCfg := cgrCfg.RadiusAgentCfg()
dts := make(map[string]*radigo.Dictionary, len(radAgentCfg.ClientDictionaries))
for clntID, dictPath := range radAgentCfg.ClientDictionaries {
utils.Logger.Info(fmt.Sprintf(
"<%s> loading dictionary for clientID: <%s> out of path <%s>",
utils.RadiusAgent, clntID, dictPath))
if dts[clntID], err = radigo.NewDictionaryFromFoldersWithRFC2865(dictPath); err != nil {
return nil, err
}
}
dicts := radigo.NewDictionaries(dts)
secrets := radigo.NewSecrets(radAgentCfg.ClientSecrets)
radAgent.dacCfg = newRadiusDAClientCfg(dicts, secrets, radAgentCfg)
radAgent.rsAuth = make(map[string]*radigo.Server, len(radAgentCfg.Listeners))
radAgent.rsAcct = make(map[string]*radigo.Server, len(radAgentCfg.Listeners))
for i := range radAgentCfg.Listeners {
net := radAgentCfg.Listeners[i].Network
authAddr := radAgentCfg.Listeners[i].AuthAddr
radAgent.rsAuth[net+"://"+authAddr] = radigo.NewServer(net, authAddr, secrets, dicts,
map[radigo.PacketCode]func(*radigo.Packet) (*radigo.Packet, error){
radigo.AccessRequest: radAgent.handleAuth,
}, nil, utils.Logger)
acctAddr := radAgentCfg.Listeners[i].AcctAddr
radAgent.rsAcct[net+"://"+acctAddr] = radigo.NewServer(net, acctAddr, secrets, dicts,
map[radigo.PacketCode]func(*radigo.Packet) (*radigo.Packet, error){
radigo.AccountingRequest: radAgent.handleAcct,
}, nil, utils.Logger)
}
return radAgent, nil
}
type RadiusAgent struct {
sync.RWMutex
cgrCfg *config.CGRConfig // reference for future config reloads
connMgr *engine.ConnManager
filterS *engine.FilterS
rsAuth map[string]*radigo.Server
rsAcct map[string]*radigo.Server
dacCfg radiusDAClientCfg
ctx *context.Context
sync.WaitGroup
}
// radiusDAClientCfg holds the dictionaries and secrets necessary for initializing Dynamic Authorization Clients in RADIUS (only for
// the clients mentioned in the client_da_addresses map).
// This configuration enables the RadiusAgent to send server-initiated actions, such as Disconnect Requests and CoA requests,
// to manage ongoing user sessions dynamically.
type radiusDAClientCfg struct {
dicts *radigo.Dictionaries
secrets *radigo.Secrets
}
// newRadiusDAClientCfg is a constructor for the radiusDAClientCfg type.
func newRadiusDAClientCfg(dicts *radigo.Dictionaries, secrets *radigo.Secrets, radAgentCfg *config.RadiusAgentCfg) radiusDAClientCfg {
dacDicts := make(map[string]*radigo.Dictionary, len(radAgentCfg.ClientDaAddresses))
dacSecrets := make(map[string]string, len(radAgentCfg.ClientDaAddresses))
for client := range radAgentCfg.ClientDaAddresses {
dacDicts[client] = dicts.GetInstance(client)
dacSecrets[client] = secrets.GetSecret(client)
}
var rdac radiusDAClientCfg
if len(dacDicts) != 0 {
rdac.dicts = radigo.NewDictionaries(dacDicts)
}
if len(dacSecrets) != 0 {
rdac.secrets = radigo.NewSecrets(dacSecrets)
}
return rdac
}
// handleAuth handles RADIUS Authorization request
func (ra *RadiusAgent) handleAuth(reqPacket *radigo.Packet) (*radigo.Packet, error) {
reqPacket.SetAVPValues() // populate string values in AVPs
replyPacket := reqPacket.Reply()
replyPacket.Code = radigo.AccessAccept
cgrReplyNM := &utils.DataNode{Type: utils.NMMapType, Map: map[string]*utils.DataNode{}}
replyNM := utils.NewOrderedNavigableMap()
opts := utils.MapStorage{}
varsDataNode := &utils.DataNode{
Type: utils.NMMapType,
Map: map[string]*utils.DataNode{
utils.RemoteHost: utils.NewLeafNode(reqPacket.RemoteAddr().String()),
},
}
radDP := newRADataProvider(reqPacket)
sessionID, err := radDP.FieldAsString([]string{"Acct-Session-Id"})
if err == nil {
_, err = varsDataNode.Set([]string{utils.MetaSessionID}, []*utils.DataNode{
{
Type: utils.NMDataType, Value: &utils.DataLeaf{
Data: sessionID,
},
},
})
}
if err != nil {
utils.Logger.Warning(fmt.Sprintf(
"<%s> setting default *vars.*sessionID (used to track packets of active sessions) failed: %v",
utils.RadiusAgent, err))
}
var processed bool
var processReqErr error
for _, reqProcessor := range ra.cgrCfg.RadiusAgentCfg().RequestProcessors {
agReq := NewAgentRequest(radDP, varsDataNode, cgrReplyNM, replyNM, opts,
reqProcessor.Tenant, ra.cgrCfg.GeneralCfg().DefaultTenant,
utils.FirstNonEmpty(reqProcessor.Timezone,
config.CgrConfig().GeneralCfg().DefaultTimezone),
ra.filterS, nil)
agReq.Vars.Map[MetaRadReqType] = utils.NewLeafNode(MetaRadAuth)
var lclProcessed bool
if lclProcessed, processReqErr = ra.processRequest(reqPacket, reqProcessor, agReq, replyPacket); lclProcessed {
processed = lclProcessed
}
if processReqErr != nil || (lclProcessed && !reqProcessor.Flags.GetBool(utils.MetaContinue)) {
break
}
}
if processReqErr != nil {
utils.Logger.Warning(fmt.Sprintf("<%s> error: <%v> ignoring request: %s",
utils.RadiusAgent, processReqErr, utils.ToJSON(reqPacket)))
return nil, nil
} else if !processed {
utils.Logger.Warning(fmt.Sprintf("<%s> no request processor enabled, ignoring request %s",
utils.RadiusAgent, utils.ToJSON(reqPacket)))
return nil, nil
}
// Cache the RADIUS Packet for future Disconnect Requests.
if ra.cgrCfg.RadiusAgentCfg().DMRTemplate != "" {
// Retrieve the identifying sessionID from the *vars map, as it was probably modified
// by one of the processed requests. The path element is suffixed by '[0]' due to the
// fields being set as []*DataNode inside the *AgentRequest.SetFields method.
sessionID, err := varsDataNode.FieldAsInterface([]string{utils.MetaSessionID + "[0]"})
if err != nil {
return nil, fmt.Errorf("retrieving sessionID failed: %w", err)
}
if errCh := engine.Cache.Set(utils.CacheRadiusPackets, utils.IfaceAsString(sessionID), reqPacket,
nil, true, utils.NonTransactional); errCh != nil {
return nil, fmt.Errorf("caching RADIUS Packet failed: %w", err)
}
}
if err := radAppendAttributes(replyPacket, replyNM); err != nil {
utils.Logger.Err(fmt.Sprintf("<%s> err: %v, replying to message: %+v",
utils.RadiusAgent, err, utils.ToIJSON(reqPacket)))
return nil, err
}
return replyPacket, nil
}
// handleAcct handles RADIUS Accounting request
// supports: Acct-Status-Type = Start, Interim-Update, Stop
func (ra *RadiusAgent) handleAcct(req *radigo.Packet) (rpl *radigo.Packet, err error) {
req.SetAVPValues() // populate string values in AVPs
dcdr := newRADataProvider(req) // dcdr will provide information from request
rpl = req.Reply()
rpl.Code = radigo.AccountingResponse
cgrRplyNM := &utils.DataNode{Type: utils.NMMapType, Map: map[string]*utils.DataNode{}}
rplyNM := utils.NewOrderedNavigableMap()
opts := utils.MapStorage{}
var processed bool
reqVars := &utils.DataNode{Type: utils.NMMapType, Map: map[string]*utils.DataNode{utils.RemoteHost: utils.NewLeafNode(req.RemoteAddr().String())}}
for _, reqProcessor := range ra.cgrCfg.RadiusAgentCfg().RequestProcessors {
agReq := NewAgentRequest(dcdr, reqVars, cgrRplyNM, rplyNM, opts,
reqProcessor.Tenant, ra.cgrCfg.GeneralCfg().DefaultTenant,
utils.FirstNonEmpty(reqProcessor.Timezone,
config.CgrConfig().GeneralCfg().DefaultTimezone),
ra.filterS, nil)
var lclProcessed bool
if lclProcessed, err = ra.processRequest(req, reqProcessor, agReq, rpl); lclProcessed {
processed = lclProcessed
}
if err != nil || (lclProcessed && !reqProcessor.Flags.GetBool(utils.MetaContinue)) {
break
}
}
if err != nil {
utils.Logger.Err(fmt.Sprintf("<%s> error: <%s> ignoring request: %s, ",
utils.RadiusAgent, err.Error(), utils.ToIJSON(req)))
return nil, nil
} else if !processed {
utils.Logger.Err(fmt.Sprintf("<%s> no request processor enabled, ignoring request %s",
utils.RadiusAgent, utils.ToIJSON(req)))
return nil, nil
}
if err := radAppendAttributes(rpl, rplyNM); err != nil {
utils.Logger.Err(fmt.Sprintf("<%s> err: %s, replying to message: %+v",
utils.RadiusAgent, err.Error(), utils.ToIJSON(req)))
return nil, err
}
return
}
// processRequest represents one processor processing the request
func (ra *RadiusAgent) processRequest(req *radigo.Packet, reqProcessor *config.RequestProcessor,
agReq *AgentRequest, rpl *radigo.Packet) (processed bool, err error) {
if pass, err := ra.filterS.Pass(agReq.Tenant,
reqProcessor.Filters, agReq); err != nil || !pass {
return pass, err
}
if err = agReq.SetFields(reqProcessor.RequestFields); err != nil {
return
}
cgrEv := utils.NMAsCGREvent(agReq.CGRRequest, agReq.Tenant, utils.NestingSep, agReq.Opts)
var reqType string
for _, typ := range []string{
utils.MetaDryRun, utils.MetaAuthorize,
utils.MetaInitiate, utils.MetaUpdate,
utils.MetaTerminate, utils.MetaMessage,
utils.MetaCDRs, utils.MetaEvent, utils.MetaNone, utils.MetaRadauth} {
if reqProcessor.Flags.Has(typ) { // request type is identified through flags
reqType = typ
break
}
}
var cgrArgs utils.Paginator
if reqType == utils.MetaAuthorize ||
reqType == utils.MetaMessage ||
reqType == utils.MetaEvent {
if cgrArgs, err = utils.GetRoutePaginatorFromOpts(cgrEv.APIOpts); err != nil {
utils.Logger.Warning(fmt.Sprintf("<%s> args extraction failed because <%s>",
utils.RadiusAgent, err.Error()))
err = nil // reset the error and continue the processing
}
}
if reqProcessor.Flags.Has(utils.MetaLog) {
utils.Logger.Info(
fmt.Sprintf("<%s> LOG, processorID: %s, radius message: %s",
utils.RadiusAgent, reqProcessor.ID, agReq.Request.String()))
}
switch reqType {
default:
return false, fmt.Errorf("unknown request type: <%s>", reqType)
case utils.MetaNone: // do nothing on CGRateS side
case utils.MetaDryRun:
utils.Logger.Info(
fmt.Sprintf("<%s> DRY_RUN, processorID: %s, CGREvent: %s",
utils.RadiusAgent, reqProcessor.ID, utils.ToJSON(cgrEv)))
case utils.MetaAuthorize:
authArgs := sessions.NewV1AuthorizeArgs(
reqProcessor.Flags.GetBool(utils.MetaAttributes),
reqProcessor.Flags.ParamsSlice(utils.MetaAttributes, utils.MetaIDs),
reqProcessor.Flags.GetBool(utils.MetaThresholds),
reqProcessor.Flags.ParamsSlice(utils.MetaThresholds, utils.MetaIDs),
reqProcessor.Flags.GetBool(utils.MetaStats),
reqProcessor.Flags.ParamsSlice(utils.MetaStats, utils.MetaIDs),
reqProcessor.Flags.GetBool(utils.MetaResources),
reqProcessor.Flags.Has(utils.MetaAccounts),
reqProcessor.Flags.GetBool(utils.MetaRoutes),
reqProcessor.Flags.Has(utils.MetaRoutesIgnoreErrors),
reqProcessor.Flags.Has(utils.MetaRoutesEventCost),
cgrEv, cgrArgs, reqProcessor.Flags.Has(utils.MetaFD),
reqProcessor.Flags.ParamValue(utils.MetaRoutesMaxCost),
)
rply := new(sessions.V1AuthorizeReply)
err = ra.connMgr.Call(ra.ctx, ra.cgrCfg.RadiusAgentCfg().SessionSConns, utils.SessionSv1AuthorizeEvent,
authArgs, rply)
rply.SetMaxUsageNeeded(authArgs.GetMaxUsage)
agReq.setCGRReply(rply, err)
case utils.MetaInitiate:
initArgs := sessions.NewV1InitSessionArgs(
reqProcessor.Flags.GetBool(utils.MetaAttributes),
reqProcessor.Flags.ParamsSlice(utils.MetaAttributes, utils.MetaIDs),
reqProcessor.Flags.GetBool(utils.MetaThresholds),
reqProcessor.Flags.ParamsSlice(utils.MetaThresholds, utils.MetaIDs),
reqProcessor.Flags.GetBool(utils.MetaStats),
reqProcessor.Flags.ParamsSlice(utils.MetaStats, utils.MetaIDs),
reqProcessor.Flags.GetBool(utils.MetaResources),
reqProcessor.Flags.Has(utils.MetaAccounts),
cgrEv, reqProcessor.Flags.Has(utils.MetaFD))
rply := new(sessions.V1InitSessionReply)
err = ra.connMgr.Call(ra.ctx, ra.cgrCfg.RadiusAgentCfg().SessionSConns, utils.SessionSv1InitiateSession,
initArgs, rply)
rply.SetMaxUsageNeeded(initArgs.InitSession)
agReq.setCGRReply(rply, err)
case utils.MetaUpdate:
updateArgs := sessions.NewV1UpdateSessionArgs(
reqProcessor.Flags.GetBool(utils.MetaAttributes),
reqProcessor.Flags.ParamsSlice(utils.MetaAttributes, utils.MetaIDs),
reqProcessor.Flags.Has(utils.MetaAccounts),
cgrEv, reqProcessor.Flags.Has(utils.MetaFD))
rply := new(sessions.V1UpdateSessionReply)
err = ra.connMgr.Call(ra.ctx, ra.cgrCfg.RadiusAgentCfg().SessionSConns, utils.SessionSv1UpdateSession,
updateArgs, rply)
rply.SetMaxUsageNeeded(updateArgs.UpdateSession)
agReq.setCGRReply(rply, err)
case utils.MetaTerminate:
terminateArgs := sessions.NewV1TerminateSessionArgs(
reqProcessor.Flags.Has(utils.MetaAccounts),
reqProcessor.Flags.GetBool(utils.MetaResources),
reqProcessor.Flags.GetBool(utils.MetaThresholds),
reqProcessor.Flags.ParamsSlice(utils.MetaThresholds, utils.MetaIDs),
reqProcessor.Flags.GetBool(utils.MetaStats),
reqProcessor.Flags.ParamsSlice(utils.MetaStats, utils.MetaIDs),
cgrEv, reqProcessor.Flags.Has(utils.MetaFD))
var rply string
err = ra.connMgr.Call(ra.ctx, ra.cgrCfg.RadiusAgentCfg().SessionSConns, utils.SessionSv1TerminateSession,
terminateArgs, &rply)
agReq.setCGRReply(nil, err)
case utils.MetaMessage:
evArgs := sessions.NewV1ProcessMessageArgs(
reqProcessor.Flags.GetBool(utils.MetaAttributes),
reqProcessor.Flags.ParamsSlice(utils.MetaAttributes, utils.MetaIDs),
reqProcessor.Flags.GetBool(utils.MetaThresholds),
reqProcessor.Flags.ParamsSlice(utils.MetaThresholds, utils.MetaIDs),
reqProcessor.Flags.GetBool(utils.MetaStats),
reqProcessor.Flags.ParamsSlice(utils.MetaStats, utils.MetaIDs),
reqProcessor.Flags.GetBool(utils.MetaResources),
reqProcessor.Flags.Has(utils.MetaAccounts),
reqProcessor.Flags.GetBool(utils.MetaRoutes),
reqProcessor.Flags.Has(utils.MetaRoutesIgnoreErrors),
reqProcessor.Flags.Has(utils.MetaRoutesEventCost),
cgrEv, cgrArgs, reqProcessor.Flags.Has(utils.MetaFD),
reqProcessor.Flags.ParamValue(utils.MetaRoutesMaxCost),
)
rply := new(sessions.V1ProcessMessageReply)
err = ra.connMgr.Call(ra.ctx, ra.cgrCfg.RadiusAgentCfg().SessionSConns, utils.SessionSv1ProcessMessage, evArgs, rply)
if utils.ErrHasPrefix(err, utils.RalsErrorPrfx) {
cgrEv.Event[utils.Usage] = 0 // avoid further debits
} else if evArgs.Debit {
cgrEv.Event[utils.Usage] = rply.MaxUsage // make sure the CDR reflects the debit
}
rply.SetMaxUsageNeeded(evArgs.Debit)
agReq.setCGRReply(rply, err)
case utils.MetaEvent:
evArgs := &sessions.V1ProcessEventArgs{
Flags: reqProcessor.Flags.SliceFlags(),
CGREvent: cgrEv,
Paginator: cgrArgs,
}
rply := new(sessions.V1ProcessEventReply)
err = ra.connMgr.Call(ra.ctx, ra.cgrCfg.RadiusAgentCfg().SessionSConns, utils.SessionSv1ProcessEvent,
evArgs, rply)
if utils.ErrHasPrefix(err, utils.RalsErrorPrfx) {
cgrEv.Event[utils.Usage] = 0 // avoid further debits
} else if needsMaxUsage(reqProcessor.Flags[utils.MetaRALs]) {
cgrEv.Event[utils.Usage] = rply.MaxUsage // make sure the CDR reflects the debit
}
agReq.setCGRReply(rply, err)
case utils.MetaCDRs: // allow this method
case utils.MetaRadauth:
if pass, err := radauthReq(reqProcessor.Flags, req, agReq, rpl); err != nil {
agReq.CGRReply.Map[utils.Error] = utils.NewLeafNode(err.Error())
} else if !pass {
agReq.CGRReply.Map[utils.Error] = utils.NewLeafNode(utils.RadauthFailed)
}
}
// separate request so we can capture the Terminate/Event also here
if reqProcessor.Flags.GetBool(utils.MetaCDRs) {
var rplyCDRs string
if err = ra.connMgr.Call(ra.ctx, ra.cgrCfg.RadiusAgentCfg().SessionSConns, utils.SessionSv1ProcessCDR,
cgrEv, &rplyCDRs); err != nil {
agReq.CGRReply.Map[utils.Error] = utils.NewLeafNode(err.Error())
}
}
if err := agReq.SetFields(reqProcessor.ReplyFields); err != nil {
return false, err
}
if reqProcessor.Flags.Has(utils.MetaLog) {
utils.Logger.Info(
fmt.Sprintf("<%s> LOG, Radius reply: %s",
utils.RadiusAgent, agReq.Reply))
}
if reqType == utils.MetaDryRun {
utils.Logger.Info(
fmt.Sprintf("<%s> DRY_RUN, Radius reply: %s",
utils.RadiusAgent, agReq.Reply))
}
return true, nil
}
func (ra *RadiusAgent) ListenAndServe(stopChan <-chan struct{}) (err error) {
errListen := make(chan error, 2)
for uri, server := range ra.rsAuth {
ra.Add(1)
go func(srv *radigo.Server, uri string) {
defer ra.Done()
utils.Logger.Info(fmt.Sprintf("<%s> Start listening for auth requests on <%s>", utils.RadiusAgent, uri))
if err := srv.ListenAndServe(stopChan); err != nil {
utils.Logger.Warning(fmt.Sprintf("<%s> error <%v>, on ListenAndServe <%s>",
utils.RadiusAgent, err, uri))
if strings.Contains(err.Error(), "address already in use") {
return
}
errListen <- err
}
}(server, uri)
}
for uri, server := range ra.rsAcct {
ra.Add(1)
go func(srv *radigo.Server, uri string) {
defer ra.Done()
utils.Logger.Info(fmt.Sprintf("<%s> Start listening for acct requests on <%s>", utils.RadiusAgent, uri))
if err := srv.ListenAndServe(stopChan); err != nil {
utils.Logger.Warning(fmt.Sprintf("<%s> error <%v>, on ListenAndServe <%s>",
utils.RadiusAgent, err, uri))
if strings.Contains(err.Error(), "address already in use") {
return
}
errListen <- err
}
}(server, uri)
}
err = <-errListen
return
}
// V1DisconnectPeer is needed to satisfy the sessions.BiRPClient interface
func (*RadiusAgent) V1DisconnectPeer(_ *context.Context, _ *utils.DPRArgs, _ *string) error {
return utils.ErrNotImplemented
}
// V1GetActiveSessionIDs is needed to satisfy the sessions.BiRPClient interface
func (*RadiusAgent) V1GetActiveSessionIDs(_ *context.Context, _ string, _ *[]*sessions.SessionID) error {
return utils.ErrNotImplemented
}
// V1DisconnectSession remotely disconnects a session by making use of the RADIUS Disconnect Message functionality.
func (ra *RadiusAgent) V1DisconnectSession(_ *context.Context, attr utils.AttrDisconnectSession, reply *string) error {
ifaceOriginID, has := attr.EventStart[utils.OriginID]
if !has {
return utils.NewErrMandatoryIeMissing(utils.OriginID)
}
originID := utils.IfaceAsString(ifaceOriginID)
cachedPacket, has := engine.Cache.Get(utils.CacheRadiusPackets, originID)
if !has {
return fmt.Errorf("failed to retrieve packet from cache: %w", utils.ErrNotFound)
}
packet := cachedPacket.(*radigo.Packet)
reqVars := &utils.DataNode{
Type: utils.NMMapType,
Map: map[string]*utils.DataNode{
utils.DisconnectCause: utils.NewLeafNode(attr.Reason),
},
}
agReq := NewAgentRequest(
newRADataProvider(packet), reqVars, nil, nil, nil, nil,
ra.cgrCfg.GeneralCfg().DefaultTenant,
ra.cgrCfg.GeneralCfg().DefaultTimezone,
ra.filterS, nil)
err := agReq.SetFields(ra.cgrCfg.TemplatesCfg()[ra.cgrCfg.RadiusAgentCfg().DMRTemplate])
if err != nil {
return fmt.Errorf("could not set attributes: %w", err)
}
remoteAddr, remoteHost, err := dmRemoteAddr(packet.RemoteAddr().String(), ra.cgrCfg.RadiusAgentCfg().ClientDaAddresses)
if err != nil {
return fmt.Errorf("retrieving remote address failed: %w", err)
}
dynAuthClient, err := radigo.NewClient(utils.UDP, remoteAddr,
ra.dacCfg.secrets.GetSecret(remoteHost),
ra.dacCfg.dicts.GetInstance(remoteHost),
ra.cgrCfg.GeneralCfg().ConnectAttempts, nil, utils.Logger)
if err != nil {
return fmt.Errorf("dynamic authorization client init failed: %w", err)
}
dscReq := dynAuthClient.NewRequest(radigo.DisconnectRequest, 1)
if err = radAppendAttributes(dscReq, agReq.radDAdiscMsg); err != nil {
return errors.New("could not append attributes to the request packet")
}
dscReply, err := dynAuthClient.SendRequest(dscReq)
if err != nil {
return fmt.Errorf("failed to send request: %w", err)
}
switch dscReply.Code {
case radigo.DisconnectACK:
*reply = utils.OK
case radigo.DisconnectNAK:
return errors.New("received DisconnectNAK from RADIUS client")
default:
return errors.New("unexpected reply code")
}
return nil
}
// dmRemoteAddr ranges over the client_da_addresses map and returns the address configured for a
// specific client alongside the host.
func dmRemoteAddr(remoteAddr string, dynAuthAddresses map[string]string) (string, string, error) {
if len(dynAuthAddresses) == 0 {
return "", "", utils.ErrNotFound
}
remoteHost, _, err := net.SplitHostPort(remoteAddr)
if err != nil {
return "", "", err
}
for host, addr := range dynAuthAddresses {
if host == remoteHost {
return addr, host, nil
}
}
return "", "", utils.ErrNotFound
}
// V1ReAuthorize is needed to satisfy the sessions.BiRPClient interface
func (*RadiusAgent) V1ReAuthorize(_ *context.Context, _ string, _ *string) error {
return utils.ErrNotImplemented
}
// V1WarnDisconnect is needed to satisfy the sessions.BiRPClient interface
func (*RadiusAgent) V1WarnDisconnect(_ *context.Context, _ map[string]any, _ *string) error {
return utils.ErrNotImplemented
}