Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rate limit the api calls to httpd.aaa from the accounting requests #8494

Open
wants to merge 9 commits into
base: devel
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions conf/documentation.conf
Original file line number Diff line number Diff line change
Expand Up @@ -1880,6 +1880,19 @@ description=<<EOT
The size of the queue for each worker.
EOT

[radius_configuration.pfacct_rate_limit]
type=toggle
options=enabled|disabled
description=<<EOT
Enable the rate limiting on pfacct to prevent hammering httpd.aaa
EOT

[radius_configuration.pfacct_rate_limit_cache_ttl]
type=numeric
description=<<EOT
If rate limiting on pfacct is enabled, the Time to Live value in minutes represent the time of the accounting cache
EOT

[dns_configuration.record_dns_in_sql]
type=toggle
options=enabled|disabled
Expand Down
8 changes: 8 additions & 0 deletions conf/pf.conf.defaults
Original file line number Diff line number Diff line change
Expand Up @@ -1418,6 +1418,14 @@ pfacct_workers = 0
#
# The size of the queue for each worker.
pfacct_work_queue_size = 1000
# radius_configuration.pfacct_rate_limit
#
# Enable the rate limiting on pfacct to prevent hammering httpd.aaa
pfacct_rate_limit=disabled
# radius_configuration.pfacct_rate_limit_cache_ttl
#
# If rate limiting on pfacct is enabled, the Time to Live value in minutes represent the time of the accounting cache
pfacct_rate_limit_cache_ttl=5

