mirror of
https://github.com/cgrates/cgrates.git
synced 2026-02-22 15:48:44 +05:00
The one from sessions takes an additional event alongside the SessionFilter, while the one from agents will accept a CGREvent instead of a simple originID string The additional event sent to SessionSv1ReAuthorize will be merged with the EventStart event from the matched session and can be used when building server initiated requests from the *req map. The initial packet which was initially inside *req, will be moved to the *oreq ExtraDP (stands for original request).
610 lines
23 KiB
Go
610 lines
23 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 CoA/Disconnect Requests.
|
|
if ra.cgrCfg.RadiusAgentCfg().DMRTemplate != "" || ra.cgrCfg.RadiusAgentCfg().CoATemplate != "" {
|
|
|
|
// 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)
|
|
|
|
reqVars := &utils.DataNode{
|
|
Type: utils.NMMapType,
|
|
Map: map[string]*utils.DataNode{
|
|
utils.DisconnectCause: utils.NewLeafNode(attr.Reason),
|
|
},
|
|
}
|
|
|
|
replyCode, err := ra.sendRadDaReq(radigo.DisconnectRequest, ra.cgrCfg.RadiusAgentCfg().DMRTemplate, originID, nil, reqVars)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch replyCode {
|
|
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
|
|
}
|
|
|
|
// V1ReAuthorize updates session authorization using RADIUS CoA functionality.
|
|
func (ra *RadiusAgent) V1ReAuthorize(_ *context.Context, cgrEv utils.CGREvent, reply *string) error {
|
|
originID, err := cgrEv.FieldAsString(utils.OriginID)
|
|
if err != nil {
|
|
return fmt.Errorf("could not retrieve OriginID: %w", err)
|
|
}
|
|
if originID == "" {
|
|
return utils.NewErrMandatoryIeMissing(utils.OriginID)
|
|
}
|
|
replyCode, err := ra.sendRadDaReq(radigo.CoARequest, ra.cgrCfg.RadiusAgentCfg().CoATemplate,
|
|
originID, utils.MapStorage(cgrEv.Event), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch replyCode {
|
|
case radigo.CoAACK:
|
|
*reply = utils.OK
|
|
case radigo.CoANAK:
|
|
return errors.New("received CoANAK from RADIUS client")
|
|
default:
|
|
return errors.New("unexpected reply code")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// sendRadDaReq prepares and sends a Radius CoA/Disconnect Request and returns the reply code or an error.
|
|
func (ra *RadiusAgent) sendRadDaReq(requestType radigo.PacketCode, requestTemplate, sessionID string, requestEv utils.DataProvider, requestVars *utils.DataNode) (radigo.PacketCode, error) {
|
|
cachedPacket, has := engine.Cache.Get(utils.CacheRadiusPackets, sessionID)
|
|
if !has {
|
|
return 0, fmt.Errorf("failed to retrieve packet from cache: %w", utils.ErrNotFound)
|
|
}
|
|
packet := cachedPacket.(*radigo.Packet)
|
|
|
|
agReq := NewAgentRequest(
|
|
requestEv, requestVars, nil, nil, nil, nil,
|
|
ra.cgrCfg.GeneralCfg().DefaultTenant,
|
|
ra.cgrCfg.GeneralCfg().DefaultTimezone,
|
|
ra.filterS, map[string]utils.DataProvider{
|
|
utils.MetaOReq: newRADataProvider(packet),
|
|
})
|
|
err := agReq.SetFields(ra.cgrCfg.TemplatesCfg()[requestTemplate])
|
|
if err != nil {
|
|
return 0, fmt.Errorf("could not set attributes: %w", err)
|
|
}
|
|
|
|
remoteAddr, remoteHost, err := dmRemoteAddr(packet.RemoteAddr().String(), ra.cgrCfg.RadiusAgentCfg().ClientDaAddresses)
|
|
if err != nil {
|
|
return 0, 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 0, fmt.Errorf("dynamic authorization client init failed: %w", err)
|
|
}
|
|
dynAuthReq := dynAuthClient.NewRequest(requestType, 1)
|
|
if err = radAppendAttributes(dynAuthReq, agReq.radDAReq); err != nil {
|
|
return 0, fmt.Errorf("could not append attributes to the request packet: %w", err)
|
|
}
|
|
dynAuthReply, err := dynAuthClient.SendRequest(dynAuthReq)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("failed to send request: %w", err)
|
|
}
|
|
return dynAuthReply.Code, 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
|
|
}
|
|
|
|
// V1WarnDisconnect is needed to satisfy the sessions.BiRPClient interface
|
|
func (*RadiusAgent) V1WarnDisconnect(_ *context.Context, _ map[string]any, _ *string) error {
|
|
return utils.ErrNotImplemented
|
|
}
|