[dns_configuration]
#
Expand Down
63 changes: 37 additions & 26 deletions go/cmd/pfacct/pfacct.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,31 +38,35 @@ type radiusRequest struct {

type PfAcct struct {
RadiusStatements
TimeDuration time.Duration
Db *sql.DB
AllowedNetworks []net.IPNet
NetFlowPort string
NetFlowAddress string
Management pfconfigdriver.ManagementNetwork
AAAClient *jsonrpc2.Client
LoggerCtx context.Context
Dispatcher *Dispatcher
SwitchInfoCache *cache.Cache
NodeSessionCache *cache.Cache
AcctSessionCache *cache.Cache
StatsdAddress string
StatsdOption statsd.Option
StatsdClient *statsd.Client
radiusRequests []chan<- radiusRequest
overflows []atomic.Int64
localSecret string
StatsdOnce tryableonce.TryableOnce
isProxied bool
radiusdAcctEnabled bool
AllNetworks bool
ProcessBandwidthAcct bool
RadiusWorkers int
RadiusWorkQueueSize int
TimeDuration time.Duration
Db *sql.DB
AllowedNetworks []net.IPNet
NetFlowPort string
NetFlowAddress string
Management pfconfigdriver.ManagementNetwork
AAAClient *jsonrpc2.Client
LoggerCtx context.Context
Dispatcher *Dispatcher
SwitchInfoCache *cache.Cache
NodeSessionCache *cache.Cache
AcctSessionCache *cache.Cache
RateLimitCache *cache.Cache
MacNasCache *cache.Cache
RateLimit bool
PfacctRateLimitCacheTtl int
StatsdAddress string
StatsdOption statsd.Option
StatsdClient *statsd.Client
radiusRequests []chan<- radiusRequest
overflows []atomic.Int64
localSecret string
StatsdOnce tryableonce.TryableOnce
isProxied bool
radiusdAcctEnabled bool
AllNetworks bool
ProcessBandwidthAcct bool
RadiusWorkers int
RadiusWorkQueueSize int
}

func NewPfAcct() *PfAcct {
Expand Down Expand Up @@ -90,6 +94,7 @@ func NewPfAcct() *PfAcct {
pfAcct.SwitchInfoCache = cache.New(5*time.Minute, 10*time.Minute)
pfAcct.NodeSessionCache = cache.New(cache.NoExpiration, cache.NoExpiration)
pfAcct.AcctSessionCache = cache.New(5*time.Minute, 10*time.Minute)

pfAcct.LoggerCtx = ctx
pfAcct.RadiusStatements.Setup(pfAcct.Db)

Expand Down Expand Up @@ -168,7 +173,13 @@ func (pfAcct *PfAcct) SetupConfig(ctx context.Context) {
var RadiusConfiguration pfconfigdriver.PfConfRadiusConfiguration
pfconfigdriver.FetchDecodeSocket(ctx, &RadiusConfiguration)
pfAcct.ProcessBandwidthAcct = sharedutils.IsEnabled(RadiusConfiguration.ProcessBandwidthAccounting)

pfAcct.RateLimit = sharedutils.IsEnabled(RadiusConfiguration.PfacctRateLimit)
pfAcct.PfacctRateLimitCacheTtl = 5
if i, err := strconv.Atoi(RadiusConfiguration.PfacctRateLimitCacheTtl); err != nil {
pfAcct.PfacctRateLimitCacheTtl = i
}
pfAcct.RateLimitCache = cache.New(time.Duration(pfAcct.PfacctRateLimitCacheTtl)*time.Minute, 10*time.Minute)
pfAcct.MacNasCache = cache.New(time.Duration(pfAcct.PfacctRateLimitCacheTtl)*time.Minute, 10*time.Minute)
if !pfAcct.ProcessBandwidthAcct {
logInfo(ctx, "Not processing bandwidth accounting records. To enable set radius_configuration.process_bandwidth_accounting = enabled")
}
Expand Down
99 changes: 97 additions & 2 deletions go/cmd/pfacct/radius.go
Original file line number Diff line number Diff line change
Expand Up @@ -333,9 +333,104 @@ func (h *PfAcct) sendRadiusAccountingCall(r *radius.Request) {
logWarn(ctx, fmt.Sprintf("Empty NAS-IP-Address, using the source IP address of the packet (%s)", attr["NAS-IP-Address"]))
}

if err := h.AAAClient.Notify(ctx, "radius_accounting", attr); err != nil {
logError(ctx, err.Error())
status := rfc2866.AcctStatusType_Get(r.Packet)

if h.RateLimit && h.rateLimit(attr, status) {
if err := h.AAAClient.Notify(ctx, "radius_accounting", attr); err != nil {
logError(ctx, err.Error())
}
} else if !h.RateLimit {
if err := h.AAAClient.Notify(ctx, "radius_accounting", attr); err != nil {
logError(ctx, err.Error())
}
}
}

func (h *PfAcct) rateLimit(attr map[string]interface{}, status rfc2866.AcctStatusType) bool {

NasIp, NasIPExists := attr["NAS-IP-Address"]
CalledStationId, CalledStationIdExists := attr["Called-Station-Id"]
CallingStationId, CallingStationIdExists := attr["Calling-Station-Id"]
FramedIPAddress, FramedIPAddressExists := attr["Framed-IP-Addres"]
var key string
var keyStart string
var macLocValue string
var macAddress mac.Mac
// No CallingStationId
if !CallingStationIdExists {
return false
} else {
macAddress, _ = mac.NewFromString(CallingStationId.(string))
}

macOldLocation, macOldLocationExists := h.MacNasCache.Get(macAddress.String())

// Generate the keys
if CalledStationIdExists {
key = rfc2866.AcctStatusType_Strings[status] + "-" + CalledStationId.(string) + "-" + CallingStationId.(string)
keyStart = "Start" + "-" + CalledStationId.(string) + "-" + CallingStationId.(string)
macLocValue = CalledStationId.(string)

} else if NasIPExists {
key = rfc2866.AcctStatusType_Strings[status] + "-" + NasIp.(string) + "-" + CallingStationId.(string)
keyStart = "Start" + "-" + NasIp.(string) + "-" + CallingStationId.(string)
macLocValue = NasIp.(string)
} else {
return true
}

if rfc2866.AcctStatusType_Strings[status] == "Start" {
ip, exists := h.RateLimitCache.Get(key)
if !exists {
if FramedIPAddressExists {
h.RateLimitCache.Set(key, FramedIPAddress, time.Duration(h.PfacctRateLimitCacheTtl)*time.Minute)
} else {
h.RateLimitCache.Set(key, "0.0.0.0", time.Duration(h.PfacctRateLimitCacheTtl)*time.Minute)
}
// Purge old Start entry
if macOldLocationExists && macOldLocation != macLocValue {
// Replace the location
h.MacNasCache.Set(macAddress.String(), macLocValue, time.Duration(h.PfacctRateLimitCacheTtl)*time.Minute)
h.RateLimitCache.Delete("Start" + "-" + macOldLocation.(string) + "-" + CallingStationId.(string))
}
return true
} else {
if FramedIPAddressExists && FramedIPAddress != ip.(string) {
h.RateLimitCache.Set(key, FramedIPAddress, time.Duration(h.PfacctRateLimitCacheTtl)*time.Minute)
h.MacNasCache.Set(macAddress.String(), macLocValue, time.Duration(h.PfacctRateLimitCacheTtl)*time.Minute)
return true
} else {
return false
}
}
}
if rfc2866.AcctStatusType_Strings[status] == "Interim-Update" {
// Verify that we already got a start
ip, exists := h.RateLimitCache.Get(keyStart)
if exists {
if FramedIPAddressExists && FramedIPAddress != ip.(string) {
h.RateLimitCache.Set(keyStart, FramedIPAddress, time.Duration(h.PfacctRateLimitCacheTtl)*time.Minute)
h.MacNasCache.Set(macAddress.String(), macLocValue, time.Duration(h.PfacctRateLimitCacheTtl)*time.Minute)
return true
} else {
return false
}
}
}
if rfc2866.AcctStatusType_Strings[status] == "Stop" {
// Verify that we already got a start
ip, exists := h.RateLimitCache.Get(keyStart)
if exists {
if FramedIPAddressExists && FramedIPAddress != ip.(string) {
h.RateLimitCache.Set(keyStart, FramedIPAddress, time.Duration(h.PfacctRateLimitCacheTtl)*time.Minute)
h.MacNasCache.Set(macAddress.String(), macLocValue, time.Duration(h.PfacctRateLimitCacheTtl)*time.Minute)
return true
} else {
return false
}
}
}
return false
}

func (h *PfAcct) radiusListen(w *sync.WaitGroup) *radius.PacketServer {
Expand Down
2 changes: 2 additions & 0 deletions go/pfconfigdriver/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -711,6 +711,8 @@ type PfConfRadiusConfiguration struct {
ProcessBandwidthAccounting string `json:"process_bandwidth_accounting"`
PfacctWorkers string `json:"pfacct_workers"`
PfacctWorkQueueSize string `json:"pfacct_work_queue_size"`
PfacctRateLimit string `json:"pfacct_rate_limit"`
PfacctRateLimitCacheTtl string `json:"pfacct_rate_limit_cache_ttl"`
}

type PfQueueConfig struct {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,18 @@
:text="$i18n.t('The size of the queue for each worker. Requires a restart of pfacct to be effective')"
/>

<form-group-pfacct-rate-limit namespace="pfacct_rate_limit"
:column-label="$i18n.t('Pfacct Rate Limiting')"
:text="$i18n.t('Enable the rate limiting in pfacct to avoid hammering httpd.aaa. Requires a restart of pfacct to be effective')"
enabled-value="enabled"
disabled-value="disabled"
/>

<form-group-pfacct-rate-limit-cache-ttl namespace="pfacct_rate_limit_cache_ttl"
:column-label="$i18n.t('Pfacct Rate Limiting Time to Live')"
:text="$i18n.t('Time to Live value in minutes, should be a little bit more than the Interim Update value. Requires a restart of pfacct to be effective')"
/>

<form-group-radius-attributes namespace="radius_attributes"
:column-label="$i18n.t('RADIUS attributes')"
:text="$i18n.t('List of RADIUS attributes that can be used in the sources configuration.')"
Expand Down Expand Up @@ -159,6 +171,8 @@ import {
FormGroupNtlmRedisCache,
FormGroupPfacctWorkers,
FormGroupPfacctWorkQueueSize,
FormGroupPfacctRateLimit,
FormGroupPfacctRateLimitCacheTtl,
FormGroupProcessBandwidthAccounting,
FormGroupRadiusAttributes,
FormGroupRecordAccountingInSql,
Expand Down Expand Up @@ -188,7 +202,9 @@ const components = {
FormGroupProcessBandwidthAccounting,
FormGroupUsernameAttributes,
FormGroupPfacctWorkers,
FormGroupPfacctWorkQueueSize
FormGroupPfacctWorkQueueSize,
FormGroupPfacctRateLimit,
FormGroupPfacctRateLimitCacheTtl
}

export const props = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ export {
BaseFormGroupTextarea as FormGroupUsernameAttributes,
BaseFormGroupInputNumber as FormGroupPfacctWorkers,
BaseFormGroupInputNumber as FormGroupPfacctWorkQueueSize,
BaseFormGroupSwitch as FormGroupPfacctRateLimit,
BaseFormGroupInputNumber as FormGroupPfacctRateLimitCacheTtl,

BaseViewResource as BaseView,
TheForm,
Expand Down