diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 09:49:46 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 09:49:46 +0000 |
commit | 50b37d4a27d3295a29afca2286f1a5a086142cec (patch) | |
tree | 9212f763934ee090ef72d823f559f52ce387f268 /raddb/mods-config | |
parent | Initial commit. (diff) | |
download | freeradius-upstream.tar.xz freeradius-upstream.zip |
Adding upstream version 3.2.1+dfsg.upstream/3.2.1+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'raddb/mods-config')
113 files changed, 12842 insertions, 0 deletions
diff --git a/raddb/mods-config/README.rst b/raddb/mods-config/README.rst new file mode 100644 index 0000000..abb4c8d --- /dev/null +++ b/raddb/mods-config/README.rst @@ -0,0 +1,22 @@ +The mods-config Directory +========================= + +This directory contains module-specific configuration files. These +files are in a format different from the one used by the main +`radiusd.conf` files. Earlier versions of the server had many +module-specific files in the main `raddb` directory. The directory +contained many files, and it was not clear which files did what. + +For Version 3 of FreeRADIUS, we have moved to a consistent naming +scheme. Each module-specific configuration file is placed in this +directory, in a subdirectory named for the module. Where necessary, +files in the subdirectory have been named for the processing section +where they are used. + +For example, the `users` file is now located in +`mods-config/files/authorize`. That filename tells us three things: + +1. The file is used in the `authorize` section. +2. The file is used by the `files` module. +3. It is a "module configuration" file, which is a specific format. + diff --git a/raddb/mods-config/attr_filter/access_challenge b/raddb/mods-config/attr_filter/access_challenge new file mode 100644 index 0000000..12ed619 --- /dev/null +++ b/raddb/mods-config/attr_filter/access_challenge @@ -0,0 +1,19 @@ +# +# Configuration file for the rlm_attr_filter module. +# Please see rlm_attr_filter(5) manpage for more information. +# +# $Id$ +# +# This configuration file is used to remove almost all of the +# attributes From an Access-Challenge message. The RFCs say +# that an Access-Challenge packet can contain only a few +# attributes. We enforce that here. +# +DEFAULT + EAP-Message =* ANY, + State =* ANY, + Message-Authenticator =* ANY, + Reply-Message =* ANY, + Proxy-State =* ANY, + Session-Timeout =* ANY, + Idle-Timeout =* ANY diff --git a/raddb/mods-config/attr_filter/access_reject b/raddb/mods-config/attr_filter/access_reject new file mode 100644 index 0000000..47f167b --- /dev/null +++ b/raddb/mods-config/attr_filter/access_reject @@ -0,0 +1,18 @@ +# +# Configuration file for the rlm_attr_filter module. +# Please see rlm_attr_filter(5) manpage for more information. +# +# $Id$ +# +# This configuration file is used to remove almost all of the attributes +# From an Access-Reject message. The RFCs say that an Access-Reject +# packet can contain only a few attributes. We enforce that here. +# +DEFAULT + EAP-Message =* ANY, + State =* ANY, + Message-Authenticator =* ANY, + Error-Cause =* ANY, + Reply-Message =* ANY, + MS-CHAP-Error =* ANY, + Proxy-State =* ANY diff --git a/raddb/mods-config/attr_filter/accounting_response b/raddb/mods-config/attr_filter/accounting_response new file mode 100644 index 0000000..01e9c6f --- /dev/null +++ b/raddb/mods-config/attr_filter/accounting_response @@ -0,0 +1,16 @@ +# +# Configuration file for the rlm_attr_filter module. +# Please see rlm_attr_filter(5) manpage for more information. +# +# $Id$ +# +# This configuration file is used to remove almost all of the attributes +# From an Accounting-Response message. The RFC's say that an +# Accounting-Response packet can contain only a few attributes. +# We enforce that here. +# +DEFAULT + Vendor-Specific =* ANY, + Message-Authenticator =* ANY, + Error-Cause =* ANY, + Proxy-State =* ANY diff --git a/raddb/mods-config/attr_filter/coa b/raddb/mods-config/attr_filter/coa new file mode 100644 index 0000000..89cea2e --- /dev/null +++ b/raddb/mods-config/attr_filter/coa @@ -0,0 +1,22 @@ +# +# Configuration file for the rlm_attr_filter module. +# Please see rlm_attr_filter(5) manpage for more information. +# +# $Id$ +# +# This configuration file is used to remove attributes From an +# CoA-Request or Disconnect-Request message. We have specified +# a sample list here. This will have to be modified to add +# attributes needed by your local configuration. +# +DEFAULT + User-Name =* ANY, + NAS-IP-Address =* ANY, + NAS-IPv6-Address =* ANY, + NAS-Port =* ANY, + NAS-Identifier =* ANY, + NAS-Port-Type =* ANY, + Calling-Station-Id =* ANY, + State =* ANY, + Message-Authenticator =* ANY, + Proxy-State =* ANY diff --git a/raddb/mods-config/attr_filter/post-proxy b/raddb/mods-config/attr_filter/post-proxy new file mode 100644 index 0000000..5d889ea --- /dev/null +++ b/raddb/mods-config/attr_filter/post-proxy @@ -0,0 +1,119 @@ +# +# Configuration file for the rlm_attr_filter module. +# Please see rlm_attr_filter(5) manpage for more information. +# +# $Id$ +# +# This file contains security and configuration information +# for each realm. The first field is the realm name and +# can be up to 253 characters in length. This is followed (on +# the next line) with the list of filter rules to be used to +# decide what attributes and/or values we allow proxy servers +# to pass to the NAS for this realm. +# +# When a proxy-reply packet is received from a home server, +# these attributes and values are tested. Only the first match +# is used unless the "Fall-Through" variable is set to "Yes". +# In that case the rules defined in the DEFAULT case are +# processed as well. +# +# A special realm named "DEFAULT" matches on all realm names. +# You can have only one DEFAULT entry. All entries are processed +# in the order they appear in this file. The first entry that +# matches the login-request will stop processing unless you use +# the Fall-Through variable. +# +# Indented (with the tab character) lines following the first +# line indicate the filter rules. +# +# You can include another `attrs' file with `$INCLUDE attrs.other' +# + +# +# This is a complete entry for realm "fisp". Note that there is no +# Fall-Through entry so that no DEFAULT entry will be used, and the +# server will NOT allow any other a/v pairs other than the ones +# listed here. +# +# These rules allow: +# o Only Framed-User Service-Types ( no telnet, rlogin, tcp-clear ) +# o PPP sessions ( no SLIP, CSLIP, etc. ) +# o dynamic ip assignment ( can't assign a static ip ) +# o an idle timeout value set to 600 seconds (10 min) or less +# o a max session time set to 28800 seconds (8 hours) or less +# +#fisp +# Service-Type == Framed-User, +# Framed-Protocol == PPP, +# Framed-IP-Address == 255.255.255.254, +# Idle-Timeout <= 600, +# Session-Timeout <= 28800 + +# +# This is a complete entry for realm "tisp". Note that there is no +# Fall-Through entry so that no DEFAULT entry will be used, and the +# server will NOT allow any other a/v pairs other than the ones +# listed here. +# +# These rules allow: +# o Only Login-User Service-Type ( no framed/ppp sessions ) +# o Telnet sessions only ( no rlogin, tcp-clear ) +# o Login host of 192.0.2.1 +# +#tisp +# Service-Type == Login-User, +# Login-Service == Telnet, +# Login-TCP-Port == 23, +# Login-IP-Host == 192.0.2.1 + +# +# The following example can be used for a home server which is only +# allowed to supply a Reply-Message, a Session-Timeout attribute of +# maximum 86400, a Idle-Timeout attribute of maximum 600 and a +# Acct-Interim-Interval attribute between 300 and 3600. +# All other attributes sent back will be filtered out. +# +#strictrealm +# Reply-Message =* ANY, +# Session-Timeout <= 86400, +# Idle-Timeout <= 600, +# Acct-Interim-Interval >= 300, +# Acct-Interim-Interval <= 3600 + +# +# This is a complete entry for realm "spamrealm". Fall-Through is used, +# so that the DEFAULT filter rules are used in addition to these. +# +# These rules allow: +# o Force the application of Filter-ID attribute to be returned +# in the proxy reply, whether the proxy sent it or not. +# o The standard DEFAULT rules as defined below +# +#spamrealm +# Framed-Filter-Id := "nosmtp.in", +# Fall-Through = Yes + +# +# The rest of this file contains the DEFAULT entry. +# DEFAULT matches with all realm names. (except if the realm previously +# matched an entry with no Fall-Through) +# + +DEFAULT + Framed-IP-Address == 255.255.255.254, + Framed-IP-Netmask == 255.255.255.255, + Framed-MTU >= 576, + Framed-Filter-ID =* ANY, + Reply-Message =* ANY, + Proxy-State =* ANY, + EAP-Message =* ANY, + Message-Authenticator =* ANY, + MS-MPPE-Recv-Key =* ANY, + MS-MPPE-Send-Key =* ANY, + MS-CHAP-MPPE-Keys =* ANY, + State =* ANY, + Session-Timeout <= 28800, + Idle-Timeout <= 600, + Calling-Station-Id =* ANY, + Operator-Name =* ANY, + Port-Limit <= 2 diff --git a/raddb/mods-config/attr_filter/pre-proxy b/raddb/mods-config/attr_filter/pre-proxy new file mode 100644 index 0000000..47b0126 --- /dev/null +++ b/raddb/mods-config/attr_filter/pre-proxy @@ -0,0 +1,65 @@ +# +# Configuration file for the rlm_attr_filter module. +# Please see rlm_attr_filter(5) manpage for more information. +# +# $Id$ +# +# This file contains security and configuration information +# for each realm. It can be used be an rlm_attr_filter module +# instance to filter attributes before sending packets to the +# home server of a realm. +# +# When a packet is sent to a home server, these attributes +# and values are tested. Only the first match is used unless +# the "Fall-Through" variable is set to "Yes". In that case +# the rules defined in the DEFAULT case are processed as well. +# +# A special realm named "DEFAULT" matches on all realm names. +# You can have only one DEFAULT entry. All entries are processed +# in the order they appear in this file. The first entry that +# matches the login-request will stop processing unless you use +# the Fall-Through variable. +# +# The first line indicates the realm to which the rules apply. +# Indented (with the tab character) lines following the first +# line indicate the filter rules. +# + +# This is a complete entry for 'nochap' realm. It allows to send very +# basic attributes to the home server. Note that there is no Fall-Through +# entry so that no DEFAULT entry will be used. Only the listed attributes +# will be sent in the packet, all other attributes will be filtered out. +# +#nochap +# User-Name =* ANY, +# User-Password =* ANY, +# NAS-IP-Address =* ANY, +# NAS-Identifier =* ANY + +# The entry for the 'brokenas' realm removes the attribute NAS-Port-Type +# if its value is different from 'Ethernet'. Then the default rules are +# applied. +# +#brokenas +# NAS-Port-Type == Ethernet +# Fall-Through = Yes + +# The rest of this file contains the DEFAULT entry. +# DEFAULT matches with all realm names. + +DEFAULT + User-Name =* ANY, + User-Password =* ANY, + CHAP-Password =* ANY, + CHAP-Challenge =* ANY, + MS-CHAP-Challenge =* ANY, + MS-CHAP-Response =* ANY, + EAP-Message =* ANY, + Message-Authenticator =* ANY, + State =* ANY, + NAS-IP-Address =* ANY, + NAS-Identifier =* ANY, + Operator-Name =* ANY, + Calling-Station-Id =* ANY, + Chargeable-User-Identity =* ANY, + Proxy-State =* ANY diff --git a/raddb/mods-config/files/accounting b/raddb/mods-config/files/accounting new file mode 100644 index 0000000..eaf952a --- /dev/null +++ b/raddb/mods-config/files/accounting @@ -0,0 +1,27 @@ +# +# $Id$ +# +# This is like the 'users' file, but it is processed only for +# accounting packets. +# + +# Select between different accounting methods based for example on the +# Realm, the Huntgroup-Name or any combinaison of the attribute/value +# pairs contained in an accounting packet. +# +# You will need to add an "Acct-Type foo {...}" subsection to the +# main "accounting" section in order for these sample configurations +# to work. +# +#DEFAULT Realm == "foo.net", Acct-Type := foo +# +#DEFAULT Huntgroup-Name == "wifi", Acct-Type := wifi +# +#DEFAULT Client-IP-Address == 10.0.0.1, Acct-Type := other +# +#DEFAULT Acct-Status-Type == Start, Acct-Type := start + +# Replace the User-Name with the Stripped-User-Name, if it exists. +# +#DEFAULT +# User-Name := "%{%{Stripped-User-Name}:-%{User-Name}}" diff --git a/raddb/mods-config/files/authorize b/raddb/mods-config/files/authorize new file mode 100644 index 0000000..ddf805f --- /dev/null +++ b/raddb/mods-config/files/authorize @@ -0,0 +1,206 @@ +# +# Configuration file for the rlm_files module. +# Please see rlm_files(5) manpage for more information. +# +# This file contains authentication security and configuration +# information for each user. Accounting requests are NOT processed +# through this file. Instead, see 'accounting', in this directory. +# +# The first field is the user's name and can be up to +# 253 characters in length. This is followed (on the same line) with +# the list of authentication requirements for that user. This can +# include password, comm server name, comm server port number, protocol +# type (perhaps set by the "hints" file), and huntgroup name (set by +# the "huntgroups" file). +# +# If you are not sure why a particular reply is being sent by the +# server, then run the server in debugging mode (radiusd -X), and +# you will see which entries in this file are matched. +# +# When an authentication request is received from the comm server, +# these values are tested. Only the first match is used unless the +# "Fall-Through" variable is set to "Yes". +# +# A special user named "DEFAULT" matches on all usernames. +# You can have several DEFAULT entries. All entries are processed +# in the order they appear in this file. The first entry that +# matches the login-request will stop processing unless you use +# the Fall-Through variable. +# +# Indented (with the tab character) lines following the first +# line indicate the configuration values to be passed back to +# the comm server to allow the initiation of a user session. +# This can include things like the PPP configuration values +# or the host to log the user onto. +# +# You can include another `users' file with `$INCLUDE users.other' + +# +# For a list of RADIUS attributes, and links to their definitions, +# see: http://www.freeradius.org/rfc/attributes.html +# +# Entries below this point are examples included in the server for +# educational purposes. They may be deleted from the deployed +# configuration without impacting the operation of the server. +# + +# +# Deny access for a specific user. Note that this entry MUST +# be before any other 'Auth-Type' attribute which results in the user +# being authenticated. +# +# Note that there is NO 'Fall-Through' attribute, so the user will not +# be given any additional resources. +# +#lameuser Auth-Type := Reject +# Reply-Message = "Your account has been disabled." + +# +# Deny access for a group of users. +# +# Note that there is NO 'Fall-Through' attribute, so the user will not +# be given any additional resources. +# +#DEFAULT Group == "disabled", Auth-Type := Reject +# Reply-Message = "Your account has been disabled." +# + +# +# This is a complete entry for "steve". Note that there is no Fall-Through +# entry so that no DEFAULT entry will be used, and the user will NOT +# get any attributes in addition to the ones listed here. +# +#steve Cleartext-Password := "testing" +# Service-Type = Framed-User, +# Framed-Protocol = PPP, +# Framed-IP-Address = 172.16.3.33, +# Framed-IP-Netmask = 255.255.255.0, +# Framed-Routing = Broadcast-Listen, +# Framed-Filter-Id = "std.ppp", +# Framed-MTU = 1500, +# Framed-Compression = Van-Jacobsen-TCP-IP + +# +# The canonical testing user which is in most of the +# examples. +# +#bob Cleartext-Password := "hello" +# Reply-Message := "Hello, %{User-Name}" +# + +# +# This is an entry for a user with a space in their name. +# Note the double quotes surrounding the name. If you have +# users with spaces in their names, you must also change +# the "filter_username" policy to allow spaces. +# +# See raddb/policy.d/filter, filter_username {} section. +# +#"John Doe" Cleartext-Password := "hello" +# Reply-Message = "Hello, %{User-Name}" + +# +# Dial user back and telnet to the default host for that port +# +#Deg Cleartext-Password := "ge55ged" +# Service-Type = Callback-Login-User, +# Login-IP-Host = 0.0.0.0, +# Callback-Number = "9,5551212", +# Login-Service = Telnet, +# Login-TCP-Port = Telnet + +# +# Another complete entry. After the user "dialbk" has logged in, the +# connection will be broken and the user will be dialed back after which +# he will get a connection to the host "timeshare1". +# +#dialbk Cleartext-Password := "callme" +# Service-Type = Callback-Login-User, +# Login-IP-Host = timeshare1, +# Login-Service = PortMaster, +# Callback-Number = "9,1-800-555-1212" + +# +# user "swilson" will only get a static IP number if he logs in with +# a framed protocol on a terminal server in Alphen (see the huntgroups file). +# +# Note that by setting "Fall-Through", other attributes will be added from +# the following DEFAULT entries +# +#swilson Service-Type == Framed-User, Huntgroup-Name == "alphen" +# Framed-IP-Address = 192.0.2.65, +# Fall-Through = Yes + +# +# If the user logs in as 'username.shell', then authenticate them +# using the default method, give them shell access, and stop processing +# the rest of the file. +# +#DEFAULT Suffix == ".shell" +# Service-Type = Login-User, +# Login-Service = Telnet, +# Login-IP-Host = your.shell.machine + + +# +# The rest of this file contains the several DEFAULT entries. +# DEFAULT entries match with all login names. +# Note that DEFAULT entries can also Fall-Through (see first entry). +# A name-value pair from a DEFAULT entry will _NEVER_ override +# an already existing name-value pair. +# + +# Sample defaults for all framed connections. +# +#DEFAULT Service-Type == Framed-User +# Framed-IP-Address = 255.255.255.254, +# Framed-MTU = 576, +# Service-Type = Framed-User, +# Fall-Through = Yes + +# +# Default for PPP: dynamic IP address, PPP mode, VJ-compression. +# NOTE: we do not use Hint = "PPP", since PPP might also be auto-detected +# by the terminal server in which case there may not be a "P" suffix. +# The terminal server sends "Framed-Protocol = PPP" for auto PPP. +# +DEFAULT Framed-Protocol == PPP + Framed-Protocol = PPP, + Framed-Compression = Van-Jacobson-TCP-IP + +# +# Default for CSLIP: dynamic IP address, SLIP mode, VJ-compression. +# +DEFAULT Hint == "CSLIP" + Framed-Protocol = SLIP, + Framed-Compression = Van-Jacobson-TCP-IP + +# +# Default for SLIP: dynamic IP address, SLIP mode. +# +DEFAULT Hint == "SLIP" + Framed-Protocol = SLIP + +# +# Last default: rlogin to our main server. +# +#DEFAULT +# Service-Type = Login-User, +# Login-Service = Rlogin, +# Login-IP-Host = shellbox.ispdomain.com + +# # +# # Last default: shell on the local terminal server. +# # +# DEFAULT +# Service-Type = Administrative-User + + +# On no match, the user is denied access. + + +######################################################### +# You should add test accounts to the TOP of this file! # +# See the example user "bob" above. # +######################################################### + diff --git a/raddb/mods-config/files/dhcp b/raddb/mods-config/files/dhcp new file mode 100644 index 0000000..04f37b5 --- /dev/null +++ b/raddb/mods-config/files/dhcp @@ -0,0 +1,153 @@ +# +# This configuration file that may be used by multiple instances of rlm_files +# to set reply and control options for defining DHCP replies. +# +# The content of this file is all made up and needs to be set appropriate to +# the network being served. +# + +############################################ +# Global and network-specific parameters # +############################################ + +# +# Note: This section is matched by calling the dhcp_network instance of the +# files module. +# + + +# +# Default options that can be overridden by subsequent matches. +# +network + DHCP-Domain-Name-Server := 192.0.1.100, + DHCP-Domain-Name-Server += 192.0.1.101, + DHCP-Time-Server := 192.0.1.200, + DHCP-Domain-Name := "example.org", + DHCP-IP-Address-Lease-Time := 7200, + Fall-Through := yes + + +# +# The following examples set options specific to the Layer 2 network, matched +# on whether the internal attribute DHCP-Network-Subnet (that acts as a +# network identifier) is within the indicated range. This is equivalent to a +# "shared-network" or "multinet" configuration (i.e. one that is possibly +# composed of multiple subnets) as defined by some other DHCP servers. +# + +# +# Here is an example for a network containing a single IP subnet. We can set +# the network-specific options *and* we directly set the DHCP-Subnet-Mask, +# DHCP-Router-Address and DHCP-Broadcast-Address since it is a common reply +# parameter for all DHCP requests originating from this network. +# +# The use of the ^= "prepend" operator for setting DHCP-Domain-Name-Server +# results in this new value being inserted at the start of the list, meaning +# this will become the first DNS server presented in the reply. +# +# Note: If the architecture has only a single subnet for each Layer 2 network +# then by placing all subnet-related options here we can avoid calling the +# dhcp_subnet policy after IP allocation. +# +network DHCP-Network-Subnet < 10.20.0.0/16, Pool-Name := "smalldept" + DHCP-IP-Address-Lease-Time := 3600, + DHCP-Domain-Name := "smalldept.example.org", + DHCP-Subnet-Mask := 255.255.0.0, + DHCP-Router-Address := 10.20.0.1, + DHCP-Domain-Name-Server ^= 10.20.0.2, + DHCP-Broadcast-Address := 10.20.255.255 + +# +# Here is an example for a network that consists of multiple IP subnets, each +# of which is valid for a DHCP request originating from the network. We set +# the Pool-Name parameter to identify a single pool that contains the IP +# address within each subnet, any of which is suitable. +# +# We set the options that are common to the network but we defer the setting +# of DHCP-Subnet-Mask, DHCP-Router-Address and DHCP-Broadcast-Address until an +# address has been allocated. Only then do we know which subnet parameters are +# required. See the next section. +# +network DHCP-Network-Subnet < 10.30.0.0/16, Pool-Name := "bigdept" + DHCP-Domain-Name := "bigdept.example.org" + + +# +# Here is an example for a network that has a dedicated pool for admin staff +# and a seperate pool for everything else. +# +network DHCP-Network-Subnet < 192.0.2.0/24, DHCP-Group-Name == "admin", Pool-Name := "admin-only" +network DHCP-Network-Subnet < 192.0.2.0/24, Pool-Name := "general" + + +################################ +# Subnet-specific parameters # +################################ + +# +# Note: This section is matched by calling the dhcp_subnet policy which sets +# DHCP-Network-Subnet to the allocated IP address of the device and then +# calls the dhcp_subnet instance of the files module. +# +# Layer 2 networks many contain multiple subnets, each with their own gateway. +# We call this section *after* the allocation of an IP address (e.g. from a +# single pool containing addresses within multiple equally-valid subnets for +# the network) so that we then know which subnet-specific parameters to +# return. +# + +# +# Subnet-specific options, matched on whether the allocated IP address is +# within the indicated range. +# +subnet DHCP-Network-Subnet < 10.30.10.0/24 + DHCP-Subnet-Mask := 255.255.255.0, + DHCP-Router-Address := 10.30.10.1, + DHCP-Broadcast-Address := 10.30.10.255 + +subnet DHCP-Network-Subnet < 10.30.20.0/24 + DHCP-Subnet-Mask := 255.255.255.0, + DHCP-Router-Address := 10.30.20.1, + DHCP-Broadcast-Address := 10.30.20.255 + + +############################### +# Group-specific parameters # +############################### + +# +# Note: This section is matched by calling the dhcp_group_options policy. +# +# It should be called *after* defining the device's group memberships in +# DHCP-Group-Name request attributes. In the default dhcp virtual server this +# is demonstrated with the help of the dhcp_group_membership instance of the +# passwd module. +# + +# +# Group-specific options, keyed by DHCP-Group-Name +# +group1 + DHCP-Server-Host-Name := "terminal-booter.example.org", + DHCP-Boot-Filename := "bootfile.pxe" + + +############################## +# Host-specific parameters # +############################## + +# +# Note: This section is matched by calling the dhcp_hosts instance of the +# files module. +# + +# +# Host-specific options, keyed by DHCP-Client-Hardware-Address +# +host-00:10:20:30:40:50 + DHCP-Boot-Filename := "customboot.pxe" + +host-10:90:80:70:aa:bb + DHCP-X-Window-Font-Server := 10.20.1.10, + DHCP-Impress-Server := 10.20.1.20 diff --git a/raddb/mods-config/files/pre-proxy b/raddb/mods-config/files/pre-proxy new file mode 100644 index 0000000..7292e23 --- /dev/null +++ b/raddb/mods-config/files/pre-proxy @@ -0,0 +1,31 @@ +# +# Configuration file for the rlm_files module. +# Please see rlm_files(5) manpage for more information. +# +# $Id$ +# +# This file is similar to the "users" file. The check items +# are compared against the request, but the "reply" items are +# used to update the proxied packet, not the reply to the NAS. +# +# You can use this file to re-write requests which are about to +# be sent to a home server. +# + +# +# Requests destinated to realm "extisp" are sent to a RADIUS +# home server hosted by an other company which doesn't know about +# the IP addresses of our NASes. Therefore we replace the value of +# the NAS-IP-Address attribute by a unique value we communicated +# to them. +# +#DEFAULT Realm == "extisp" +# NAS-IP-Address := 10.1.2.3 + +# +# For all proxied packets, set the User-Name in the proxied packet +# to the Stripped-User-Name, if it exists. If not, set it to the +# User-Name from the original request. +# +#DEFAULT +# User-Name := `%{%{Stripped-User-Name}:-%{User-Name}}` diff --git a/raddb/mods-config/perl/example.pl b/raddb/mods-config/perl/example.pl new file mode 100644 index 0000000..f00b17b --- /dev/null +++ b/raddb/mods-config/perl/example.pl @@ -0,0 +1,230 @@ + +# +# 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 2 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, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA +# +# Copyright 2002 The FreeRADIUS server project +# Copyright 2002 Boian Jordanov <bjordanov@orbitel.bg> +# + +# +# Example code for use with rlm_perl +# +# You can use every module that comes with your perl distribution! +# +# If you are using DBI and do some queries to DB, please be sure to +# use the CLONE function to initialize the DBI connection to DB. +# + +use strict; +use warnings; + +# use ... +use Data::Dumper; + +# Bring the global hashes into the package scope +our (%RAD_REQUEST, %RAD_REPLY, %RAD_CHECK, %RAD_STATE, %RAD_PERLCONF); + +# This is hash wich hold original request from radius +#my %RAD_REQUEST; +# In this hash you add values that will be returned to NAS. +#my %RAD_REPLY; +#This is for check items +#my %RAD_CHECK; +# This is the session-sate +#my %RAD_STATE; +# This is configuration items from "config" perl module configuration section +#my %RAD_PERLCONF; + +# Multi-value attributes are mapped to perl arrayrefs. +# +# update request { +# Filter-Id := 'foo' +# Filter-Id += 'bar' +# } +# +# This results to the following entry in %RAD_REQUEST: +# +# $RAD_REQUEST{'Filter-Id'} = [ 'foo', 'bar' ]; +# +# Likewise, you can assign an arrayref to return multi-value attributes + +# +# This the remapping of return values +# +use constant { + RLM_MODULE_REJECT => 0, # immediately reject the request + RLM_MODULE_OK => 2, # the module is OK, continue + RLM_MODULE_HANDLED => 3, # the module handled the request, so stop + RLM_MODULE_INVALID => 4, # the module considers the request invalid + RLM_MODULE_USERLOCK => 5, # reject the request (user is locked out) + RLM_MODULE_NOTFOUND => 6, # user not found + RLM_MODULE_NOOP => 7, # module succeeded without doing anything + RLM_MODULE_UPDATED => 8, # OK (pairs modified) + RLM_MODULE_NUMCODES => 9 # How many return codes there are +}; + +# Same as src/include/log.h +use constant { + L_AUTH => 2, # Authentication message + L_INFO => 3, # Informational message + L_ERR => 4, # Error message + L_WARN => 5, # Warning + L_PROXY => 6, # Proxy messages + L_ACCT => 7, # Accounting messages + L_DBG => 16, # Only displayed when debugging is enabled + L_DBG_WARN => 17, # Warning only displayed when debugging is enabled + L_DBG_ERR => 18, # Error only displayed when debugging is enabled + L_DBG_WARN_REQ => 19, # Less severe warning only displayed when debugging is enabled + L_DBG_ERR_REQ => 20, # Less severe error only displayed when debugging is enabled +}; + +# Global variables can persist across different calls to the module. +# +# +# { +# my %static_global_hash = (); +# +# sub post_auth { +# ... +# } +# ... +# } + + +# Function to handle authorize +sub authorize { + # For debugging purposes only +# &log_request_attributes; + + # Here's where your authorization code comes + # You can call another function from here: + &test_call; + + return RLM_MODULE_OK; +} + +# Function to handle authenticate +sub authenticate { + # For debugging purposes only +# &log_request_attributes; + + if ($RAD_REQUEST{'User-Name'} =~ /^baduser/i) { + # Reject user and tell him why + $RAD_REPLY{'Reply-Message'} = "Denied access by rlm_perl function"; + return RLM_MODULE_REJECT; + } else { + # Accept user and set some attribute + if (&radiusd::xlat("%{client:group}") eq 'UltraAllInclusive') { + # User called from NAS with unlim plan set, set higher limits + $RAD_REPLY{'h323-credit-amount'} = "1000000"; + } else { + $RAD_REPLY{'h323-credit-amount'} = "100"; + } + return RLM_MODULE_OK; + } +} + +# Function to handle preacct +sub preacct { + # For debugging purposes only +# &log_request_attributes; + + return RLM_MODULE_OK; +} + +# Function to handle accounting +sub accounting { + # For debugging purposes only +# &log_request_attributes; + + # You can call another subroutine from here + &test_call; + + return RLM_MODULE_OK; +} + +# Function to handle checksimul +sub checksimul { + # For debugging purposes only +# &log_request_attributes; + + return RLM_MODULE_OK; +} + +# Function to handle pre_proxy +sub pre_proxy { + # For debugging purposes only +# &log_request_attributes; + + return RLM_MODULE_OK; +} + +# Function to handle post_proxy +sub post_proxy { + # For debugging purposes only +# &log_request_attributes; + + return RLM_MODULE_OK; +} + +# Function to handle post_auth +sub post_auth { + # For debugging purposes only +# &log_request_attributes; + + return RLM_MODULE_OK; +} + +# Function to handle xlat +sub xlat { + # For debugging purposes only +# &log_request_attributes; + + # Loads some external perl and evaluate it + my ($filename,$a,$b,$c,$d) = @_; + &radiusd::radlog(L_DBG, "From xlat $filename "); + &radiusd::radlog(L_DBG,"From xlat $a $b $c $d "); + local *FH; + open FH, $filename or die "open '$filename' $!"; + local($/) = undef; + my $sub = <FH>; + close FH; + my $eval = qq{ sub handler{ $sub;} }; + eval $eval; + eval {main->handler;}; +} + +# Function to handle detach +sub detach { + # For debugging purposes only +# &log_request_attributes; +} + +# +# Some functions that can be called from other functions +# + +sub test_call { + # Some code goes here +} + +sub log_request_attributes { + # This shouldn't be done in production environments! + # This is only meant for debugging! + for (keys %RAD_REQUEST) { + &radiusd::radlog(L_DBG, "RAD_REQUEST: $_ = $RAD_REQUEST{$_}"); + } +} + diff --git a/raddb/mods-config/preprocess/hints b/raddb/mods-config/preprocess/hints new file mode 100644 index 0000000..84d4d78 --- /dev/null +++ b/raddb/mods-config/preprocess/hints @@ -0,0 +1,86 @@ +# +# hints +# +# The hints file. This file is used to match +# a request, and then add attributes to it. This +# process allows a user to login as "bob.ppp" (for example), +# and receive a PPP connection, even if the NAS doesn't +# ask for PPP. The "hints" file is used to match the +# ".ppp" portion of the username, and to add a set of +# "user requested PPP" attributes to the request. +# +# Matching can take place with the the Prefix and Suffix +# attributes, just like in the "users" file. +# These attributes operate ONLY on the username, though. +# +# Note that the attributes that are set for each entry are +# NOT added to the reply attributes passed back to the NAS. +# Instead they are added to the list of attributes in the +# request that has been SENT by the NAS. +# +# This extra information can be used in the users file to +# match on. Usually this is done in the DEFAULT entries, +# of which there can be more than one. +# +# In addition a matching entry can transform a username +# for authentication purposes if the "Strip-User-Name" +# variable is set to Yes in an entry (default is Yes). +# +# A special non-protocol name-value pair called "Hint" +# can be set to match on in the "users" file. +# +# As with the "users" file, the first entry that matches the +# incoming request will cause the server to stop looking for +# more hints. If the "Fall-Through" attribute is set to +# "Yes" in an entry then the server will not stop, but +# continue to process further hints from the file. Matches +# on subsequent hints will be against the altered request +# from the previous hints, not against the original request. +# +# The following is how most dial-up ISPs want to set this up. +# +# Version: $Id$ +# + + +DEFAULT Suffix == ".ppp", Strip-User-Name = Yes + Hint = "PPP", + Service-Type = Framed-User, + Framed-Protocol = PPP + +DEFAULT Suffix == ".slip", Strip-User-Name = Yes + Hint = "SLIP", + Service-Type = Framed-User, + Framed-Protocol = SLIP + +DEFAULT Suffix == ".cslip", Strip-User-Name = Yes + Hint = "CSLIP", + Service-Type = Framed-User, + Framed-Protocol = SLIP, + Framed-Compression = Van-Jacobson-TCP-IP + +###################################################################### +# +# These entries are old, and commented out by default. +# They confuse too many people when "Peter" logs in, and the +# server thinks that the user "eter" is asking for PPP. +# +#DEFAULT Prefix == "U", Strip-User-Name = No +# Hint = "UUCP" + +#DEFAULT Prefix == "P", Strip-User-Name = Yes +# Hint = "PPP", +# Service-Type = Framed-User, +# Framed-Protocol = PPP + +#DEFAULT Prefix == "S", Strip-User-Name = Yes +# Hint = "SLIP", +# Service-Type = Framed-User, +# Framed-Protocol = SLIP + +#DEFAULT Prefix == "C", Strip-User-Name = Yes +# Hint = "CSLIP", +# Service-Type = Framed-User, +# Framed-Protocol = SLIP, +# Framed-Compression = Van-Jacobson-TCP-IP + diff --git a/raddb/mods-config/preprocess/huntgroups b/raddb/mods-config/preprocess/huntgroups new file mode 100644 index 0000000..da28dba --- /dev/null +++ b/raddb/mods-config/preprocess/huntgroups @@ -0,0 +1,43 @@ +# +# huntgroups This file defines the `huntgroups' that you have. A +# huntgroup is defined by specifying the IP address of +# the NAS and possibly a port. +# +# Matching is done while RADIUS scans the user file; if it +# includes the selection criteria "Huntgroup-Name == XXX" +# the huntgroup is looked up in this file to see if it +# matches. There can be multiple definitions of the same +# huntgroup; the first one that matches will be used. +# +# This file can also be used to define restricted access +# to certain huntgroups. The second and following lines +# define the access restrictions (based on username and +# UNIX usergroup) for the huntgroup. +# + +# +# Our POP in Alphen a/d Rijn has 3 terminal servers. Create a Huntgroup-Name +# called Alphen that matches on all three terminal servers. +# +#alphen NAS-IP-Address == 192.0.2.5 +#alphen NAS-IP-Address == 192.0.2.6 +#alphen NAS-IP-Address == 192.0.2.7 + +# +# The POP in Delft consists of only one terminal server. +# +#delft NAS-IP-Address == 198.51.100.5 + +# +# Port 0 on the first terminal server in Alphen are connected to +# a huntgroup that is for business users only. Note that only one +# of the username or groupname has to match to get access (OR/OR). +# +# Note that this huntgroup is a subset of the "alphen" huntgroup. +# +#business NAS-IP-Address == 198.51.100.5, NAS-Port-Id == 0 +# User-Name == rogerl, +# User-Name == henks, +# Group == business, +# Group == staff + diff --git a/raddb/mods-config/realm/freeradius-naptr-to-home-server.sh b/raddb/mods-config/realm/freeradius-naptr-to-home-server.sh new file mode 100755 index 0000000..66388d3 --- /dev/null +++ b/raddb/mods-config/realm/freeradius-naptr-to-home-server.sh @@ -0,0 +1,161 @@ +#!/bin/sh + +# This script looks up radsec srv records in DNS for the one realm +# given as argument, and creates a home_server template based on the +# information found. +# +# It currently ignores weight markers, but does sort servers on +# priority marker, lowest number first. For host command this is +# column 5, for dig it is column 1. +# +# It then tells FreeRADIUS (via radmin) that +# there is a new home server. +# +# Note that in order for it to work, you need to have the +# "control-socket" enabled. + +usage() { + echo "Usage: ${0} [OPTIONS] <realm> <optional NAPTR tag>" + echo " -d RADIUS_DIR Set radius directory" + echo " -t test (skip running radmin)" + exit 1 +} + +test -n "${1}" || usage + +RADDB=/etc/raddb +RADMIN=y + +# +# Parse command-line options +# +while [ `echo "$1" | cut -c 1` = "-" ] +do + case "$1" in + -d) + RADDB=$2 + shift;shift + ;; + -t) + RADMIN= + shift + ;; + + *) + usage + ;; + esac +done + +test -n "${2}" && NAPTRTAG="${2}" || NAPTRTAG="x-eduroam:radius.tls" + +DIGCMD=$(command -v dig) +HOSTCMD=$(command -v host) +PRINTCMD=$(command -v printf) + +# +# These validations prevent rogue DNS records from pwning your RADIUS installation. +# +# See https://github.com/radsecproxy/radsecproxy/security/advisories/GHSA-56gw-9rj9-55rc +# and https://www.usenix.org/conference/usenixsecurity21/presentation/jeitner +# +# The contents of these validation routines should NOT be changed without a deep understanding +# of DNS! +# +validate_host() { + echo ${@} | tr -d '\n\t\r' | grep -E '^[_0-9a-zA-Z][-._0-9a-zA-Z]*$' +} + +validate_port() { + echo ${@} | tr -d '\n\t\r' | grep -E '^[0-9]+$' +} + +dig_it_srv() { + ${DIGCMD} +short srv $SRV_HOST | sort -n -k1 | + while read line; do + set $line + PORT=$(validate_port $3) + HOST=$(validate_host $4) + if [ -n "${HOST}" ] && [ -n "${PORT}" ]; then + $PRINTCMD "\tipaddr = ${HOST%.}\n\tport = ${PORT}\n" + fi + done +} + +dig_it_naptr() { + ${DIGCMD} +short naptr "${REALM}" | grep $NAPTRTAG | sort -n -k1 | + while read line; do + set $line + TYPE=$3 + HOST=$(validate_host $6) + if ( [ "$TYPE" = "\"s\"" ] || [ "$TYPE" = "\"S\"" ] ) && [ -n "${HOST}" ]; then + SRV_HOST=${HOST%.} + dig_it_srv + fi + done +} + +host_it_srv() { + ${HOSTCMD} -t srv $SRV_HOST | sort -n -k5 | + while read line; do + set $line + PORT=$(validate_port $7) + HOST=$(validate_host $8) + if [ -n "${HOST}" ] && [ -n "${PORT}" ]; then + $PRINTCMD "\tipaddr ${HOST%.}:${PORT}\n" + fi + done +} + +host_it_naptr() { + ${HOSTCMD} -t naptr "${REALM}" | grep $NAPTRTAG | sort -n -k5 | + while read line; do + set $line + TYPE=$7 + HOST=$(validate_host ${10}) + if ( [ "$TYPE" = "\"s\"" ] || [ "$TYPE" = "\"S\"" ] ) && [ -n "${HOST}" ]; then + SRV_HOST=${HOST%.} + host_it_srv + fi + done +} + +REALM=$(validate_host ${1}) +if [ -z "${REALM}" ]; then + echo "realm \"${1}\" failed validation" >&2 + usage +fi + +if [ -x "${DIGCMD}" ]; then + SERVERS=$(dig_it_naptr) + +elif [ -x "${HOSTCMD}" ]; then + SERVERS=$(host_it_naptr) + +else + echo "${0} requires either \"dig\" or \"host\" command." >&2 + exit 1 +fi + +if [ ! -n "${SERVERS}" ]; then + echo "No servers found" >&2 + exit 1 +fi + +# +# Just testing - don't do anything else. +# +if [ -z "${RADMIN}" ]; then + $PRINTCMD "home_server ${REALM} {\n${SERVERS}\n\t\$INCLUDE tls.conf\n}\n" + exit 0 +fi + +# +# Print out the template, and include the site-local tls.conf file. +# +$PRINTCMD "home_server ${REALM} {\n${SERVERS}\n\t\$INCLUDE tls.conf\n}\n" > $RADDB/home_servers/$1 + +# +# @todo - use ${prefix} or some such thing to find radmin. +# +/usr/sbin/radmin -e "add home_server file $RADDB/home_servers/$1" diff --git a/raddb/mods-config/sql/counter/mysql/dailycounter.conf b/raddb/mods-config/sql/counter/mysql/dailycounter.conf new file mode 100644 index 0000000..67c7f35 --- /dev/null +++ b/raddb/mods-config/sql/counter/mysql/dailycounter.conf @@ -0,0 +1,46 @@ +# +# This query properly handles calls that span from the +# previous reset period into the current period but +# involves more work for the SQL server than those +# below +# +query = "\ + SELECT SUM(acctsessiontime - GREATEST((%%b - UNIX_TIMESTAMP(acctstarttime)), 0)) \ + FROM radacct \ + WHERE username = '%{${key}}' \ + AND UNIX_TIMESTAMP(acctstarttime) + acctsessiontime > '%%b'" + +# +# This query ignores calls that started in a previous +# reset period and continue into into this one. But it +# is a little easier on the SQL server +# +#query = "\ +# SELECT SUM(acctsessiontime) \ +# FROM radacct \ +# WHERE username = '%{${key}}' \ +# AND acctstarttime > FROM_UNIXTIME('%%b')" + +# +# This query is the same as above, but demonstrates an +# additional counter parameter '%%e' which is the +# timestamp for the end of the period +# +#query = "\ +# SELECT SUM(acctsessiontime) \ +# FROM radacct \ +# WHERE username = '%{${key}}' \ +# AND acctstarttime BETWEEN FROM_UNIXTIME('%%b') AND FROM_UNIXTIME('%%e')" + +# +# This query allows retrieving the entries based on a +# period that resets on a particular day of the month. +# +#reset_day = 21 +#query = "\ +# SELECT SUM(acctsessiontime) FROM radacct WHERE username = '%{${key}}' AND \ +# IF (DAY(CURDATE()) >= ${reset_day}, \ +# acctstarttime > DATE(DATE_FORMAT(NOW(), '%Y-%m-${reset_day}')), \ +# acctstarttime > DATE(DATE_FORMAT(NOW() - INTERVAL 1 MONTH, '%Y-%m-${reset_day}')) \ +# )" +# diff --git a/raddb/mods-config/sql/counter/mysql/expire_on_login.conf b/raddb/mods-config/sql/counter/mysql/expire_on_login.conf new file mode 100644 index 0000000..73e2ca3 --- /dev/null +++ b/raddb/mods-config/sql/counter/mysql/expire_on_login.conf @@ -0,0 +1,6 @@ +query = "\ + SELECT IFNULL( MAX(TIME_TO_SEC(TIMEDIFF(NOW(), acctstarttime))),0) \ + FROM radacct \ + WHERE UserName='%{${key}}' \ + ORDER BY acctstarttime \ + LIMIT 1;" diff --git a/raddb/mods-config/sql/counter/mysql/monthlycounter.conf b/raddb/mods-config/sql/counter/mysql/monthlycounter.conf new file mode 100644 index 0000000..8999765 --- /dev/null +++ b/raddb/mods-config/sql/counter/mysql/monthlycounter.conf @@ -0,0 +1,34 @@ +# +# This query properly handles calls that span from the +# previous reset period into the current period but +# involves more work for the SQL server than those +# below +# +query = "\ + SELECT SUM(acctsessiontime - GREATEST((%%b - UNIX_TIMESTAMP(acctstarttime)), 0)) \ + FROM radacct \ + WHERE username='%{${key}}' \ + AND UNIX_TIMESTAMP(acctstarttime) + acctsessiontime > '%%b'" + +# +# This query ignores calls that started in a previous +# reset period and continue into into this one. But it +# is a little easier on the SQL server +# +#query = "\ +# SELECT SUM(acctsessiontime) \ +# FROM radacct\ +# WHERE username='%{${key}}' \ +# AND acctstarttime > FROM_UNIXTIME('%%b')" + +# +# This query is the same as above, but demonstrates an +# additional counter parameter '%%e' which is the +# timestamp for the end of the period +# +#query = "\ +# SELECT SUM(acctsessiontime) \ +# FROM radacct \ +# WHERE username='%{${key}}' \ +# AND acctstarttime BETWEEN FROM_UNIXTIME('%%b') \ +# AND FROM_UNIXTIME('%%e')" diff --git a/raddb/mods-config/sql/counter/mysql/noresetcounter.conf b/raddb/mods-config/sql/counter/mysql/noresetcounter.conf new file mode 100644 index 0000000..abcb21b --- /dev/null +++ b/raddb/mods-config/sql/counter/mysql/noresetcounter.conf @@ -0,0 +1,4 @@ +query = "\ + SELECT IFNULL(SUM(AcctSessionTime),0) \ + FROM radacct \ + WHERE UserName='%{${key}}'" diff --git a/raddb/mods-config/sql/counter/mysql/weeklycounter.conf b/raddb/mods-config/sql/counter/mysql/weeklycounter.conf new file mode 100644 index 0000000..bf8a4c4 --- /dev/null +++ b/raddb/mods-config/sql/counter/mysql/weeklycounter.conf @@ -0,0 +1,11 @@ +# +# This query properly handles calls that span from the +# previous reset period into the current period but +# involves more work for the SQL server than those +# below +# +query = "\ + SELECT SUM(acctsessiontime - GREATEST((%%b - UNIX_TIMESTAMP(acctstarttime)), 0)) \ + FROM radacct \ + WHERE username = '%{${key}}' \ + AND UNIX_TIMESTAMP(acctstarttime) + acctsessiontime > '%%b'" diff --git a/raddb/mods-config/sql/counter/postgresql/dailycounter.conf b/raddb/mods-config/sql/counter/postgresql/dailycounter.conf new file mode 100644 index 0000000..1e2f7fa --- /dev/null +++ b/raddb/mods-config/sql/counter/postgresql/dailycounter.conf @@ -0,0 +1,34 @@ +# +# This query properly handles calls that span from the +# previous reset period into the current period but +# involves more work for the SQL server than those +# below +# +query = "\ + SELECT SUM(AcctSessionTime - GREATEST((%%b - EXTRACT(epoch FROM AcctStartTime)), 0)) \ + FROM radacct \ + WHERE UserName='%{${key}}' \ + AND EXTRACT(epoch FROM AcctStartTime) + AcctSessionTime > '%%b'" + +# +# This query ignores calls that started in a previous +# reset period and continue into into this one. But it +# is a little easier on the SQL server +# +#query = "\ +# SELECT SUM(AcctSessionTime) \ +# FROM radacct \ +# WHERE UserName='%{${key}}' \ +# AND EXTRACT(epoch FROM AcctStartTime) > '%%b'" + +# +# This query is the same as above, but demonstrates an +# additional counter parameter '%%e' which is the +# timestamp for the end of the period +# +#query = "\ +# SELECT SUM(AcctSessionTime) \ +# FROM radacct \ +# WHERE UserName='%{${key}}' \ +# AND EXTRACT(epoch FROM AcctStartTime) BETWEEN '%%b' \ +# AND '%%e'" diff --git a/raddb/mods-config/sql/counter/postgresql/expire_on_login.conf b/raddb/mods-config/sql/counter/postgresql/expire_on_login.conf new file mode 100644 index 0000000..6ec4c4e --- /dev/null +++ b/raddb/mods-config/sql/counter/postgresql/expire_on_login.conf @@ -0,0 +1,6 @@ +query = "\ + SELECT EXTRACT(EPOCH FROM (NOW() - acctstarttime)) \ + FROM radacct \ + WHERE UserName='%{${key}}' \ + ORDER BY acctstarttime \ + LIMIT 1;" diff --git a/raddb/mods-config/sql/counter/postgresql/monthlycounter.conf b/raddb/mods-config/sql/counter/postgresql/monthlycounter.conf new file mode 100644 index 0000000..cdaf83a --- /dev/null +++ b/raddb/mods-config/sql/counter/postgresql/monthlycounter.conf @@ -0,0 +1,31 @@ +# This query properly handles calls that span from the +# previous reset period into the current period but +# involves more work for the SQL server than those +# below +query = "\ + SELECT SUM(AcctSessionTime - GREATEST((%%b - EXTRACT(epoch FROM AcctStartTime)), 0)) \ + FROM radacct \ + WHERE UserName='%{${key}}' \ + AND EXTRACT(epoch FROM AcctStartTime) + AcctSessionTime > '%%b'" + +# +# This query ignores calls that started in a previous +# reset period and continue into into this one. But it +# is a little easier on the SQL server +# +#query = "\ +# SELECT SUM(AcctSessionTime) \ +# FROM radacct \ +# WHERE UserName='%{${key}}' \ +# AND EXTRACT(epoch FROM AcctStartTime) > '%%b'" + +# +# This query is the same as above, but demonstrates an +# additional counter parameter '%%e' which is the +# timestamp for the end of the period +# +#query = "\ +# SELECT SUM(AcctSessionTime) \ +# FROM radacct \ +# WHERE UserName='%{${key}}' \ +# AND EXTRACT(epoch FROM AcctStartTime) BETWEEN '%%b' AND '%%e'" diff --git a/raddb/mods-config/sql/counter/postgresql/noresetcounter.conf b/raddb/mods-config/sql/counter/postgresql/noresetcounter.conf new file mode 100644 index 0000000..ac5182e --- /dev/null +++ b/raddb/mods-config/sql/counter/postgresql/noresetcounter.conf @@ -0,0 +1,4 @@ +query = "\ + SELECT SUM(AcctSessionTime) \ + FROM radacct \ + WHERE UserName='%{${key}}'" diff --git a/raddb/mods-config/sql/counter/postgresql/weeklycounter.conf b/raddb/mods-config/sql/counter/postgresql/weeklycounter.conf new file mode 100644 index 0000000..0d809c1 --- /dev/null +++ b/raddb/mods-config/sql/counter/postgresql/weeklycounter.conf @@ -0,0 +1,12 @@ +# +# This query properly handles calls that span from the +# previous reset period into the current period but +# involves more work for the SQL server than those +# below +# +query = "\ + SELECT SUM(AcctSessionTime - GREATEST((%%b - EXTRACT(epoch FROM AcctStartTime)), 0)) \ + FROM radacct \ + WHERE UserName='%{${key}}' \ + AND EXTRACT(epoch FROM AcctStartTime) + AcctSessionTime > '%%b'" + diff --git a/raddb/mods-config/sql/counter/sqlite/dailycounter.conf b/raddb/mods-config/sql/counter/sqlite/dailycounter.conf new file mode 100644 index 0000000..9a2ec38 --- /dev/null +++ b/raddb/mods-config/sql/counter/sqlite/dailycounter.conf @@ -0,0 +1,33 @@ +# +# This query properly handles calls that span from the +# previous reset period into the current period but +# involves more work for the SQL server than those +# below +# +query = "\ + SELECT SUM(acctsessiontime - GREATEST((%%b - strftime('%%s', acctstarttime)), 0)) \ + FROM radacct \ + WHERE username = '%{${key}}' \ + AND (strftime('%%s', acctstarttime) + acctsessiontime) > %%b" + +# +# This query ignores calls that started in a previous +# reset period and continue into into this one. But it +# is a little easier on the SQL server +# +#query = "\ +# SELECT SUM(acctsessiontime) \ +# FROM radacct \ +# WHERE \username = '%{${key}}' \ +# AND acctstarttime > %%b" + +# +# This query is the same as above, but demonstrates an +# additional counter parameter '%%e' which is the +# timestamp for the end of the period +# +#query = "\ +# SELECT SUM(acctsessiontime) FROM radacct \ +# WHERE username = '%{${key}}' \ +# AND acctstarttime BETWEEN %%b \ +# AND %%e" diff --git a/raddb/mods-config/sql/counter/sqlite/expire_on_login.conf b/raddb/mods-config/sql/counter/sqlite/expire_on_login.conf new file mode 100644 index 0000000..f4e95a5 --- /dev/null +++ b/raddb/mods-config/sql/counter/sqlite/expire_on_login.conf @@ -0,0 +1,6 @@ +query = "\ + SELECT GREATEST(strftime('%%s', NOW()) - strftime('%%s', acctstarttime), 0) AS expires \ + FROM radacct \ + WHERE username = '%{${key}}' \ + ORDER BY acctstarttime \ + LIMIT 1;" diff --git a/raddb/mods-config/sql/counter/sqlite/monthlycounter.conf b/raddb/mods-config/sql/counter/sqlite/monthlycounter.conf new file mode 100644 index 0000000..5262097 --- /dev/null +++ b/raddb/mods-config/sql/counter/sqlite/monthlycounter.conf @@ -0,0 +1,34 @@ +# +# This query properly handles calls that span from the +# previous reset period into the current period but +# involves more work for the SQL server than those +# below +# +query = "\ + SELECT SUM(acctsessiontime - GREATEST((%%b - strftime('%%s', acctstarttime)), 0)) \ + FROM radacct \ + WHERE username = '%{${key}}' AND \ + (strftime('%%s', acctstarttime) + acctsessiontime) > %%b" + +# +# This query ignores calls that started in a previous +# reset period and continue into into this one. But it +# is a little easier on the SQL server +# +#query = "\ +# SELECT SUM(acctsessiontime) \ +# FROM radacct \ +# WHERE username = '%{${key}}' \ +# AND acctstarttime > %%b" + +# +# This query is the same as above, but demonstrates an +# additional counter parameter '%%e' which is the +# timestamp for the end of the period +# +#query = "\ +# SELECT SUM(acctsessiontime) \ +# FROM radacct \ +# WHERE username = '%{${key}}' \ +# AND acctstarttime BETWEEN %%b \ +# AND %%e" diff --git a/raddb/mods-config/sql/counter/sqlite/noresetcounter.conf b/raddb/mods-config/sql/counter/sqlite/noresetcounter.conf new file mode 100644 index 0000000..ac2d869 --- /dev/null +++ b/raddb/mods-config/sql/counter/sqlite/noresetcounter.conf @@ -0,0 +1,4 @@ +query = "\ + SELECT IFNULL(SUM(acctsessiontime),0) \ + FROM radacct \ + WHERE username = '%{${key}}'" diff --git a/raddb/mods-config/sql/counter/sqlite/weeklycounter.conf b/raddb/mods-config/sql/counter/sqlite/weeklycounter.conf new file mode 100644 index 0000000..06ce3b6 --- /dev/null +++ b/raddb/mods-config/sql/counter/sqlite/weeklycounter.conf @@ -0,0 +1,12 @@ +# +# This query properly handles calls that span from the +# previous reset period into the current period but +# involves more work for the SQL server than those +# below +# +query = "\ + SELECT SUM(acctsessiontime - GREATEST((%%b - strftime('%%s', acctstarttime)), 0)) \ + FROM radacct \ + WHERE username = '%{${key}}' \ + AND (strftime('%%s', acctstarttime) + acctsessiontime) > %%b" + diff --git a/raddb/mods-config/sql/cui/mysql/queries.conf b/raddb/mods-config/sql/cui/mysql/queries.conf new file mode 100644 index 0000000..f8f18ca --- /dev/null +++ b/raddb/mods-config/sql/cui/mysql/queries.conf @@ -0,0 +1,50 @@ +# -*- text -*- +# +# cui/mysql/queries.conf -- Queries to update a MySQL CUI table. +# +# $Id$ + +post-auth { + query = "\ + INSERT IGNORE INTO ${..cui_table} \ + (clientipaddress, callingstationid, username, cui, lastaccounting) \ + VALUES \ + ('%{%{Packet-Src-IPv6-Address}:-%{Packet-Src-IP-Address}}', '%{Calling-Station-Id}', \ + '%{User-Name}', '%{reply:Chargeable-User-Identity}', NULL) \ + ON DUPLICATE KEY UPDATE \ + lastaccounting='0000-00-00 00:00:00', \ + cui='%{reply:Chargeable-User-Identity}'" + +} + +accounting { + reference = "%{tolower:type.%{Acct-Status-Type}.query}" + type { + start { + query = "\ + UPDATE ${....cui_table} SET \ + lastaccounting = CURRENT_TIMESTAMP \ + WHERE clientipaddress = '%{%{Packet-Src-IPv6-Address}:-%{Packet-Src-IP-Address}}' \ + AND callingstationid = '%{Calling-Station-Id}' \ + AND username = '%{User-Name}' \ + AND cui = '%{Chargeable-User-Identity}'" + } + interim-update { + query ="\ + UPDATE ${....cui_table} SET \ + lastaccounting = CURRENT_TIMESTAMP \ + WHERE clientipaddress = '%{%{Packet-Src-IPv6-Address}:-%{Packet-Src-IP-Address}}' \ + AND callingstationid = '%{Calling-Station-Id}' \ + AND username = '%{User-Name}' \ + AND cui = '%{Chargeable-User-Identity}'" + } + stop { + query ="\ + DELETE FROM ${....cui_table} \ + WHERE clientipaddress = '%{%{Packet-Src-IPv6-Address}:-%{Packet-Src-IP-Address}}' \ + AND callingstationid = '%{Calling-Station-Id}' \ + AND username = '%{User-Name}' \ + AND cui = '%{Chargeable-User-Identity}'" + } + } +} diff --git a/raddb/mods-config/sql/cui/mysql/schema.sql b/raddb/mods-config/sql/cui/mysql/schema.sql new file mode 100644 index 0000000..da9b2f7 --- /dev/null +++ b/raddb/mods-config/sql/cui/mysql/schema.sql @@ -0,0 +1,9 @@ +CREATE TABLE `cui` ( + `clientipaddress` varchar(46) NOT NULL default '', + `callingstationid` varchar(50) NOT NULL default '', + `username` varchar(64) NOT NULL default '', + `cui` varchar(32) NOT NULL default '', + `creationdate` timestamp NOT NULL default CURRENT_TIMESTAMP, + `lastaccounting` timestamp NOT NULL default '0000-00-00 00:00:00', + PRIMARY KEY (`username`,`clientipaddress`,`callingstationid`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/raddb/mods-config/sql/cui/postgresql/queries.conf b/raddb/mods-config/sql/cui/postgresql/queries.conf new file mode 100644 index 0000000..6c2215f --- /dev/null +++ b/raddb/mods-config/sql/cui/postgresql/queries.conf @@ -0,0 +1,47 @@ +# -*- text -*- +# +# cui/postgresql/queries.conf -- Queries to update a PostgreSQL CUI table. +# +# $Id$ + +post-auth { + query = "\ + INSERT INTO ${..cui_table} \ + (clientipaddress, callingstationid, username, cui) \ + VALUES \ + ('%{%{Packet-Src-IPv6-Address}:-%{Packet-Src-IP-Address}}', '%{Calling-Station-Id}', \ + '%{User-Name}', '%{reply:Chargeable-User-Identity}')" + +} + +accounting { + reference = "%{tolower:type.%{Acct-Status-Type}.query}" + type { + start { + query = "\ + UPDATE ${....cui_table} SET \ + lastaccounting = now() \ + WHERE clientipaddress = '%{%{Packet-Src-IPv6-Address}:-%{Packet-Src-IP-Address}}' \ + AND callingstationid = '%{Calling-Station-Id}' \ + AND username = '%{User-Name}' \ + AND cui = '%{Chargeable-User-Identity}'" + } + interim-update { + query ="\ + UPDATE ${....cui_table} SET \ + lastaccounting = now() \ + WHERE clientipaddress = '%{%{Packet-Src-IPv6-Address}:-%{Packet-Src-IP-Address}}' \ + AND callingstationid = '%{Calling-Station-Id}' \ + AND username = '%{User-Name}' \ + AND cui = '%{Chargeable-User-Identity}'" + } + stop { + query ="\ + DELETE FROM ${....cui_table} \ + WHERE clientipaddress = '%{%{Packet-Src-IPv6-Address}:-%{Packet-Src-IP-Address}}' \ + AND callingstationid = '%{Calling-Station-Id}' \ + AND username = '%{User-Name}' \ + AND cui = '%{Chargeable-User-Identity}'" + } + } +} diff --git a/raddb/mods-config/sql/cui/postgresql/schema.sql b/raddb/mods-config/sql/cui/postgresql/schema.sql new file mode 100644 index 0000000..3b24401 --- /dev/null +++ b/raddb/mods-config/sql/cui/postgresql/schema.sql @@ -0,0 +1,14 @@ +CREATE TABLE cui ( + clientipaddress INET NOT NULL DEFAULT '0.0.0.0', + callingstationid varchar(50) NOT NULL DEFAULT '', + username varchar(64) NOT NULL DEFAULT '', + cui varchar(32) NOT NULL DEFAULT '', + creationdate TIMESTAMP with time zone NOT NULL default 'now()', + lastaccounting TIMESTAMP with time zone NOT NULL default '-infinity'::timestamp, + PRIMARY KEY (username, clientipaddress, callingstationid) +); + +CREATE RULE postauth_query AS ON INSERT TO cui + WHERE EXISTS(SELECT 1 FROM cui WHERE (username, clientipaddress, callingstationid)=(NEW.username, NEW.clientipaddress, NEW.callingstationid)) + DO INSTEAD UPDATE cui SET lastaccounting ='-infinity'::timestamp with time zone, cui=NEW.cui WHERE (username, clientipaddress, callingstationid)=(NEW.username, NEW.clientipaddress, NEW.callingstationid); + diff --git a/raddb/mods-config/sql/cui/sqlite/queries.conf b/raddb/mods-config/sql/cui/sqlite/queries.conf new file mode 100644 index 0000000..41741eb --- /dev/null +++ b/raddb/mods-config/sql/cui/sqlite/queries.conf @@ -0,0 +1,47 @@ +# -*- text -*- +# +# cui/sqlite/queries.conf -- Queries to update a sqlite CUI table. +# +# $Id$ + +post-auth { + query = "\ + INSERT OR REPLACE INTO ${..cui_table} \ + (clientipaddress, callingstationid, username, cui, lastaccounting) \ + VALUES \ + ('%{%{Packet-Src-IPv6-Address}:-%{Packet-Src-IP-Address}}', '%{Calling-Station-Id}', \ + '%{User-Name}', '%{reply:Chargeable-User-Identity}', NULL)" + +} + +accounting { + reference = "%{tolower:type.%{Acct-Status-Type}.query}" + type { + start { + query = "\ + UPDATE ${....cui_table} SET \ + lastaccounting = CURRENT_TIMESTAMP \ + WHERE clientipaddress = '%{%{Packet-Src-IPv6-Address}:-%{Packet-Src-IP-Address}}' \ + AND callingstationid = '%{Calling-Station-Id}' \ + AND username = '%{User-Name}' \ + AND cui = '%{Chargeable-User-Identity}'" + } + interim-update { + query ="\ + UPDATE ${....cui_table} SET \ + lastaccounting = CURRENT_TIMESTAMP \ + WHERE clientipaddress = '%{%{Packet-Src-IPv6-Address}:-%{Packet-Src-IP-Address}}' \ + AND callingstationid = '%{Calling-Station-Id}' \ + AND username = '%{User-Name}' \ + AND cui = '%{Chargeable-User-Identity}'" + } + stop { + query ="\ + DELETE FROM ${....cui_table} \ + WHERE clientipaddress = '%{%{Packet-Src-IPv6-Address}:-%{Packet-Src-IP-Address}}' \ + AND callingstationid = '%{Calling-Station-Id}' \ + AND username = '%{User-Name}' \ + AND cui = '%{Chargeable-User-Identity}'" + } + } +} diff --git a/raddb/mods-config/sql/cui/sqlite/schema.sql b/raddb/mods-config/sql/cui/sqlite/schema.sql new file mode 100644 index 0000000..8473534 --- /dev/null +++ b/raddb/mods-config/sql/cui/sqlite/schema.sql @@ -0,0 +1,9 @@ +CREATE TABLE `cui` ( + `clientipaddress` varchar(46) NOT NULL default '', + `callingstationid` varchar(50) NOT NULL default '', + `username` varchar(64) NOT NULL default '', + `cui` varchar(32) NOT NULL default '', + `creationdate` timestamp NOT NULL default CURRENT_TIMESTAMP, + `lastaccounting` timestamp NOT NULL default '0000-00-00 00:00:00', + PRIMARY KEY (`username`,`clientipaddress`,`callingstationid`) +); diff --git a/raddb/mods-config/sql/dhcp/mssql/queries.conf b/raddb/mods-config/sql/dhcp/mssql/queries.conf new file mode 100644 index 0000000..8345c70 --- /dev/null +++ b/raddb/mods-config/sql/dhcp/mssql/queries.conf @@ -0,0 +1,52 @@ +# -*- text -*- +# +# dhcp/mssql/queries.conf -- MSSQL configuration for DHCP schema (schema.sql) +# +# $Id$ + +# Safe characters list for sql queries. Everything else is replaced +# with their mime-encoded equivalents. +# The default list should be ok +# safe_characters = "@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_: /" + +####################################################################### +# Query config: Identifier +####################################################################### +# This is the identifier that will get substituted, escaped, and added +# as attribute 'SQL-User-Name'. '%{SQL-User-Name}' should be used +# below everywhere an identifier substitution is needed so you you can +# be sure the identifier passed from the client is escaped properly. +# +sql_user_name = "%{control:DHCP-SQL-Option-Identifier}" + +####################################################################### +# Attribute Lookup Queries +####################################################################### +# These queries setup the reply items in ${dhcpreply_table} and +# ${group_reply_query}. You can use any query/tables you want, but +# the return data for each row MUST be in the following order: +# +# 0. Row ID (currently unused) +# 1. Identifier +# 2. Item Attr Name +# 3. Item Attr Value +# 4. Item Attr Operation +####################################################################### + +authorize_reply_query = "\ + SELECT id, Identifier, Attribute, Value, op \ + FROM ${dhcpreply_table} \ + WHERE Identifier = '%{SQL-User-Name}' AND Context = '%{control:DHCP-SQL-Option-Context}' \ + ORDER BY id" + +authorize_group_reply_query = "\ + SELECT id, GroupName, Attribute, Value, op \ + FROM ${groupreply_table} \ + WHERE GroupName = '%{${group_attribute}}' AND Context = '%{control:DHCP-SQL-Option-Context}' \ + ORDER BY id" + +group_membership_query = "\ + SELECT GroupName \ + FROM ${dhcpgroup_table} \ + WHERE Identifier='%{SQL-User-Name}' AND Context = '%{control:DHCP-SQL-Option-Context}' \ + ORDER BY priority" diff --git a/raddb/mods-config/sql/dhcp/mssql/schema.sql b/raddb/mods-config/sql/dhcp/mssql/schema.sql new file mode 100644 index 0000000..8584949 --- /dev/null +++ b/raddb/mods-config/sql/dhcp/mssql/schema.sql @@ -0,0 +1,91 @@ +-- $Id$ +-- +-- MSSQL schema for DHCP for FreeRADIUS +-- +-- To load: +-- isql -S db_ip_addr -d db_name -U db_login -P db_passwd -i schema.sql + +-- +-- Table structure for table 'dhcpgroupreply' +-- +CREATE TABLE [dhcpgroupreply] ( + [id] [int] IDENTITY (1, 1) NOT NULL, + [GroupName] [varchar] (64) NOT NULL, + [Attribute] [varchar] (32) NOT NULL, + [Value] [varchar] (253) NOT NULL, + [op] [char] (2) NULL, + [prio] [int] NOT NULL, + [Context] [varchar] (16) NOT NULL +) ON [PRIMARY] +GO + +ALTER TABLE [dhcpgroupreply] WITH NOCHECK ADD + CONSTRAINT [DF_dhcpgroupreply_GroupName] DEFAULT ('') FOR [GroupName], + CONSTRAINT [DF_dhcpgroupreply_Attribute] DEFAULT ('') FOR [Attribute], + CONSTRAINT [DF_dhcpgroupreply_Value] DEFAULT ('') FOR [Value], + CONSTRAINT [DF_dhcpgroupreply_op] DEFAULT (null) FOR [op], + CONSTRAINT [DF_dhcpgroupreply_prio] DEFAULT (0) FOR [prio], + CONSTRAINT [DF_dhcpgroupreply_context] DEFAULT ('') FOR [Context], + CONSTRAINT [PK_dhcpgroupreply] PRIMARY KEY NONCLUSTERED + ( + [id] + ) ON [PRIMARY] +GO + +CREATE INDEX [GroupName] ON [dhcpgroupreply]([Context],[GroupName]) ON [PRIMARY] +GO + + +-- +-- Table structure for table 'dhcpreply' +-- +CREATE TABLE [dhcpreply] ( + [id] [int] IDENTITY (1, 1) NOT NULL, + [Identifier] [varchar] (64) NOT NULL, + [Attribute] [varchar] (32) NOT NULL, + [Value] [varchar] (253) NOT NULL, + [op] [char] (2) NULL, + [Context] [varchar] (16) NOT NULL +) ON [PRIMARY] +GO + +ALTER TABLE [dhcpreply] WITH NOCHECK ADD + CONSTRAINT [DF_dhcpreply_Identifier] DEFAULT ('') FOR [Identifier], + CONSTRAINT [DF_dhcpreply_Attribute] DEFAULT ('') FOR [Attribute], + CONSTRAINT [DF_dhcpreply_Value] DEFAULT ('') FOR [Value], + CONSTRAINT [DF_dhcpreply_op] DEFAULT (null) FOR [op], + CONSTRAINT [DF_dhcpreply_Context] DEFAULT ('') FOR [Context], + CONSTRAINT [PK_dhcpreply] PRIMARY KEY NONCLUSTERED + ( + [id] + ) ON [PRIMARY] +GO + +CREATE INDEX [Identifier] ON [dhcpreply]([Context],[Identifier]) ON [PRIMARY] +GO + + +-- +-- Table structure for table 'dhcpgroup' +-- +CREATE TABLE [dhcpgroup] ( + [id] [int] IDENTITY (1, 1) NOT NULL, + [Identifier] [varchar] (64) NOT NULL, + [GroupName] [varchar] (64) NULL, + [Priority] [int] NULL, + [Context] [varchar] (16) NULL +) ON [PRIMARY] +GO + +ALTER TABLE [dhcpgroup] WITH NOCHECK ADD + CONSTRAINT [DF_dhcpgroup_Identifier] DEFAULT ('') FOR [Identifier], + CONSTRAINT [DF_dhcpgroup_GroupName] DEFAULT ('') FOR [GroupName], + CONSTRAINT [DF_dhcpgroup_Context] DEFAULT ('') FOR [Context], + CONSTRAINT [PK_dhcpgroup] PRIMARY KEY NONCLUSTERED + ( + [id] + ) ON [PRIMARY] +GO + +CREATE INDEX [Identifier] ON [dhcpgroup]([Context],[Identifier]) ON [PRIMARY] +GO diff --git a/raddb/mods-config/sql/dhcp/mysql/queries.conf b/raddb/mods-config/sql/dhcp/mysql/queries.conf new file mode 100644 index 0000000..a28037b --- /dev/null +++ b/raddb/mods-config/sql/dhcp/mysql/queries.conf @@ -0,0 +1,75 @@ +# -*- text -*- +# +# dhcp/mysql/queries.conf -- MySQL configuration for DHCP schema (schema.sql) +# +# $Id$ + +# Use the driver specific SQL escape method. +# +# If you enable this configuration item, the "safe_characters" +# configuration is ignored. FreeRADIUS then uses the PostgreSQL escape +# functions to escape input strings. The only downside to making this +# change is that the PostgreSQL escaping method is not the same the one +# used by FreeRADIUS. So characters which are NOT in the +# "safe_characters" list will now be stored differently in the database. +# +#auto_escape = yes + +# Safe characters list for sql queries. Everything else is replaced +# with their mime-encoded equivalents. +# The default list should be ok +# Using 'auto_escape' is preferred +safe_characters = "@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_: /" + +####################################################################### +# Connection config +####################################################################### +# The character set is not configurable. The default character set of +# the mysql client library is used. To control the character set, +# create/edit my.cnf (typically in /etc/mysql/my.cnf or /etc/my.cnf) +# and enter +# [client] +# default-character-set = utf8 +# + +####################################################################### +# Query config: Identifier +####################################################################### +# This is the identifier that will get substituted, escaped, and added +# as attribute 'SQL-User-Name'. '%{SQL-User-Name}' should be used +# below everywhere an identifier substitution is needed so you you can +# be sure the identifier passed from the client is escaped properly. +# +sql_user_name = "%{control:DHCP-SQL-Option-Identifier}" + +####################################################################### +# Attribute Lookup Queries +####################################################################### +# These queries setup the reply items in ${dhcpreply_table} and +# ${group_reply_query}. You can use any query/tables you want, but +# the return data for each row MUST be in the following order: +# +# 0. Row ID (currently unused) +# 1. Identifier +# 2. Item Attr Name +# 3. Item Attr Value +# 4. Item Attr Operation +####################################################################### + +authorize_reply_query = "\ + SELECT id, identifier, attribute, value, Op \ + FROM ${dhcpreply_table} \ + WHERE identifier = '%{SQL-User-Name}' AND context = '%{control:DHCP-SQL-Option-Context}' \ + ORDER BY id" + +authorize_group_reply_query = "\ + SELECT id, groupname, attribute, value, op \ + FROM ${groupreply_table} \ + WHERE groupname = '%{${group_attribute}}' AND context = '%{control:DHCP-SQL-Option-Context}' \ + ORDER BY id" + +group_membership_query = "\ + SELECT groupnme \ + FROM ${dhcpgroup_table} \ + WHERE identifier='%{SQL-User-Name}' AND context = '%{control:DHCP-SQL-Option-Context}' \ + ORDER BY priority" diff --git a/raddb/mods-config/sql/dhcp/mysql/schema.sql b/raddb/mods-config/sql/dhcp/mysql/schema.sql new file mode 100644 index 0000000..85a121a --- /dev/null +++ b/raddb/mods-config/sql/dhcp/mysql/schema.sql @@ -0,0 +1,47 @@ +# +# $Id$ +# +# PostgreSQL schema for DHCP for FreeRADIUS +# +# + +# +# Table structure for table 'dhcpgroupreply' +# +CREATE TABLE IF NOT EXISTS dhcpgroupreply ( + id int(11) unsigned NOT NULL auto_increment, + groupname varchar(64) NOT NULL default '', + attribute varchar(64) NOT NULL default '', + op char(2) NOT NULL DEFAULT '=', + value varchar(253) NOT NULL default '', + context varchar(16) NOT NULL default '', + PRIMARY KEY (id), + KEY groupname (context,groupname(32)) +); + +# +# Table structure for table 'dhcpreply' +# +CREATE TABLE IF NOT EXISTS dhcpreply ( + id int(11) unsigned NOT NULL auto_increment, + identifier varchar(253) NOT NULL default '', + attribute varchar(64) NOT NULL default '', + op char(2) NOT NULL DEFAULT '=', + value varchar(253) NOT NULL default '', + context varchar(16) NOT NULL default '', + PRIMARY KEY (id), + KEY identifier (context,identifier(32)) +); + +# +# Table structure for table 'dhcpgroup' +# +CREATE TABLE IF NOT EXISTS dhcpgroup ( + id int(11) unsigned NOT NULL auto_increment, + identifier varchar(253) NOT NULL default '', + groupname varchar(64) NOT NULL default '', + priority int(11) NOT NULL default '1', + context varchar(16) NOT NULL default '', + PRIMARY KEY (id), + KEY identifier (context,identifier(32)) +); diff --git a/raddb/mods-config/sql/dhcp/mysql/setup.sql b/raddb/mods-config/sql/dhcp/mysql/setup.sql new file mode 100644 index 0000000..d20a82c --- /dev/null +++ b/raddb/mods-config/sql/dhcp/mysql/setup.sql @@ -0,0 +1,21 @@ +/* + * setup.sql -- MySQL commands for creating the RADIUS user. + * + * WARNING: You should change 'localhost' and 'radpass' + * to something else. Also update raddb/mods-available/sql + * with the new RADIUS password. + * + * WARNING: This example file is untested. Use at your own risk. + * Please send any bug fixes to the mailing list. + * + * $Id$ + */ + +/* + * Create default administrator for RADIUS + */ +CREATE USER 'radius'@'localhost' IDENTIFIED BY 'radpass'; + +GRANT SELECT ON radius.dhcpreply TO 'radius'@'localhost'; +GRANT SELECT ON radius.dhcpgroupreply TO 'radius'@'localhost'; +GRANT SELECT ON radius.dhcpgroup TO 'radius'@'localhost'; diff --git a/raddb/mods-config/sql/dhcp/oracle/queries.conf b/raddb/mods-config/sql/dhcp/oracle/queries.conf new file mode 100644 index 0000000..dd312d5 --- /dev/null +++ b/raddb/mods-config/sql/dhcp/oracle/queries.conf @@ -0,0 +1,47 @@ +# -*- text -*- +# +# dhcp/oracle/queries.conf -- Oracle configuration for DHCP schema (schema.sql) +# +# $Id$ + +####################################################################### +# Query config: Identifier +####################################################################### +# This is the identifier that will get substituted, escaped, and added +# as attribute 'SQL-User-Name'. '%{SQL-User-Name}' should be used +# below everywhere an identifier substitution is needed so you you can +# be sure the identifier passed from the client is escaped properly. +# +sql_user_name = "%{control:DHCP-SQL-Option-Identifier}" + +####################################################################### +# Attribute Lookup Queries +####################################################################### +# These queries setup the reply items in ${dhcpreply_table} and +# ${group_reply_query}. You can use any query/tables you want, but +# the return data for each row MUST be in the following order: +# +# 0. Row ID (currently unused) +# 1. Identifier +# 2. Item Attr Name +# 3. Item Attr Value +# 4. Item Attr Operation +####################################################################### + +authorize_reply_query = "\ + SELECT id, identifier, attribute, value, op \ + FROM ${dhcpreply_table} \ + WHERE identifier = '%{SQL-User-Name}' AND context = '%{control:DHCP-SQL-Option-Context}' \ + ORDER BY id" + +authorize_group_reply_query = "\ + SELECT id, groupname, attribute, value, op \ + FROM ${groupreply_table} \ + WHERE groupname = '%{${group_attribute}}' AND context = '%{control:DHCP-SQL-Option-Context}' \ + ORDER BY id" + +group_membership_query = "\ + SELECT groupname \ + FROM ${dhcpgroup_table} \ + WHERE identifier='%{SQL-User-Name}' AND context = '%{control:DHCP-SQL-Option-Context}' \ + ORDER BY priority" diff --git a/raddb/mods-config/sql/dhcp/oracle/schema.sql b/raddb/mods-config/sql/dhcp/oracle/schema.sql new file mode 100644 index 0000000..085e346 --- /dev/null +++ b/raddb/mods-config/sql/dhcp/oracle/schema.sql @@ -0,0 +1,81 @@ +/* + * $Id$ + * + * Oracle schema for DHCP for FreeRADIUS + * + */ + +/* + * Table structure for table 'dhcpgroupreply' + */ +CREATE TABLE dhcpgroupreply ( + id INT PRIMARY KEY, + groupname VARCHAR(64) NOT NULL, + attribute VARCHAR(64) NOT NULL, + op CHAR(2) NOT NULL, + value VARCHAR(253) NOT NULL, + context VARCHAR(16) NOT NULL +); +CREATE INDEX dhcpgroupreply_idx1 ON dhcpgroupreply(context,groupname); +CREATE SEQUENCE dhcpgroupreply_seq START WITH 1 INCREMENT BY 1; + +/* Trigger to emulate a serial # on the primary key */ +CREATE OR REPLACE TRIGGER dhcpgroupreply_serialnumber + BEFORE INSERT OR UPDATE OF id ON dhcpgroupreply + FOR EACH ROW + BEGIN + if ( :new.id = 0 or :new.id is null ) then + SELECT dhcpgroupreply_seq.nextval into :new.id from dual; + end if; + END; +/ + +/* + * Table structure for table 'dhcpreply' + */ +CREATE TABLE dhcpreply ( + id INT PRIMARY KEY, + identifier VARCHAR(253) NOT NULL, + attribute VARCHAR(64) NOT NULL, + op CHAR(2) NOT NULL, + value VARCHAR(253) NOT NULL, + context VARCHAR(16) NOT NULL +); +CREATE INDEX dhcpreply_idx1 ON dhcpreply(context,identifier); +CREATE SEQUENCE dhcpreply_seq START WITH 1 INCREMENT BY 1; + +/* Trigger to emulate a serial # on the primary key */ +CREATE OR REPLACE TRIGGER dhcpreply_serialnumber + BEFORE INSERT OR UPDATE OF id ON dhcpreply + FOR EACH ROW + BEGIN + if ( :new.id = 0 or :new.id is null ) then + SELECT dhcpreply_seq.nextval into :new.id from dual; + end if; + END; +/ + +/* + * Table structure for table 'dhcpgroup' + */ +CREATE TABLE dhcpgroup ( + id INT PRIMARY KEY, + identifier VARCHAR(253) NOT NULL, + groupname VARCHAR(64) NOT NULL, + priority INT NOT NULL, + context VARCHAR(16) NOT NULL +); +CREATE INDEX dhcpgroup_idx1 ON dhcpgroup(context,identifier); +CREATE SEQUENCE dhcpgroup_seq START WITH 1 INCREMENT BY 1; + +/* Trigger to emulate a serial # on the primary key */ +CREATE OR REPLACE TRIGGER dhcpgroup_serialnumber + BEFORE INSERT OR UPDATE OF id ON dhcpgroup + FOR EACH ROW + BEGIN + if ( :new.id = 0 or :new.id is null ) then + SELECT dhcpgroup_seq.nextval into :new.id from dual; + end if; + END; +/ + diff --git a/raddb/mods-config/sql/dhcp/postgresql/queries.conf b/raddb/mods-config/sql/dhcp/postgresql/queries.conf new file mode 100644 index 0000000..14ca79a --- /dev/null +++ b/raddb/mods-config/sql/dhcp/postgresql/queries.conf @@ -0,0 +1,76 @@ +# -*- text -*- +# +# dhcp/postgresql/queries.conf -- PostgreSQL configuration for DHCP schema (schema.sql) +# +# $Id$ + +# Use the driver specific SQL escape method. +# +# If you enable this configuration item, the "safe_characters" +# configuration is ignored. FreeRADIUS then uses the PostgreSQL escape +# functions to escape input strings. The only downside to making this +# change is that the PostgreSQL escaping method is not the same the one +# used by FreeRADIUS. So characters which are NOT in the +# "safe_characters" list will now be stored differently in the database. +# +#auto_escape = yes + +# Safe characters list for sql queries. Everything else is replaced +# with their mime-encoded equivalents. +# The default list should be ok +# Using 'auto_escape' is preferred +# safe_characters = "@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_: /" + +####################################################################### +# Query config: Identifier +####################################################################### +# This is the identifier that will get substituted, escaped, and added +# as attribute 'SQL-User-Name'. '%{SQL-User-Name}' should be used +# below everywhere an identifier substitution is needed so you you can +# be sure the identifier passed from the client is escaped properly. +# +sql_user_name = "%{control:DHCP-SQL-Option-Identifier}" + +####################################################################### +# Open Query +####################################################################### +# This query is run whenever a new connection is opened. +# It is commented out by default. +# +# If you have issues with connections hanging for too long, uncomment +# the next line, and set the timeout in milliseconds. As a general +# rule, if the queries take longer than a second, something is wrong +# with the database. +#open_query = "set statement_timeout to 1000" + +####################################################################### +# Attribute Lookup Queries +####################################################################### +# These queries setup the reply items in ${dhcpreply_table} and +# ${group_reply_query}. You can use any query/tables you want, but +# the return data for each row MUST be in the following order: +# +# 0. Row ID (currently unused) +# 1. Identifier +# 2. Item Attr Name +# 3. Item Attr Value +# 4. Item Attr Operation +####################################################################### + +authorize_reply_query = "\ + SELECT id, Identifier, Attribute, Value, Op \ + FROM ${dhcpreply_table} \ + WHERE Identifier = '%{SQL-User-Name}' AND Context = '%{control:DHCP-SQL-Option-Context}' \ + ORDER BY id" + +authorize_group_reply_query = "\ + SELECT id, GroupName, Attribute, Value, op \ + FROM ${groupreply_table} \ + WHERE GroupName = '%{${group_attribute}}' AND Context = '%{control:DHCP-SQL-Option-Context}' \ + ORDER BY id" + +group_membership_query = "\ + SELECT GroupName \ + FROM ${dhcpgroup_table} \ + WHERE Identifier='%{SQL-User-Name}' AND Context = '%{control:DHCP-SQL-Option-Context}' \ + ORDER BY priority" diff --git a/raddb/mods-config/sql/dhcp/postgresql/schema.sql b/raddb/mods-config/sql/dhcp/postgresql/schema.sql new file mode 100644 index 0000000..0d1727f --- /dev/null +++ b/raddb/mods-config/sql/dhcp/postgresql/schema.sql @@ -0,0 +1,44 @@ +/* + * $Id$ + * + * PostgreSQL schema for DHCP for FreeRADIUS + * + */ + +/* + * Table structure for table 'dhcpgroupreply' + */ +CREATE TABLE IF NOT EXISTS dhcpgroupreply ( + id serial PRIMARY KEY, + GroupName text NOT NULL DEFAULT '', + Attribute text NOT NULL DEFAULT '', + op VARCHAR(2) NOT NULL DEFAULT '=', + Value text NOT NULL DEFAULT '', + Context text NOT NULL DEFAULT '' +); +CREATE INDEX dhcpgroupreply_GroupName ON dhcpgroupreply (Context,GroupName,Attribute); + +/* + * Table structure for table 'dhcpreply' + */ +CREATE TABLE IF NOT EXISTS dhcpreply ( + id serial PRIMARY KEY, + Identifier text NOT NULL DEFAULT '', + Attribute text NOT NULL DEFAULT '', + op VARCHAR(2) NOT NULL DEFAULT '=', + Value text NOT NULL DEFAULT '', + Context text NOT NULL DEFAULT '' +); +CREATE INDEX dhcpreply_Identifier ON dhcpreply (Context,Identifier,Attribute); + +/* + * Table structure for table 'dhcpgroup' + */ +CREATE TABLE IF NOT EXISTS dhcpgroup ( + id serial PRIMARY KEY, + Identifier text NOT NULL DEFAULT '', + GroupName text NOT NULL DEFAULT '', + Priority integer NOT NULL DEFAULT 0, + Context text NOT NULL DEFAULT '' +); +CREATE INDEX dhcpgroup_Identifier ON dhcpgroup (Context,Identifier); diff --git a/raddb/mods-config/sql/dhcp/postgresql/setup.sql b/raddb/mods-config/sql/dhcp/postgresql/setup.sql new file mode 100644 index 0000000..884aa5a --- /dev/null +++ b/raddb/mods-config/sql/dhcp/postgresql/setup.sql @@ -0,0 +1,28 @@ +/* + * admin.sql -- PostgreSQL commands for creating the RADIUS user. + * + * WARNING: You should change 'localhost' and 'radpass' + * to something else. Also update raddb/mods-available/sql + * with the new RADIUS password. + * + * WARNING: This example file is untested. Use at your own risk. + * Please send any bug fixes to the mailing list. + * + * $Id$ + */ + +/* + * Create default administrator for RADIUS + */ +CREATE USER radius WITH PASSWORD 'radpass'; + +/* + * The server can read any table in SQL + */ +GRANT SELECT ON dhcpreply TO radius; +GRANT SELECT ON dhcpgroupreply TO radius; +GRANT SELECT ON dhcpgroup TO radius; + +GRANT USAGE, SELECT ON SEQUENCE dhcpgroupreply_id_seq TO radius; +GRANT USAGE, SELECT ON SEQUENCE dhcpreply_id_seq TO radius; +GRANT USAGE, SELECT ON SEQUENCE dhcpgroup_id_seq TO radius; diff --git a/raddb/mods-config/sql/dhcp/sqlite/queries.conf b/raddb/mods-config/sql/dhcp/sqlite/queries.conf new file mode 100644 index 0000000..0cc7202 --- /dev/null +++ b/raddb/mods-config/sql/dhcp/sqlite/queries.conf @@ -0,0 +1,52 @@ +# -*- text -*- +# +# dhcp/sqlite/queries.conf -- SQLite configuration for DHCP schema (schema.sql) +# +# $Id$ + +# Safe characters list for sql queries. Everything else is replaced +# with their mime-encoded equivalents. +# The default list should be ok +# safe_characters = "@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_: /" + +####################################################################### +# Query config: Identifier +####################################################################### +# This is the identifier that will get substituted, escaped, and added +# as attribute 'SQL-User-Name'. '%{SQL-User-Name}' should be used +# below everywhere an identifier substitution is needed so you you can +# be sure the identifier passed from the client is escaped properly. +# +sql_user_name = "%{control:DHCP-SQL-Option-Identifier}" + +####################################################################### +# Attribute Lookup Queries +####################################################################### +# These queries setup the reply items in ${dhcpreply_table} and +# ${group_reply_query}. You can use any query/tables you want, but +# the return data for each row MUST be in the following order: +# +# 0. Row ID (currently unused) +# 1. Identifier +# 2. Item Attr Name +# 3. Item Attr Value +# 4. Item Attr Operation +####################################################################### + +authorize_reply_query = "\ + SELECT id, identifier, attribute, value, op \ + FROM ${dhcpreply_table} \ + WHERE identifier = '%{SQL-User-Name}' AND context = '%{control:DHCP-SQL-Option-Context}' \ + ORDER BY id" + +authorize_group_reply_query = "\ + SELECT id, groupname, attribute, value, op \ + FROM ${groupreply_table} \ + WHERE groupname = '%{${group_attribute}}' AND context = '%{control:DHCP-SQL-Option-Context}' \ + ORDER BY id" + +group_membership_query = "\ + SELECT groupname \ + FROM ${dhcpgroup_table} \ + WHERE identifier='%{SQL-User-Name}' AND context = '%{control:DHCP-SQL-Option-Context}' \ + ORDER BY priority" diff --git a/raddb/mods-config/sql/dhcp/sqlite/schema.sql b/raddb/mods-config/sql/dhcp/sqlite/schema.sql new file mode 100644 index 0000000..54a9abb --- /dev/null +++ b/raddb/mods-config/sql/dhcp/sqlite/schema.sql @@ -0,0 +1,46 @@ +----------------------------------------------------------------------------- +-- $Id$ ␉···· -- +-- -- +-- schema.sql rlm_sql - FreeRADIUS SQLite Module -- +-- -- +-- Database schema for SQLite rlm_sql module for DHCP -- +-- -- +----------------------------------------------------------------------------- + +-- +-- Table structure for table 'dhcpgroupreply' +-- +CREATE TABLE IF NOT EXISTS dhcpgroupreply ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + groupname varchar(64) NOT NULL default '', + attribute varchar(64) NOT NULL default '', + op char(2) NOT NULL DEFAULT '=', + value varchar(253) NOT NULL default '', + context varchar(16) NOT NULL default '' +); +CREATE INDEX dhcpgroupreply_groupname ON dhcpgroupreply(context,groupname); + +-- +-- Table structure for table 'dhcpreply' +-- +CREATE TABLE IF NOT EXISTS dhcpreply ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + identifier varchar(253) NOT NULL default '', + attribute varchar(64) NOT NULL default '', + op char(2) NOT NULL DEFAULT '=', + value varchar(253) NOT NULL default '', + context varchar(16) NOT NULL default '' +); +CREATE INDEX dhcpreply_identifier ON dhcpreply(context,identifier); + +-- +-- Table structure for table 'dhcpgroup' +-- +CREATE TABLE IF NOT EXISTS dhcpgroup ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + identifier varchar(253) NOT NULL default '', + groupname varchar(64) NOT NULL default '', + priority int(11) NOT NULL default '1', + context varchar(16) NOT NULL default '' +); +CREATE INDEX dhcpgroup_identifier ON dhcpgroup(context,identifier); diff --git a/raddb/mods-config/sql/ippool-dhcp/mssql/procedure.sql b/raddb/mods-config/sql/ippool-dhcp/mssql/procedure.sql new file mode 100644 index 0000000..4cfbe1c --- /dev/null +++ b/raddb/mods-config/sql/ippool-dhcp/mssql/procedure.sql @@ -0,0 +1,159 @@ +-- +-- A stored procedure to reallocate a user's previous address, otherwise +-- provide a free address. +-- +-- Using this SP reduces the usual set dialogue of queries to a single +-- query: +-- +-- BEGIN TRAN; "SELECT FOR UPDATE"; UPDATE; COMMIT TRAN; -> EXEC sp +-- +-- The stored procedure is executed on an database instance within a single +-- round trip which often leads to reduced deadlocking and significant +-- performance improvements especially on multi-master clusters, perhaps even +-- by an order of magnitude or more. +-- +-- To use this stored procedure the corresponding queries.conf statements must +-- be configured as follows: +-- +-- allocate_begin = "" +-- allocate_find = "\ +-- EXEC fr_dhcp_allocate_previous_or_new_framedipaddress \ +-- @v_pool_name = '%{control:${pool_name}}', \ +-- @v_gateway = '%{DHCP-Gateway-IP-Address}', \ +-- @v_pool_key = '${pool_key}', \ +-- @v_lease_duration = ${lease_duration}, \ +-- @v_requested_address = '%{%{${req_attribute_name}}:-0.0.0.0}' \ +-- " +-- allocate_update = "" +-- allocate_commit = "" +-- + +CREATE OR ALTER PROCEDURE fr_dhcp_allocate_previous_or_new_framedipaddress + @v_pool_name VARCHAR(64), + @v_gateway VARCHAR(15), + @v_pool_key VARCHAR(64), + @v_lease_duration INT, + @v_requested_address VARCHAR(15) +AS + BEGIN + + -- MS SQL lacks a "SELECT FOR UPDATE" statement, and its table + -- hints do not provide a direct means to implement the row-level + -- read lock needed to guarentee that concurrent queries do not + -- select the same Framed-IP-Address for allocation to distinct + -- users. + -- + -- The "WITH cte AS ( SELECT ... ) UPDATE cte ... OUTPUT INTO" + -- patterns in this procedure body compensate by wrapping + -- the SELECT in a synthetic UPDATE which locks the row. + + DECLARE @r_address_tab TABLE(id VARCHAR(15)); + DECLARE @r_address VARCHAR(15); + + BEGIN TRAN; + + -- Reissue an existing IP address lease when re-authenticating a session + -- + WITH cte AS ( + SELECT TOP(1) FramedIPAddress + FROM dhcpippool WITH (rowlock, readpast) + JOIN dhcpstatus + ON dhcpstatus.status_id = dhcpippool.status_id + WHERE pool_name = @v_pool_name + AND expiry_time > CURRENT_TIMESTAMP + AND pool_key = @v_pool_key + AND dhcpstatus.status IN ('dynamic', 'static') + ) + UPDATE cte + SET FramedIPAddress = FramedIPAddress + OUTPUT INSERTED.FramedIPAddress INTO @r_address_tab; + SELECT @r_address = id FROM @r_address_tab; + + -- Reissue an user's previous IP address, provided that the lease is + -- available (i.e. enable sticky IPs) + -- + -- When using this SELECT you should delete the one above. You must also + -- set allocate_clear = "" in queries.conf to persist the associations + -- for expired leases. + -- + -- WITH cte AS ( + -- SELECT TOP(1) FramedIPAddress + -- FROM dhcpippool WITH (rowlock, readpast) + -- JOIN dhcpstatus + -- ON dhcpstatus.status_id = dhcpippool.status_id + -- WHERE pool_name = @v_pool_name + -- AND pool_key = @v_pool_key + -- AND dhcpstatus.status IN ('dynamic', 'static') + -- ) + -- UPDATE cte + -- SET FramedIPAddress = FramedIPAddress + -- OUTPUT INSERTED.FramedIPAddress INTO @r_address_tab; + -- SELECT @r_address = id FROM @r_address_tab; + + -- Issue the requested IP address if it is available + -- + IF @r_address IS NULL AND @v_requested_address <> '0.0.0.0' + BEGIN + WITH cte AS ( + SELECT TOP(1) FramedIPAddress + FROM dhcpippool WITH (rowlock, readpast) + JOIN dhcpstatus + ON dhcpstatus.status_id = dhcpippool.status_id + WHERE pool_name = @v_pool_name + AND framedipaddress = @v_requested_address + AND dhcpstatus.status = 'dynamic' + AND ( pool_key = @v_pool_name OR expiry_time < CURRENT_TIMESTAMP ) + ) + UPDATE cte + SET FramedIPAddress = FramedIPAddress + OUTPUT INSERTED.FramedIPAddress INTO @r_address_tab; + SELECT @r_address = id FROM @r_address_tab; + END + + -- If we didn't reallocate a previous address then pick the least + -- recently used address from the pool which maximises the likelihood + -- of re-assigning the other addresses to their recent user + -- + IF @r_address IS NULL + BEGIN + WITH cte AS ( + SELECT TOP(1) FramedIPAddress + FROM dhcpippool WITH (rowlock, readpast) + JOIN dhcpstatus + ON dhcpstatus.status_id = dhcpippool.status_id + WHERE pool_name = @v_pool_name + AND expiry_time < CURRENT_TIMESTAMP + AND dhcpstatus.status = 'dynamic' + ORDER BY + expiry_time + ) + UPDATE cte + SET FramedIPAddress = FramedIPAddress + OUTPUT INSERTED.FramedIPAddress INTO @r_address_tab; + SELECT @r_address = id FROM @r_address_tab; + END + + -- Return nothing if we failed to allocated an address + -- + IF @r_address IS NULL + BEGIN + COMMIT TRAN; + RETURN; + END + + -- Update the pool having allocated an IP address + -- + UPDATE dhcpippool + SET + gateway = @v_gateway, + pool_key = @v_pool_key, + expiry_time = DATEADD(SECOND,@v_lease_duration,CURRENT_TIMESTAMP) + WHERE framedipaddress = @r_address; + + COMMIT TRAN; + + -- Return the address that we allocated + SELECT @r_address; + + END +GO diff --git a/raddb/mods-config/sql/ippool-dhcp/mssql/queries.conf b/raddb/mods-config/sql/ippool-dhcp/mssql/queries.conf new file mode 100644 index 0000000..c919e2d --- /dev/null +++ b/raddb/mods-config/sql/ippool-dhcp/mssql/queries.conf @@ -0,0 +1,257 @@ +# -*- text -*- +# +# ippool-dhcp/mssql/queries.conf -- MSSQL queries for rlm_sqlippool +# +# $Id$ + +# ***************** +# * DHCP DISCOVER * +# ***************** + +# +# This series of queries allocates an IP address +# + +# +# MSSQL-specific syntax - required if finding the address and updating +# it are separate queries +# +#allocate_begin = "BEGIN TRAN" +#allocate_commit = "COMMIT TRAN" + +allocate_begin = "" +allocate_commit = "" + +# +# Attempt to find the most recent existing IP address for the client +# +allocate_existing = "\ + WITH cte AS ( \ + SELECT TOP(1) framedipaddress, expiry_time, gateway \ + FROM ${ippool_table} WITH (xlock rowlock readpast) \ + JOIN dhcpstatus ON ${ippool_table}.status_id = dhcpstatus.status_id \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND pool_key = '${pool_key}' \ + AND dhcpstatus.status IN ('dynamic', 'static') \ + ORDER BY expiry_time DESC \ + ) \ + UPDATE cte \ + SET expiry_time = DATEADD(SECOND,${offer_duration},CURRENT_TIMESTAMP), \ + gateway = '%{DHCP-Gateway-IP-Address}' \ + OUTPUT INSERTED.FramedIPAddress \ + FROM ${ippool_table}" + +# +# Determine whether the requested IP address is available +# +allocate_requested = "\ + WITH cte AS ( \ + SELECT TOP(1) framedipaddress, expiry_time, gateway \ + FROM ${ippool_table} WITH (xlock rowlock readpast) \ + JOIN dhcpstatus ON ${ippool_table}.status_id = dhcpstatus.status_id \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND framedipaddress = '%{%{${req_attribute_name}}:-0.0.0.0}' \ + AND dhcpstatus.status = 'dynamic' \ + AND expiry_time < CURRENT_TIMESTAMP \ + ) \ + UPDATE cte \ + SET expiry_time = DATEADD(SECOND,${offer_duration},CURRENT_TIMESTAMP), \ + gateway = '%{DHCP-Gateway-IP-Address}', \ + pool_key = '${pool_key}' \ + OUTPUT INSERTED.FramedIPAddress \ + FROM ${ippool_table}" + +# +# If the existing address can't be found this query will be run to +# find a free address +# +allocate_find = "\ + WITH cte AS ( \ + SELECT TOP(1) framedipaddress, expiry_time, gateway, pool_key \ + FROM ${ippool_table} WITH (xlock rowlock readpast) \ + JOIN dhcpstatus ON ${ippool_table}.status_id = dhcpstatus.status_id \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND expiry_time < CURRENT_TIMESTAMP \ + AND dhcpstatus.status = 'dynamic' \ + ORDER BY expiry_time \ + ) \ + UPDATE cte \ + SET expiry_time = DATEADD(SECOND,${offer_duration},CURRENT_TIMESTAMP), \ + gateway = '%{DHCP-Gateway-IP-Address}', \ + pool_key = '${pool_key}' \ + OUTPUT INSERTED.FramedIPAddress \ + FROM ${ippool_table}" + +# +# Alternatively attempt all in one, more complex, query +# +# The ORDER BY clause of this query tries to allocate the same IP-address +# which user had last session. Ensure that pool_key is unique to the user +# within a given pool. +# +#allocate_find = "\ +# UPDATE TOP(1) ${ippool_table} \ +# SET FramedIPAddress = FramedIPAddress, \ +# pool_key = '${pool_key}', \ +# expiry_time = DATEADD(SECOND,${offer_duration},CURRENT_TIMESTAMP), \ +# GatewayIPAddress = '%{DHCP-Gateway-IP-Address}' \ +# OUTPUT INSERTED.FramedIPAddress \ +# FROM ${ippool_table} \ +# WHERE ${ippool_table}.id IN ( \ +# SELECT TOP (1) id FROM ( \ +# (SELECT TOP(1) id, 1 AS o FROM ${ippool_table} WITH (xlock rowlock readpast) \ +# JOIN dhcpstatus ON ${ippool_table}.status_id = dhcpstatus.status_id \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND pool_key = '${pool_key}' \ +# AND dhcpstatus.status IN ('dynamic', 'static')) \ +# UNION \ +# (SELECT TOP(1) id, 2 AS o FROM ${ippool_table} WITH (xlock rowlock readpast) \ +# JOIN dhcpstatus ON ${ippool_table}.status_id = dhcpstatus.status_id \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND framedipaddress = '%{%{${req_attribute_name}}:-0.0.0.0}' \ +# AND dhcpstatus.status = 'dynamic' \ +# AND ( pool_key = '%{pool_key}' OR expiry_time < CURRENT_TIMESTAMP )) \ +# UNION \ +# (SELECT TOP(1) id, 3 AS o FROM ${ippool_table} WITH (xlock rowlock readpast) \ +# JOIN dhcpstatus ON ${ippool_table}.status_id = dhcpstatus.status_id \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND expiry_time < CURRENT_TIMESTAMP \ +# AND dhcpstatus.status = 'dynamic' \ +# ORDER BY expiry_time) \ +# ) AS q ORDER BY q.o \ +# )" + +# +# If you prefer to allocate a random IP address every time, use this query instead. +# Note: This is very slow if you have a lot of free IPs. +# +#allocate_find = "\ +# WITH cte AS ( \ +# SELECT TOP(1) FramedIPAddress FROM ${ippool_table} \ +# JOIN dhcpstatus ON ${ippool_table}.status_id = dhcpstatus.status_id \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND expiry_time < CURRENT_TIMESTAMP \ +# AND dhcpstatus.status = 'dynamic' \ +# ORDER BY \ +# newid() \ +# ) \ +# UPDATE cte WITH (rowlock, readpast) \ +# SET FramedIPAddress = FramedIPAddress \ +# OUTPUT INSERTED.FramedIPAddress" + +# +# If an IP could not be allocated, check to see if the pool exists or not +# This allows the module to differentiate between a full pool and no pool +# Note: If you are not running redundant pool modules this query may be +# commented out to save running this query every time an ip is not allocated. +# +pool_check = "\ + SELECT TOP(1) id \ + FROM ${ippool_table} \ + WHERE pool_name='%{control:${pool_name}}'" + +# +# This is the final IP Allocation query, which saves the allocated ip details. +# Only needed if the initial "find" query is not storing the allocation. +# +#allocate_update = "\ +# UPDATE ${ippool_table} \ +# SET \ +# gateway = '%{DHCP-Gateway-IP-Address}', pool_key = '${pool_key}', \ +# expiry_time = DATEADD(SECOND,${offer_duration},CURRENT_TIMESTAMP) \ +# WHERE FramedIPAddress = '%I'" + +# +# Use a stored procedure to find AND allocate the address. Read and customise +# `procedure.sql` in this directory to determine the optimal configuration. +# +#allocate_begin = "" +#allocate_find = "\ +# EXEC fr_dhcp_allocate_previous_or_new_framedipaddress \ +# @v_pool_name = '%{control:${pool_name}}', \ +# @v_gateway = '%{DHCP-Gateway-IP-Address}', \ +# @v_pool_key = '${pool_key}', \ +# @v_lease_duration = ${offer_duration}, \ +# @v_requested_address = '%{%{${req_attribute_name}}:-0.0.0.0}' \ +# " +#allocate_update = "" +#allocate_commit = "" + + +# **************** +# * DHCP REQUEST * +# **************** + +# +# This query revokes any active offers for addresses that a client is not +# requesting when a DHCP REQUEST packet arrives +# +start_update = "\ + UPDATE ${ippool_table} \ + SET \ + gateway = '', \ + pool_key = '', \ + expiry_time = CURRENT_TIMESTAMP \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND pool_key = '${pool_key}' \ + AND framedipaddress <> '%{DHCP-Requested-IP-Address}' \ + AND expiry_time > CURRENT_TIMESTAMP \ + AND ${ippool_table}.status_id IN \ + (SELECT status_id FROM dhcpstatus WHERE status = 'dynamic')" + +# +# This query extends an existing lease (or offer) when a DHCP REQUEST packet +# arrives. This query must update a row when a lease is succesfully requested +# - queries that update no rows will result in a "notfound" response to +# the module which by default will give a DHCP-NAK reply. In this example +# incrementing "counter" is used to achieve this. +# +alive_update = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = DATEADD(SECOND,${lease_duration},CURRENT_TIMESTAMP), \ + counter = counter + 1 \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND pool_key = '${pool_key}' \ + AND framedipaddress = '%{%{DHCP-Requested-IP-Address}:-%{DHCP-Client-IP-Address}}'" + + +# **************** +# * DHCP RELEASE * +# **************** + +# +# This query frees an IP address when a DHCP RELEASE packet arrives +# +stop_clear = "\ + UPDATE ${ippool_table} \ + SET \ + gateway = '', \ + pool_key = '', \ + expiry_time = CURRENT_TIMESTAMP \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND pool_key = '${pool_key}' \ + AND FramedIPAddress = '%{DHCP-Client-IP-Address}' \ + AND ${ippool_table}.status_id IN \ + (SELECT status_id FROM dhcpstatus WHERE status = 'dynamic')" + +# +# This query is not applicable to DHCP +# +on_clear = "" + + +# **************** +# * DHCP DECLINE * +# **************** + +# +# This query marks an IP address as declined when a DHCP Decline +# packet arrives +# +off_clear = "\ + UPDATE ${ippool_table} \ + SET status_id = (SELECT status_id FROM dhcpstatus WHERE status = 'declined') \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND pool_key = '${pool_key}' \ + AND framedipaddress = '%{DHCP-Requested-IP-Address}'" diff --git a/raddb/mods-config/sql/ippool-dhcp/mssql/schema.sql b/raddb/mods-config/sql/ippool-dhcp/mssql/schema.sql new file mode 100644 index 0000000..dae4eff --- /dev/null +++ b/raddb/mods-config/sql/ippool-dhcp/mssql/schema.sql @@ -0,0 +1,40 @@ +-- +-- Table structure for table 'dhcpippool' +-- +-- See also "procedure.sql" in this directory for +-- a stored procedure that gives much faster response. +-- + +CREATE TABLE dhcpstatus ( + status_id int NOT NULL, + status varchar(10) NOT NULL, + PRIMARY KEY (status_id) +) +GO + +INSERT INTO dhcpstatus (status_id, status) VALUES (1, 'dynamic'), (2, 'static'), (3, 'declined'), (4, 'disabled') +GO + +CREATE TABLE dhcpippool ( + id int IDENTITY (1,1) NOT NULL, + pool_name varchar(30) NOT NULL, + FramedIPAddress varchar(15) NOT NULL default '', + pool_key varchar(30) NOT NULL default '', + gateway varchar(15) NOT NULL default '', + expiry_time DATETIME NOT NULL default CURRENT_TIMESTAMP, + status_id int NOT NULL default 1, + counter int NOT NULL default 0, + CONSTRAINT fk_status_id FOREIGN KEY (status_id) REFERENCES dhcpstatus (status_id), + PRIMARY KEY (id) +) +GO + +CREATE INDEX dhcp_poolname_expire ON dhcpippool(pool_name, expiry_time) +GO + +CREATE INDEX dhcp_FramedIPAddress ON dhcpippool(FramedIPAddress) +GO + +CREATE INDEX dhcp_poolname_poolkey_FramedIPAddress ON dhcpippool(pool_name, pool_key, FramedIPAddress) +GO + diff --git a/raddb/mods-config/sql/ippool-dhcp/mysql/procedure-no-skip-locked.sql b/raddb/mods-config/sql/ippool-dhcp/mysql/procedure-no-skip-locked.sql new file mode 100644 index 0000000..bee37de --- /dev/null +++ b/raddb/mods-config/sql/ippool-dhcp/mysql/procedure-no-skip-locked.sql @@ -0,0 +1,160 @@ +-- +-- A stored procedure to reallocate a user's previous address, otherwise +-- provide a free address. +-- +-- NOTE: This version of the SP is intended for MySQL variants that do not +-- support the SKIP LOCKED pragma, i.e. MariaDB and versions of MySQL +-- prior to 8.0. It should be a lot faster than using the default SP +-- without the SKIP LOCKED pragma under highly concurrent workloads +-- and not result in thread starvation. +-- +-- It is however a *useful hack* which should not be used if SKIP +-- LOCKED is available. +-- +-- WARNING: This query uses server-local, "user locks" (GET_LOCK and +-- RELEASE_LOCK), without the need for a transaction, to emulate +-- row locking with locked-row skipping. User locks are not +-- supported on clusters such as Galera and MaxScale. +-- +-- Using this SP reduces the usual set dialogue of queries to a single +-- query: +-- +-- START TRANSACTION; SELECT FOR UPDATE; UPDATE; COMMIT; -> CALL sp() +-- +-- The stored procedure is executed within a single round trip which often +-- leads to reduced deadlocking and significant performance improvements. +-- +-- To use this stored procedure the corresponding queries.conf statements must +-- be configured as follows: +-- +-- allocate_begin = "" +-- allocate_find = "\ +-- CALL fr_dhcp_allocate_previous_or_new_framedipaddress( \ +-- '%{control:${pool_name}}', \ +-- '%{DHCP-Gateway-IP-Address}', \ +-- '${pool_key}', \ +-- ${lease_duration}, \ +-- '%{%{${req_attribute_name}}:-0.0.0.0}' \ +-- )" +-- allocate_update = "" +-- allocate_commit = "" +-- + +DELIMITER $$ + +DROP PROCEDURE IF EXISTS fr_dhcp_allocate_previous_or_new_framedipaddress; +CREATE PROCEDURE fr_allocate_previous_or_new_framedipaddress ( + IN v_pool_name VARCHAR(64), + IN v_gateway VARCHAR(15), + IN v_pool_key VARCHAR(64), + IN v_lease_duration INT, + IN v_requested_address VARCHAR(15) +) +SQL SECURITY INVOKER +proc:BEGIN + DECLARE r_address VARCHAR(15); + + -- Reissue an existing IP address lease when re-authenticating a session + -- + -- Note: In this query we get away without the need for FOR UPDATE + -- becase: + -- + -- (a) Each existing lease only belongs to a single device, so + -- no two devices will be racing over a single address. + -- (b) The set of existing leases (not yet expired) are + -- disjoint from the set of free leases, so not subject to + -- reallocation. + -- + SELECT framedipaddress INTO r_address + FROM dhcpippool + WHERE pool_name = v_pool_name + AND expiry_time > NOW() + AND pool_key = v_pool_key + AND `status` IN ('dynamic', 'static') + LIMIT 1; + + -- Reissue an user's previous IP address, provided that the lease is + -- available (i.e. enable sticky IPs) + -- + -- When using this SELECT you should delete the one above. You must also + -- set allocate_clear = "" in queries.conf to persist the associations + -- for expired leases. + -- + -- SELECT framedipaddress INTO r_address + -- FROM dhcpippool + -- WHERE pool_name = v_pool_name + -- AND pool_key = v_pool_key + -- AND `status` IN ('dynamic', 'static') + -- LIMIT 1; + + -- + -- Normally here we would honour an IP address hint if the IP were + -- available, however we cannot do that without taking a lock which + -- defeats the purpose of this version of the stored procedure. + -- + -- It you need to honour an IP address hint then use a database with + -- support for SKIP LOCKED and use the normal stored procedure. + -- + + IF r_address IS NOT NULL THEN + UPDATE dhcpippool + SET + gateway = v_gateway, + pool_key = v_pool_key, + expiry_time = NOW() + INTERVAL v_lease_duration SECOND + WHERE + framedipaddress = r_address; + SELECT r_address; + LEAVE proc; + END IF; + + REPEAT + + -- If we didn't reallocate a previous address then pick the least + -- recently used address from the pool which maximises the likelihood + -- of re-assigning the other addresses to their recent user + -- + SELECT framedipaddress INTO r_address + FROM dhcpippool + WHERE pool_name = v_pool_name + AND expiry_time < NOW() + AND `status` = 'dynamic' + -- + -- WHERE ... GET_LOCK(...,0) = 1 is a poor man's SKIP LOCKED that simulates + -- a row-level lock using a "user lock" that allows the locked "rows" to be + -- skipped. After the user lock is acquired and the SELECT retired it does + -- not mean that the entirety of the WHERE clause is still true: Another + -- thread may have updated the expiry time and released the lock after we + -- checked the expiry_time but before we acquired the lock since SQL is free + -- to reorder the WHERE condition. Therefore we must recheck the condition + -- in the UPDATE statement below to detect this race. + -- + AND GET_LOCK(CONCAT('dhcpippool_', framedipaddress), 0) = 1 + LIMIT 1; + + IF r_address IS NULL THEN + DO RELEASE_LOCK(CONCAT('dhcpippool_', r_address)); + LEAVE proc; + END IF; + + UPDATE dhcpippool + SET + gateway = v_gateway, + pool_key = v_pool_key, + expiry_time = NOW() + INTERVAL v_lease_duration SECOND + WHERE + framedipaddress = r_address + -- + -- Here we re-evaluate the original condition for selecting the address + -- to detect a race, in which case we try again... + -- + AND expiry_time<NOW(); + + UNTIL ROW_COUNT() <> 0 END REPEAT; + + DO RELEASE_LOCK(CONCAT('dhcpippool_', r_address)); + SELECT r_address; + +END$$ + +DELIMITER ; diff --git a/raddb/mods-config/sql/ippool-dhcp/mysql/procedure.sql b/raddb/mods-config/sql/ippool-dhcp/mysql/procedure.sql new file mode 100644 index 0000000..b5dfae0 --- /dev/null +++ b/raddb/mods-config/sql/ippool-dhcp/mysql/procedure.sql @@ -0,0 +1,144 @@ +-- +-- A stored procedure to reallocate a user's previous address, otherwise +-- provide a free address. +-- +-- Using this SP reduces the usual set dialogue of queries to a single +-- query: +-- +-- START TRANSACTION; SELECT FOR UPDATE; UPDATE; COMMIT; -> CALL sp() +-- +-- The stored procedure is executed on an database instance within a single +-- round trip which often leads to reduced deadlocking and significant +-- performance improvements especially on multi-master clusters, perhaps even +-- by an order of magnitude or more. +-- +-- To use this stored procedure the corresponding queries.conf statements must +-- be configured as follows: +-- +-- allocate_begin = "" +-- allocate_find = "\ +-- CALL fr_dhcp_allocate_previous_or_new_framedipaddress( \ +-- '%{control:${pool_name}}', \ +-- '%{DHCP-Gateway-IP-Address}', \ +-- '${pool_key}', \ +-- ${lease_duration}, \ +-- '%{%{${req_attribute_name}}:-0.0.0.0}' \ +-- )" +-- allocate_update = "" +-- allocate_commit = "" +-- + +DELIMITER $$ + +DROP PROCEDURE IF EXISTS fr_dhcp_allocate_previous_or_new_framedipaddress; +CREATE PROCEDURE fr_dhcp_allocate_previous_or_new_framedipaddress ( + IN v_pool_name VARCHAR(30), + IN v_gateway VARCHAR(15), + IN v_pool_key VARCHAR(30), + IN v_lease_duration INT, + IN v_requested_address VARCHAR(15) +) +SQL SECURITY INVOKER +proc:BEGIN + DECLARE r_address VARCHAR(15); + + DECLARE EXIT HANDLER FOR SQLEXCEPTION + BEGIN + ROLLBACK; + RESIGNAL; + END; + + SET TRANSACTION ISOLATION LEVEL READ COMMITTED; + + START TRANSACTION; + + -- Reissue an existing IP address lease when re-authenticating a session + -- + SELECT framedipaddress INTO r_address + FROM dhcpippool + WHERE pool_name = v_pool_name + AND expiry_time > NOW() + AND pool_key = v_pool_key + AND `status` IN ('dynamic', 'static') + LIMIT 1 + FOR UPDATE; +-- FOR UPDATE SKIP LOCKED; -- Better performance, but limited support + + -- NOTE: You should enable SKIP LOCKED here (as well as any other + -- instances) if your database server supports it. If it is not + -- supported and you are not running a multi-master cluster (e.g. + -- Galera or MaxScale) then you should instead consider using the + -- SP in procedure-no-skip-locked.sql which will be faster and + -- less likely to result in thread starvation under highly + -- concurrent load. + + -- Reissue an user's previous IP address, provided that the lease is + -- available (i.e. enable sticky IPs) + -- + -- When using this SELECT you should delete the one above. You must also + -- set allocate_clear = "" in queries.conf to persist the associations + -- for expired leases. + -- + -- SELECT framedipaddress INTO r_address + -- FROM dhcpippool + -- WHERE pool_name = v_pool_name + -- AND pool_key = v_pool_key + -- AND `status` IN ('dynamic', 'static') + -- LIMIT 1 + -- FOR UPDATE; + -- -- FOR UPDATE SKIP LOCKED; -- Better performance, but limited support + + -- Issue the requested IP address if it is available + -- + IF r_address IS NULL AND v_requested_address <> '0.0.0.0' THEN + SELECT framedipaddress INTO r_address + FROM dhcpippool + WHERE pool_name = v_pool_name + AND framedipaddress = v_requested_address + AND `status` = 'dynamic' + AND ( pool_key = v_pool_key OR expiry_time < NOW() ) + FOR UPDATE; +-- FOR UPDATE SKIP LOCKED; -- Better performance, but limited support + END IF; + + -- If we didn't reallocate a previous address then pick the least + -- recently used address from the pool which maximises the likelihood + -- of re-assigning the other addresses to their recent user + -- + IF r_address IS NULL THEN + SELECT framedipaddress INTO r_address + FROM dhcpippool + WHERE pool_name = v_pool_name + AND expiry_time < NOW() + AND `status` = 'dynamic' + ORDER BY + expiry_time + LIMIT 1 + FOR UPDATE; +-- FOR UPDATE SKIP LOCKED; -- Better performance, but limited support + END IF; + + -- Return nothing if we failed to allocated an address + -- + IF r_address IS NULL THEN + COMMIT; + LEAVE proc; + END IF; + + -- Update the pool having allocated an IP address + -- + UPDATE dhcpippool + SET + gateway = v_gateway, + pool_key = v_pool_key, + expiry_time = NOW() + INTERVAL v_lease_duration SECOND + WHERE framedipaddress = r_address; + + COMMIT; + + -- Return the address that we allocated + SELECT r_address; + +END$$ + +DELIMITER ; diff --git a/raddb/mods-config/sql/ippool-dhcp/mysql/queries.conf b/raddb/mods-config/sql/ippool-dhcp/mysql/queries.conf new file mode 100644 index 0000000..6aaecb1 --- /dev/null +++ b/raddb/mods-config/sql/ippool-dhcp/mysql/queries.conf @@ -0,0 +1,221 @@ +# -*- text -*- +# +# ippool-dhcp/mysql/queries.conf -- MySQL queries for rlm_sqlippool +# +# $Id$ + +# ***************** +# * DHCP DISCOVER * +# ***************** + +# +# This series of queries allocates an IP address + +# If using MySQL < 8.0.1 then remove SKIP LOCKED +# +# Attempt to find the most recent existing IP address for the client +# +allocate_existing = "\ + SELECT framedipaddress FROM ${ippool_table} \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND pool_key = '${pool_key}' \ + AND `status` IN ('dynamic', 'static') \ + ORDER BY expiry_time DESC LIMIT 1 FOR UPDATE SKIP LOCKED" + +# +# Determine whether the requested IP address is available +# +allocate_requested = "\ + SELECT framedipaddress FROM ${ippool_table} \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND framedipaddress = '%{%{${req_attribute_name}}:-0.0.0.0}' \ + AND `status` = 'dynamic' \ + AND expiry_time < NOW() \ + FOR UPDATE SKIP LOCKED" + +# +# If the existing address can't be found this query will be run to +# find a free address +# +allocate_find = "\ + SELECT framedipaddress FROM ${ippool_table} \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND expiry_time < NOW() \ + AND `status` = 'dynamic' \ + ORDER BY expiry_time LIMIT 1 FOR UPDATE SKIP LOCKED" + +# +# The ORDER BY clause of this query tries to allocate the same IP-address +# which the user last had. Ensure that pool_key is unique to the user +# within a given pool. +# + +# +# Alternatively do the operations in one query. Depending on transaction +# isolation mode, this can cause deadlocks +# +#allocate_find = "\ +# (SELECT framedipaddress, 1 AS o FROM ${ippool_table} \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND pool_key = '${pool_key}' \ +# AND `status` IN ('dynamic', 'static') \ +# ORDER BY expiry_time DESC LIMIT 1 FOR UPDATE SKIP LOCKED \ +# ) UNION ( \ +# SELECT framedipaddress, 2 AS o FROM ${ippool_table} \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND framedipaddress = '%{%{${req_attribute_name}}:-0.0.0.0}' \ +# AND `status` = 'dynamic' \ +# AND ( pool_key = '${pool_key}' OR expiry_time < NOW() ) \ +# FOR UPDATE SKIP LOCKED \ +# ) UNION ( \ +# SELECT framedipaddress, 3 AS o FROM ${ippool_table} \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND expiry_time < NOW() \ +# AND `status` = 'dynamic' \ +# ORDER BY expiry_time LIMIT 1 FOR UPDATE SKIP LOCKED \ +# ) ORDER BY o \ +# LIMIT 1" + +# +# If you prefer to allocate a random IP address every time, use this query instead. +# Note: This is very slow if you have a lot of free IPs. +# +#allocate_find = "\ +# SELECT framedipaddress FROM ${ippool_table} \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND expiry_time < NOW() \ +# AND `status` = 'dynamic' \ +# ORDER BY \ +# RAND() \ +# LIMIT 1 \ +# FOR UPDATE" + +# +# The above query again, but with SKIP LOCKED. This requires MySQL >= 8.0.1, +# and InnoDB. +# +#allocate_find = "\ +# SELECT framedipaddress FROM ${ippool_table} \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND expiry_time < NOW() \ +# AND `status` = 'dynamic' \ +# ORDER BY \ +# RAND() \ +# LIMIT 1 \ +# FOR UPDATE SKIP LOCKED" + +# +# If an IP could not be allocated, check to see if the pool exists or not +# This allows the module to differentiate between a full pool and no pool +# Note: If you are not running redundant pool modules this query may be +# commented out to save running this query every time an ip is not allocated. +# +pool_check = "\ + SELECT id \ + FROM ${ippool_table} \ + WHERE pool_name='%{control:${pool_name}}' \ + LIMIT 1" + +# +# This is the final IP Allocation query, which saves the allocated ip details. +# +allocate_update = "\ + UPDATE ${ippool_table} \ + SET \ + gateway = '%{DHCP-Gateway-IP-Address}', pool_key = '${pool_key}', \ + expiry_time = NOW() + INTERVAL ${offer_duration} SECOND \ + WHERE framedipaddress = '%I'" + +# +# Use a stored procedure to find AND allocate the address. Read and customise +# `procedure.sql` in this directory to determine the optimal configuration. +# +#allocate_begin = "" +#allocate_find = "\ +# CALL fr_dhcp_allocate_previous_or_new_framedipaddress( \ +# '%{control:${pool_name}}', \ +# '%{DHCP-Gateway-IP-Address}', \ +# '${pool_key}', \ +# ${offer_duration}, \ +# '%{%{${req_attribute_name}}:-0.0.0.0}' \ +# )" +#allocate_update = "" +#allocate_commit = "" + + +# **************** +# * DHCP REQUEST * +# **************** + +# +# This query revokes any active offers for addresses that a client is not +# requesting when a DHCP REQUEST packet arrives +# +start_update = "\ + UPDATE ${ippool_table} \ + SET \ + gateway = '', \ + pool_key = '', \ + expiry_time = NOW() \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND pool_key = '${pool_key}' \ + AND framedipaddress <> '%{DHCP-Requested-IP-Address}' \ + AND expiry_time > NOW() \ + AND `status` = 'dynamic'" + +# +# This query extends an existing lease (or offer) when a DHCP REQUEST packet +# arrives. This query must update a row when a lease is succesfully requested +# - queries that update no rows will result in a "notfound" response to +# the module which by default will give a DHCP-NAK reply. In this example +# incrementing "counter" is used to achieve this. +# +alive_update = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = NOW() + INTERVAL ${lease_duration} SECOND, \ + counter = counter + 1 \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND pool_key = '${pool_key}' \ + AND framedipaddress = '%{%{DHCP-Requested-IP-Address}:-%{DHCP-Client-IP-Address}}'" + + +# **************** +# * DHCP RELEASE * +# **************** + +# +# This query frees an IP address when a DHCP RELEASE packet arrives +# +stop_clear = "\ + UPDATE ${ippool_table} \ + SET \ + gateway = '', \ + pool_key = '', \ + expiry_time = NOW() \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND pool_key = '${pool_key}' \ + AND framedipaddress = '%{DHCP-Client-IP-Address}' \ + AND `status` = 'dynamic'" + + +# +# This query is not applicable to DHCP +# +on_clear = "" + + +# **************** +# * DHCP DECLINE * +# **************** + +# +# This query marks an IP address as declined when a DHCP Decline +# packet arrives +# +off_clear = "\ + UPDATE ${ippool_table} \ + SET status = 'declined' \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND pool_key = '${pool_key}' \ + AND framedipaddress = '%{DHCP-Requested-IP-Address}'" diff --git a/raddb/mods-config/sql/ippool-dhcp/mysql/schema.sql b/raddb/mods-config/sql/ippool-dhcp/mysql/schema.sql new file mode 100644 index 0000000..d8b1219 --- /dev/null +++ b/raddb/mods-config/sql/ippool-dhcp/mysql/schema.sql @@ -0,0 +1,21 @@ +-- +-- Table structure for table 'dhcpippool' +-- +-- See also "procedure.sql" in this directory for a stored procedure +-- that is much faster. +-- + +CREATE TABLE dhcpippool ( + id int unsigned NOT NULL auto_increment, + pool_name varchar(30) NOT NULL, + framedipaddress varchar(15) NOT NULL default '', + pool_key varchar(30) NOT NULL default '', + gateway varchar(15) NOT NULL default '', + expiry_time DATETIME NOT NULL default NOW(), + `status` ENUM('dynamic', 'static', 'declined', 'disabled') DEFAULT 'dynamic', + counter int unsigned NOT NULL default 0, + PRIMARY KEY (id), + KEY dhcpippool_poolname_expire (pool_name, expiry_time), + KEY framedipaddress (framedipaddress), + KEY dhcpippool_poolname_poolkey_ipaddress (pool_name, pool_key, framedipaddress) +) ENGINE=InnoDB; diff --git a/raddb/mods-config/sql/ippool-dhcp/oracle/procedure.sql b/raddb/mods-config/sql/ippool-dhcp/oracle/procedure.sql new file mode 100644 index 0000000..84b4596 --- /dev/null +++ b/raddb/mods-config/sql/ippool-dhcp/oracle/procedure.sql @@ -0,0 +1,217 @@ +-- +-- A stored procedure to reallocate a user's previous address, otherwise +-- provide a free address. +-- +-- Using this SP reduces the usual set dialogue of queries to a single +-- query: +-- +-- BEGIN; SELECT FOR UPDATE; UPDATE; COMMIT; -> SELECT sp() FROM dual +-- +-- The stored procedure is executed on an database instance within a single +-- round trip which often leads to reduced deadlocking and significant +-- performance improvements especially on multi-master clusters, perhaps even +-- by an order of magnitude or more. +-- +-- To use this stored procedure the corresponding queries.conf statements must +-- be configured as follows: +-- +-- allocate_begin = "" +-- allocate_find = "\ +-- SELECT fr_dhcp_allocate_previous_or_new_framedipaddress( \ +-- '%{control:${pool_name}}', \ +-- '%{DHCP-Gateway-IP-Address}', \ +-- '${pool_key}', \ +-- ${lease_duration}, \ +-- '%{%{${req_attribute_name}}:-0.0.0.0}' \ +-- ) FROM dual" +-- allocate_update = "" +-- allocate_commit = "" +-- + +CREATE OR REPLACE FUNCTION fr_dhcp_allocate_previous_or_new_framedipaddress ( + v_pool_name IN VARCHAR2, + v_gateway IN VARCHAR2, + v_pool_key IN VARCHAR2, + v_lease_duration IN INTEGER, + v_requested_address IN VARCHAR2 +) +RETURN varchar2 IS + PRAGMA AUTONOMOUS_TRANSACTION; + r_address varchar2(15); +BEGIN + + -- Reissue an existing IP address lease when re-authenticating a session + -- + BEGIN + SELECT framedipaddress INTO r_address FROM dhcpippool WHERE id IN ( + SELECT id FROM ( + SELECT * + FROM dhcpippool + JOIN dhcpstatus + ON dhcpstatus.status_id = dhcpippool.status_id + WHERE pool_name = v_pool_name + AND expiry_time > current_timestamp + AND pool_key = v_pool_key + AND dhcpstatus.status IN ('dynamic', 'static') + ) WHERE ROWNUM <= 1 + ) FOR UPDATE SKIP LOCKED; + EXCEPTION + WHEN NO_DATA_FOUND THEN + r_address := NULL; + END; + + -- Oracle >= 12c version of the above query + -- + -- BEGIN + -- SELECT framedipaddress INTO r_address FROM dhcpippool WHERE id IN ( + -- SELECT id FROM dhcpippool + -- JOIN dhcpstatus + -- ON dhcpstatus.status_id = dhcpippool.status_id + -- WHERE pool_name = v_pool_name + -- AND expiry_time > current_timestamp + -- AND pool_key = v_pool_key + -- AND dhcpstatus.status IN ('dynamic', 'static') + -- FETCH FIRST 1 ROWS ONLY + -- ) FOR UPDATE SKIP LOCKED; + -- EXCEPTION + -- WHEN NO_DATA_FOUND THEN + -- r_address := NULL; + -- END; + + + + -- Reissue an user's previous IP address, provided that the lease is + -- available (i.e. enable sticky IPs) + -- + -- When using this SELECT you should delete the one above. You must also + -- set allocate_clear = "" in queries.conf to persist the associations + -- for expired leases. + -- + -- BEGIN + -- SELECT framedipaddress INTO r_address FROM dhcpippool WHERE id IN ( + -- SELECT id FROM ( + -- SELECT * + -- FROM dhcpippool + -- JOIN dhcpstatus + -- ON dhcpstatus.status_id = dhcpippool.status_id + -- WHERE pool_name = v_pool_name + -- AND pool_key = v_pool_key + -- AND dhcpstatus.status IN ('dynamic', 'static') + -- ) WHERE ROWNUM <= 1 + -- ) FOR UPDATE SKIP LOCKED; + -- EXCEPTION + -- WHEN NO_DATA_FOUND THEN + -- r_address := NULL; + -- END; + + -- Oracle >= 12c version of the above query + -- + -- BEGIN + -- SELECT framedipaddress INTO r_address FROM dhcpippool WHERE id IN ( + -- SELECT id FROM dhcpippool + -- JOIN dhcpstatus + -- ON dhcpstatus.status_id = dhcpippool.status_id + -- WHERE pool_name = v_pool_name + -- AND pool_key = v_pool_key + -- AND dhcpstatus.status IN ('dynamic', 'static') + -- FETCH FIRST 1 ROWS ONLY + -- ) FOR UPDATE SKIP LOCKED; + -- EXCEPTION + -- WHEN NO_DATA_FOUND THEN + -- r_address := NULL; + -- END; + + + + -- Issue the requested IP address if it is available + -- + IF r_address IS NULL AND v_requested_address <> '0.0.0.0' THEN + BEGIN + SELECT framedipaddress INTO r_address FROM dhcpippool WHERE id IN ( + SELECT id FROM ( + SELECT * + FROM dhcpippool + JOIN dhcpstatus + ON dhcpstatus.status_id = dhcpippool.status_id + WHERE pool_name = v_pool_name + AND framedipaddress = v_requested_address + AND dhcpstatus.status = 'dynamic' + AND expiry_time < CURRENT_TIMESTAMP + ) WHERE ROWNUM <= 1 + ) FOR UPDATE SKIP LOCKED; + EXCEPTION + WHEN NO_DATA_FOUND THEN + r_address := NULL; + END; + END IF; + + -- Oracle >= 12c version of the above query + -- + -- IF r_address IS NULL AND v_requested_address <> '0.0.0.0' THEN + -- BEGIN + -- SELECT framedipaddress INTO r_address FROM dhcpippool WHERE id IN ( + -- SELECT id FROM dhcpippool + -- JOIN dhcpstatus + -- ON dhcpstatus.status_id = dhcpippool.status_id + -- WHERE pool_name = v_pool_name + -- AND framedipaddress = v_requested_address + -- AND dhcpstatus.status = 'dynamic' + -- AND expiry_time < CURRENT_TIMESTAMP + -- FETCH FIRST 1 ROWS ONLY + -- ) FOR UPDATE SKIP LOCKED; + -- EXCEPTION + -- WHEN NO_DATA_FOUND THEN + -- r_address := NULL; + -- END; + -- END IF; + + + + -- If we didn't reallocate a previous address then pick the least + -- recently used address from the pool which maximises the likelihood + -- of re-assigning the other addresses to their recent user + -- + IF r_address IS NULL THEN + DECLARE + l_cursor sys_refcursor; + BEGIN + OPEN l_cursor FOR + SELECT framedipaddress + FROM dhcpippool + JOIN dhcpstatus + ON dhcpstatus.status_id = dhcpippool.status_id + WHERE pool_name = v_pool_name + AND expiry_time < CURRENT_TIMESTAMP + AND dhcpstatus.status = 'dynamic' + ORDER BY expiry_time + FOR UPDATE SKIP LOCKED; + FETCH l_cursor INTO r_address; + CLOSE l_cursor; + EXCEPTION + WHEN NO_DATA_FOUND THEN + r_address := NULL; + END; + END IF; + + -- Return nothing if we failed to allocated an address + -- + IF r_address IS NULL THEN + COMMIT; + RETURN r_address; + END IF; + + -- Update the pool having allocated an IP address + -- + UPDATE dhcpippool + SET + gateway = v_gateway, + pool_key = v_pool_key, + expiry_time = CURRENT_TIMESTAMP + v_lease_duration * INTERVAL '1' SECOND(1) + WHERE framedipaddress = r_address; + + -- Return the address that we allocated + COMMIT; + RETURN r_address; + +END; + diff --git a/raddb/mods-config/sql/ippool-dhcp/oracle/queries.conf b/raddb/mods-config/sql/ippool-dhcp/oracle/queries.conf new file mode 100644 index 0000000..0fcffc3 --- /dev/null +++ b/raddb/mods-config/sql/ippool-dhcp/oracle/queries.conf @@ -0,0 +1,200 @@ +# -*- text -*- +# +# ippool-dhcp/oracle/queries.conf -- Oracle queries for rlm_sqlippool +# +# $Id$ + +start_begin = "commit" +alive_begin = "commit" +stop_begin = "commit" +on_begin = "commit" +off_begin = "commit" + + +# ***************** +# * DHCP DISCOVER * +# ***************** + +# +# Use a stored procedure to find AND allocate the address. Read and customise +# `procedure.sql` in this directory to determine the optimal configuration. +# Oracle's locking mechanism limitations prevents the use of single queries +# that can either find a client's existing address or the first available one. +# +allocate_begin = "" +allocate_find = "\ + SELECT fr_dhcp_allocate_previous_or_new_framedipaddress( \ + '%{control:${pool_name}}', \ + '%{DHCP-Gateway-IP-Address}', \ + '${pool_key}', \ + '${offer_duration}', \ + '%{%{${req_attribute_name}}:-0.0.0.0}' \ + ) FROM dual" +allocate_update = "" +allocate_commit = "" + + +# +# If you prefer to allocate a random IP address every time, use this query instead +# Note: This is very slow if you have a lot of free IPs. +# +#allocate_find = "\ +# SELECT framedipaddress FROM ${ippool_table} WHERE id IN ( \ +# SELECT id FROM ( \ +# SELECT * \ +# FROM ${ippool_table} \ +# JOIN dhcpstatus \ +# ON ${ippool_table}.status_id = dhcpstatus.status_id \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND expiry_time < current_timestamp \ +# AND dhcpstatus.status = 'dynamic' \ +# ORDER BY DBMS_RANDOM.VALUE \ +# ) WHERE ROWNUM <= 1 \ +# ) FOR UPDATE" + +# +# The above query again, but with SKIP LOCKED. This requires Oracle > 11g. +# It may work in 9i and 10g, but is not documented, so YMMV. +# +#allocate_find = "\ +# SELECT framedipaddress FROM ${ippool_table} WHERE id IN ( \ +# SELECT id FROM (\ +# SELECT * \ +# FROM ${ippool_table} \ +# JOIN dhcpstatus \ +# ON ${ippool_table}.status_id = dhcpstatus.status_id \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND expiry_time < current_timestamp \ +# AND dhcpstatus.status = 'dynamic' \ +# ORDER BY DBMS_RANDOM.VALUE \ +# ) WHERE ROWNUM <= 1 \ +# ) FOR UPDATE SKIP LOCKED" + +# +# A tidier version that needs Oracle >= 12c +# +#allocate_find = "\ +# SELECT framedipaddress FROM ${ippool_table} WHERE id IN ( +# SELECT id FROM ${ippool_table} \ +# JOIN dhcpstatus \ +# ON ${ippool_table}.status_id = dhcpstatus.status_id \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND expiry_time < current_timestamp \ +# AND dhcpstatus.status = 'dynamic' \ +# ORDER BY DBMS_RANDOM.VALUE \ +# FETCH FIRST 1 ROWS ONLY +# ) FOR UPDATE SKIP LOCKED" + +# +# If an IP could not be allocated, check to see whether the pool exists or not +# This allows the module to differentiate between a full pool and no pool +# Note: If you are not running redundant pool modules this query may be commented +# out to save running this query every time an ip is not allocated. +# +pool_check = "\ + SELECT id \ + FROM (\ + SELECT id \ + FROM ${ippool_table} \ + WHERE pool_name='%{control:${pool_name}}'\ + ) \ + WHERE ROWNUM = 1" + +# +# This query marks the IP address handed out by "allocate-find" as used +# for the period of "offer_duration" after which time it may be reused. +# Only needed if allocate_find is not using the stored procedure and therefore +# not updating the lease +# +#allocate_update = "\ +# UPDATE ${ippool_table} \ +# SET \ +# gateway = '%{DHCP-Gateway-IP-Address}', \ +# pool_key = '${pool_key}', \ +# expiry_time = current_timestamp + INTERVAL '${offer_duration}' second(1) \ +# WHERE framedipaddress = '%I'" + + +# **************** +# * DHCP REQUEST * +# **************** + +# +# This query revokes any active offers for addresses that a client is not +# requesting when a DHCP REQUEST packet arrives +# +start_update = "\ + UPDATE ${ippool_table} \ + SET \ + gateway = '', \ + pool_key = '0', \ + expiry_time = current_timestamp - INTERVAL '1' second(1) \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND pool_key = '${pool_key}' \ + AND framedipaddress <> '%{DHCP-Requested-IP-Address}' \ + AND expiry_time > current_timestamp \ + AND ${ippool_table}.status_id IN \ + (SELECT status_id FROM dhcpstatus WHERE status = 'dynamic')" +start_commit = "COMMIT" + +# +# This query extends an existing lease (or offer) when a DHCP REQUEST packet +# arrives. This query must update a row when a lease is succesfully requested +# - queries that update no rows will result in a "notfound" response to +# the module which by default will give a DHCP-NAK reply. In this example +# incrementing "counter" is used to achieve this. +# +alive_update = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = current_timestamp + INTERVAL '${lease_duration}' second(1), \ + counter = counter + 1 \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND pool_key = '${pool_key}' \ + AND framedipaddress = '%{%{DHCP-Requested-IP-Address}:-%{DHCP-Client-IP-Address}}'" +alive_commit = "COMMIT" + + +# **************** +# * DHCP RELEASE * +# **************** + +# +# This query frees an IP address when a DHCP RELEASE packet arrives +# +stop_clear = "\ + UPDATE ${ippool_table} \ + SET \ + gateway = '', \ + pool_key = '0', \ + expiry_time = current_timestamp - INTERVAL '1' second(1) \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND pool_key = '${pool_key}' \ + AND framedipaddress = '%{DHCP-Client-IP-Address}' \ + AND ${ippool_table}.status_id IN \ + (SELECT status_id FROM dhcpstatus WHERE status = 'dynamic')" +stop_commit = "COMMIT" + + +# +# This query is not applicable to DHCP +# +on_clear = "" + + +# **************** +# * DHCP DECLINE * +# **************** + +# +# This query marks an IP address as declined when a DHCP Decline +# packet arrives +# +off_clear = "\ + UPDATE ${ippool_table} \ + SET status_id = (SELECT status_id FROM dhcpstatus WHERE status = 'declined') \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND pool_key = '${pool_key}' \ + AND framedipaddress = '%{DHCP-Requested-IP-Address}'" +off_commit = "COMMIT" + diff --git a/raddb/mods-config/sql/ippool-dhcp/oracle/schema.sql b/raddb/mods-config/sql/ippool-dhcp/oracle/schema.sql new file mode 100644 index 0000000..32d28bb --- /dev/null +++ b/raddb/mods-config/sql/ippool-dhcp/oracle/schema.sql @@ -0,0 +1,28 @@ +CREATE TABLE dhcpstatus ( + status_id INT PRIMARY KEY, + status VARCHAR(10) NOT NULL +); + +INSERT INTO dhcpstatus (status_id, status) VALUES (1, 'dynamic'); +INSERT INTO dhcpstatus (status_id, status) VALUES (2, 'static'); +INSERT INTO dhcpstatus (status_id, status) VALUES (3, 'declined'); +INSERT INTO dhcpstatus (status_id, status) VALUES (4, 'disabled'); + +CREATE SEQUENCE dhcpippool_seq START WITH 1 INCREMENT BY 1; + +CREATE TABLE dhcpippool ( + id INT DEFAULT ON NULL dhcpippool_seq.NEXTVAL PRIMARY KEY, + pool_name VARCHAR(30) NOT NULL, + framedipaddress VARCHAR(15) NOT NULL, + pool_key VARCHAR(30) DEFAULT '', + gateway VARCHAR(15) DEFAULT '', + expiry_time timestamp(0) DEFAULT CURRENT_TIMESTAMP, + status_id INT DEFAULT 1, + counter INT DEFAULT 0, + FOREIGN KEY (status_id) REFERENCES dhcpstatus(status_id) +); + +CREATE INDEX dhcpippool_poolname_expire ON dhcpippool (pool_name, expiry_time); +CREATE INDEX dhcpippool_framedipaddress ON dhcpippool (framedipaddress); +CREATE INDEX dhcpippool_poolname_poolkey_ipaddress ON dhcpippool (pool_name, pool_key, framedipaddress); + diff --git a/raddb/mods-config/sql/ippool-dhcp/postgresql/procedure.sql b/raddb/mods-config/sql/ippool-dhcp/postgresql/procedure.sql new file mode 100644 index 0000000..379a349 --- /dev/null +++ b/raddb/mods-config/sql/ippool-dhcp/postgresql/procedure.sql @@ -0,0 +1,119 @@ +-- +-- A stored procedure to reallocate a user's previous address, otherwise +-- provide a free address. +-- +-- Using this SP reduces the usual set dialogue of queries to a single +-- query: +-- +-- START TRANSACTION; SELECT FOR UPDATE; UPDATE; COMMIT; -> SELECT sp() +-- +-- The stored procedure is executed on an database instance within a single +-- round trip which often leads to reduced deadlocking and significant +-- performance improvements especially on multi-master clusters, perhaps even +-- by an order of magnitude or more. +-- +-- To use this stored procedure the corresponding queries.conf statements must +-- be configured as follows: +-- +-- allocate_begin = "" +-- allocate_find = "\ +-- SELECT fr_dhcp_allocate_previous_or_new_framedipaddress( \ +-- '%{control:${pool_name}}', \ +-- '%{DHCP-Gateway-IP-Address}', \ +-- '${pool_key}', \ +-- ${lease_duration}, \ +-- '%{%{${req_attribute_name}}:-0.0.0.0}' \ +-- )" +-- allocate_update = "" +-- allocate_commit = "" +-- + +CREATE OR REPLACE FUNCTION fr_dhcp_allocate_previous_or_new_framedipaddress ( + v_pool_name VARCHAR(64), + v_gateway VARCHAR(16), + v_pool_key VARCHAR(64), + v_lease_duration INT, + v_requested_address INET +) +RETURNS inet +LANGUAGE plpgsql +AS $$ +DECLARE + r_address INET; +BEGIN + + -- Reissue an existing IP address lease when re-authenticating a session + -- + WITH ips AS ( + SELECT framedipaddress FROM dhcpippool + WHERE pool_name = v_pool_name + AND pool_key = v_pool_key + AND expiry_time > NOW() + AND status IN ('dynamic', 'static') + LIMIT 1 FOR UPDATE SKIP LOCKED ) + UPDATE dhcpippool + SET expiry_time = NOW() + v_lease_duration * interval '1 sec' + FROM ips WHERE dhcpippool.framedipaddress = ips.framedipaddress + RETURNING dhcpippool.framedipaddress INTO r_address; + + -- Reissue an user's previous IP address, provided that the lease is + -- available (i.e. enable sticky IPs) + -- + -- When using this SELECT you should delete the one above. You must also + -- set allocate_clear = "" in queries.conf to persist the associations + -- for expired leases. + -- + -- WITH ips AS ( + -- SELECT framedipaddress FROM dhcpippool + -- WHERE pool_name = v_pool_name + -- AND pool_key = v_pool_key + -- AND status IN ('dynamic', 'static') + -- LIMIT 1 FOR UPDATE SKIP LOCKED ) + -- UPDATE dhcpippool + -- SET expiry_time = NOW + v_lease_duration * interval '1 sec' + -- FROM ips WHERE dhcpippool.framedipaddress = ips.framedipaddress + -- RETURNING dhcpippool.framedipaddress INTO r_address; + + -- Issue the requested IP address if it is available + -- + IF r_address IS NULL AND v_requested_address != '0.0.0.0' THEN + WITH ips AS ( + SELECT framedipaddress FROM dhcpippool + WHERE pool_name = v_pool_name + AND framedipaddress = v_requested_address + AND status = 'dynamic' + AND ( pool_key = v_pool_key OR expiry_time < NOW() ) + LIMIT 1 FOR UPDATE SKIP LOCKED ) + UPDATE dhcpippool + SET pool_key = v_pool_key, + expiry_time = NOW() + v_lease_duration * interval '1 sec', + gateway = v_gateway + FROM ips WHERE dhcpippool.framedipaddress = ips.framedipaddress + RETURNING dhcpippool.framedipaddress INTO r_address; + END IF; + + -- If we didn't reallocate a previous address then pick the least + -- recently used address from the pool which maximises the likelihood + -- of re-assigning the other addresses to their recent user + -- + IF r_address IS NULL THEN + WITH ips AS ( + SELECT framedipaddress FROM dhcpippool + WHERE pool_name = v_pool_name + AND expiry_time < NOW() + AND status = 'dynamic' + ORDER BY expiry_time + LIMIT 1 FOR UPDATE SKIP LOCKED ) + UPDATE dhcpippool + SET pool_key = v_pool_key, + expiry_time = NOW() + v_lease_duration * interval '1 sec', + gateway = v_gateway + FROM ips WHERE dhcpippool.framedipaddress = ips.framedipaddress + RETURNING dhcpippool.framedipaddress INTO r_address; + END IF; + + -- Return the address that we allocated + RETURN r_address; + +END +$$; diff --git a/raddb/mods-config/sql/ippool-dhcp/postgresql/queries.conf b/raddb/mods-config/sql/ippool-dhcp/postgresql/queries.conf new file mode 100644 index 0000000..632fc70 --- /dev/null +++ b/raddb/mods-config/sql/ippool-dhcp/postgresql/queries.conf @@ -0,0 +1,291 @@ +# -*- text -*- +# +# ippool-dhcp/postgresql/queries.conf -- PostgreSQL queries for rlm_sqlippool +# +# $Id$ + +# ***************** +# * DHCP DISCOVER * +# ***************** + +# +# Use a stored procedure to find AND allocate the address. Read and customise +# `procedure.sql` in this directory to determine the optimal configuration. +# +# This requires PostgreSQL >= 9.5 as SKIP LOCKED is used. +# +# The "NO LOAD BALANCE" comment is included here to indicate to a PgPool +# system that this needs to be a write transaction. PgPool itself cannot +# detect this from the statement alone. If you are using PgPool and do not +# have this comment, the query may go to a read only server, and will fail. +# This has no negative effect if you are not using PgPool. +# +allocate_begin = "" +allocate_find = "\ + /*NO LOAD BALANCE*/ \ + SELECT fr_dhcp_allocate_previous_or_new_framedipaddress( \ + '%{control:${pool_name}}', \ + '%{DHCP-Gateway-IP-Address}', \ + '${pool_key}', \ + '${offer_duration}', \ + '%{%{${req_attribute_name}}:-0.0.0.0}' \ + )" +allocate_update = "" +allocate_commit = "" + +# +# If stored procedures are not able to be used, the following queries can +# be used. +# Comment out all the above queries and choose the appropriate "allocate_find" +# to match the desired outcome and also the version of "allocate_update" below. +# + +# +# This sequence of queries allocates an IP address from the Pool +# +#allocate_begin = "BEGIN" + + +# Attempt to find the most recent existing IP address for the client +# +#allocate_existing = "\ +# SELECT framedipaddress FROM ${ippool_table} \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND pool_key = '${pool_key}' \ +# AND status IN ('dynamic', 'static') \ +# ORDER BY expiry_time DESC \ +# LIMIT 1 \ +# FOR UPDATE" + +# The same query with SKIP LOCKED - requires PostgreSQL >= 9.5 +# allocate_existing = "\ +# SELECT framedipaddress FROM ${ippool_table} \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND pool_key = '${pool_key}' \ +# AND status IN ('dynamic', 'static') \ +# ORDER BY expiry_time DESC \ +# LIMIT 1 \ +# FOR UPDATE SKIP LOCKED" + + +# +# Determine whether the requested IP address is available +# +#allocate_requested = "\ +# SELECT framedipaddress FROM ${ippool_table} \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND framedipaddress = '%{%{${req_attribute_name}}:-0.0.0.0}' \ +# AND status = 'dynamic' \ +# AND expiry_time < 'now'::timestamp(0) \ +# FOR UPDATE" + +# The same query with SKIP LOCKED - requires PostgreSQL >= 9.5 +#allocate_requested = "\ +# SELECT framedipaddress FROM ${ippool_table} \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND framedipaddress = '%{%{${req_attribute_name}}:-0.0.0.0}' \ +# AND status = 'dynamic' \ +# AND expiry_time < 'now'::timestamp(0) \ +# FOR UPDATE SKIP LOCKED" + + +# +# If the existing address can't be found this query will be run to +# find a free address +# +#allocate_find = "\ +# SELECT framedipaddress FROM ${ippool_table} \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND expiry_time < 'now'::timestamp(0) \ +# AND status = 'dynamic' \ +# ORDER BY expiry_time \ +# LIMIT 1 \ +# FOR UPDATE" + +# The same query with SKIP LOCKED - requires PostgreSQL >= 9.5 +#allocate_find = "\ +# SELECT framedipaddress FROM ${ippool_table} \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND expiry_time < 'now'::timestamp(0) \ +# AND status = 'dynamic' \ +# ORDER BY expiry_time \ +# LIMIT 1 \ +# FOR UPDATE SKIP LOCKED" + +# +# If you prefer to allocate a random IP address every time, use this query instead +# Note: This is very slow if you have a lot of free IPs. +# Use of either of these next two queries should have the allocate_begin line commented out +# and allocate_update below un-commented. +# +#allocate_find = "\ +# SELECT framedipaddress FROM ${ippool_table} \ +# WHERE pool_name = '%{control:${pool_name}}' AND expiry_time < 'now'::timestamp(0) \ +# AND status = 'dynamic' \ +# ORDER BY RANDOM() \ +# LIMIT 1 \ +# FOR UPDATE" + +# +# The above query again, but with SKIP LOCKED. This requires PostgreSQL >= 9.5. +# +#allocate_find = "\ +# SELECT framedipaddress FROM ${ippool_table} \ +# WHERE pool_name = '%{control:${pool_name}}' AND expiry_time < 'now'::timestamp(0) \ +# AND status = 'dynamic' \ +# ORDER BY RANDOM() \ +# LIMIT 1 \ +# FOR UPDATE SKIP LOCKED" + +# +# This query marks the IP address handed out by "allocate-find" as used +# for the period of "lease_duration" after which time it may be reused. +# +#allocate_update = "\ +# UPDATE ${ippool_table} \ +# SET \ +# gateway = '%{DHCP-Gateway-IP-Address}', \ +# pool_key = '${pool_key}', \ +# expiry_time = 'now'::timestamp(0) + '${offer_duration} second'::interval \ +# WHERE framedipaddress = '%I'" + + +# +# Alternatively, merge the matching of existing IP and free IP into a single query +# This version does the update as well - so allocate_begin, allocate_update and +# allocate_commit should be blank +# +#allocate_begin = "" +#allocate_find = "\ +# WITH found AS ( \ +# WITH existing AS ( \ +# SELECT framedipaddress FROM ${ippool_table} \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND pool_key = '${pool_key}' \ +# ORDER BY expiry_time DESC \ +# LIMIT 1 \ +# FOR UPDATE SKIP LOCKED \ +# ), requested AS ( \ +# SELECT framedipaddress FROM ${ippool_table} \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND framedipaddress = '%{%{${req_attribute_name}}:-0.0.0.0}' \ +# AND status = 'dynamic' \ +# AND ( pool_key = '${pool_key}' OR expiry_time < 'now'::timestamp(0) ) \ +# FOR UPDATE SKIP LOCKED \ +# ), new AS ( \ +# SELECT framedipaddress FROM ${ippool_table} \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND expiry_time < 'now'::timestamp(0) \ +# AND status = 'dynamic' \ +# ORDER BY expiry_time \ +# LIMIT 1 \ +# FOR UPDATE SKIP LOCKED \ +# ) \ +# SELECT framedipaddress, 1 AS o FROM existing \ +# UNION ALL \ +# SELECT framedipaddress, 2 AS o FROM requested \ +# UNION ALL \ +# SELECT framedipaddress, 3 AS o FROM new \ +# ORDER BY o LIMIT 1 \ +# ) \ +# UPDATE ${ippool_table} \ +# SET pool_key = '${pool_key}', \ +# expiry_time = 'now'::timestamp(0) + '${offer_duration} second'::interval, \ +# gateway = '%{DHCP-Gateway-IP-Address}' \ +# FROM found \ +# WHERE found.framedipaddress = ${ippool_table}.framedipaddress \ +# RETURNING found.framedipaddress" +#allocate_update = "" +#allocate_commit = "" + + +# +# If an IP could not be allocated, check to see whether the pool exists or not +# This allows the module to differentiate between a full pool and no pool +# Note: If you are not running redundant pool modules this query may be commented +# out to save running this query every time an ip is not allocated. +# +pool_check = "\ + SELECT id \ + FROM ${ippool_table} \ + WHERE pool_name='%{control:${pool_name}}' \ + LIMIT 1" + + +# **************** +# * DHCP REQUEST * +# **************** + +# +# This query revokes any active offers for addresses that a client is not +# requesting when a DHCP REQUEST packet arrives, i.e, each server (sharing the +# same database) may have simultaneously offered a unique address. +# +start_update = "\ + UPDATE ${ippool_table} \ + SET \ + gateway = '', \ + pool_key = '', \ + expiry_time = 'now'::timestamp(0) - '1 second'::interval \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND pool_key = '${pool_key}' \ + AND framedipaddress <> '%{DHCP-Requested-IP-Address}' \ + AND expiry_time > 'now'::timestamp(0) \ + AND status = 'dynamic'" + +# +# This query extends an existing lease (or offer) when a DHCP REQUEST packet +# arrives. This query must update a row when a lease is succesfully requested +# - queries that update no rows will result in a "notfound" response to +# the module which by default will give a DHCP-NAK reply. In this example +# incrementing "counter" is used to achieve this. +# +alive_update = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = 'now'::timestamp(0) + '${lease_duration} second'::interval, \ + counter = counter + 1 \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND pool_key = '${pool_key}' \ + AND framedipaddress = '%{%{DHCP-Requested-IP-Address}:-%{DHCP-Client-IP-Address}}'" + + +# **************** +# * DHCP RELEASE * +# **************** + +# +# This query frees an IP address when a DHCP RELEASE packet arrives +# +stop_clear = "\ + UPDATE ${ippool_table} \ + SET \ + gateway = '', \ + pool_key = '', \ + expiry_time = 'now'::timestamp(0) - '1 second'::interval \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND pool_key = '${pool_key}' \ + AND framedipaddress = '%{DHCP-Client-IP-Address}' \ + AND status = 'dynamic'" + + +# +# This query is not applicable to DHCP +# +on_clear = "" + + +# **************** +# * DHCP DECLINE * +# **************** + +# +# This query marks an IP address as declined when a DHCP DECLINE packet +# arrives +# +off_clear = "\ + UPDATE ${ippool_table} \ + SET status = 'declined' \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND pool_key = '${pool_key}' \ + AND framedipaddress = '%{DHCP-Requested-IP-Address}'" diff --git a/raddb/mods-config/sql/ippool-dhcp/postgresql/schema.sql b/raddb/mods-config/sql/ippool-dhcp/postgresql/schema.sql new file mode 100644 index 0000000..af86889 --- /dev/null +++ b/raddb/mods-config/sql/ippool-dhcp/postgresql/schema.sql @@ -0,0 +1,23 @@ +-- +-- Table structure for table 'dhcpippool' +-- +-- See also "procedure.sql" in this directory for +-- a stored procedure that gives much faster response. +-- + +CREATE TYPE dhcp_status AS ENUM ('dynamic', 'static', 'declined', 'disabled'); + +CREATE TABLE dhcpippool ( + id BIGSERIAL PRIMARY KEY, + pool_name varchar(64) NOT NULL, + FramedIPAddress INET NOT NULL, + pool_key VARCHAR(64) NOT NULL default '0', + gateway VARCHAR(16) NOT NULL default '', + expiry_time TIMESTAMP(0) without time zone NOT NULL default NOW(), + status dhcp_status DEFAULT 'dynamic', + counter INT NOT NULL default 0 +); + +CREATE INDEX dhcpippool_poolname_expire ON dhcpippool USING btree (pool_name, expiry_time); +CREATE INDEX dhcpippool_framedipaddress ON dhcpippool USING btree (framedipaddress); +CREATE INDEX dhcpippool_poolname_poolkey_ipaddress ON dhcpippool USING btree (pool_name, pool_key, framedipaddress); diff --git a/raddb/mods-config/sql/ippool-dhcp/sqlite/queries.conf b/raddb/mods-config/sql/ippool-dhcp/sqlite/queries.conf new file mode 100644 index 0000000..d99e09b --- /dev/null +++ b/raddb/mods-config/sql/ippool-dhcp/sqlite/queries.conf @@ -0,0 +1,236 @@ +# -*- text -*- +# +# ippool-dhcp/sqlite/queries.conf -- SQLite queries for rlm_sqlippool +# +# $Id$ + +# ***************** +# * DHCP DISCOVER * +# ***************** + +# +# SQLite does not implement SELECT FOR UPDATE which is normally used to place +# an exclusive lock over rows to prevent the same address from being +# concurrently selected for allocation to multiple users. +# +# The most granular read-blocking lock that SQLite has is an exclusive lock +# over the database, so that's what we use. All locking in SQLite is performed +# over the entire database and we perform a row update for any IP that we +# allocate, requiring an exclusive lock. Taking the exclusive lock from the +# start of the transaction (even if it were not required to guard the SELECT) +# is actually quicker than if we deferred it causing SQLite to "upgrade" the +# automatic shared lock for the transaction to an exclusive lock for the +# subsequent UPDATE. +# +allocate_begin = "BEGIN EXCLUSIVE" +allocate_commit = "COMMIT" + +# +# Attempt to find the most recent existing IP address for the client +# +allocate_existing = "\ + SELECT framedipaddress \ + FROM ${ippool_table} \ + JOIN dhcpstatus \ + ON ${ippool_table}.status_id = dhcpstatus.status_id \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND pool_key = '${pool_key}' \ + AND status IN ('dynamic', 'static') \ + ORDER BY expiry_time DESC \ + LIMIT 1" + +# +# Determine whether the requested IP address is available +# +allocate_requested = "\ + SELECT framedipaddress \ + FROM ${ippool_table} \ + JOIN dhcpstatus \ + ON ${ippool_table}.status_id = dhcpstatus.status_id \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND framedipaddress = '%{%{${req_attribute_name}}:-0.0.0.0}' \ + AND status = 'dynamic' \ + AND expiry_time < datetime('now')" + +# +# If the existing address can't be found this query will be run to +# find a free address +# +allocate_find = "\ + SELECT framedipaddress \ + FROM ${ippool_table} \ + JOIN dhcpstatus \ + ON ${ippool_table}.status_id = dhcpstatus.status_id \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND expiry_time < datetime('now') \ + AND status = 'dynamic' \ + ORDER BY expiry_time LIMIT 1" + +# +# This series of queries allocates an IP address +# +# Either pull the most recent allocated IP for this client or the +# oldest expired one. The first sub query returns the most recent +# lease for the client (if there is one), the second returns the +# oldest expired one. +# Sorting the result by expiry_time DESC will return the client specific +# IP if it exists, otherwise an expired one. +# +#allocate_find = "\ +# SELECT framedipaddress, 1 AS o \ +# FROM ( \ +# SELECT framedipaddress \ +# FROM ${ippool_table} \ +# JOIN dhcpstatus \ +# ON ${ippool_table}.status_id = dhcpstatus.status_id \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND pool_key = '${pool_key}' \ +# AND status IN ('dynamic', 'static') \ +# ORDER BY expiry_time DESC \ +# LIMIT 1 \ +# ) UNION \ +# SELECT framedipaddress, 2 AS o \ +# FROM ( \ +# SELECT framedipaddress \ +# FROM ${ippool_table} \ +# JOIN dhcpstatus \ +# ON ${ippool_table}.status_id = dhcpstatus.status_id \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND framedipaddress = '%{%{${req_attribute_name}}:-0.0.0.0}' \ +# AND status = 'dynamic' \ +# AND ( pool_key = '${pool_key}' OR expiry_time < datetime('now') ) \ +# ) UNION \ +# SELECT framedipaddress, 3 AS o \ +# FROM ( \ +# SELECT framedipaddress \ +# FROM ${ippool_table} \ +# JOIN dhcpstatus \ +# ON ${ippool_table}.status_id = dhcpstatus.status_id \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND expiry_time < datetime('now') \ +# AND status = 'dynamic' \ +# ORDER BY expiry_time LIMIT 1 \ +# ) \ +# ORDER BY o \ +# LIMIT 1" + +# +# If you prefer to allocate a random IP address every time, i +# use this query instead +# Note: This is very slow if you have a lot of free IPs. +# + +#allocate_find = "\ +# SELECT framedipaddress \ +# FROM ${ippool_table} \ +# JOIN dhcpstatus \ +# ON ${ippool_table}.status_id = dhcpstatus.status_id \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND expiry_time < datetime('now') \ +# AND status = 'dynamic' \ +# ORDER BY RAND() \ + + +# +# If an IP could not be allocated, check to see if the pool exists or not +# This allows the module to differentiate between a full pool and no pool +# Note: If you are not running redundant pool modules this query may be +# commented out to save running this query every time an ip is not allocated. +# +pool_check = "\ + SELECT id \ + FROM ${ippool_table} \ + WHERE pool_name='%{control:${pool_name}}' \ + LIMIT 1" + +# +# This is the final IP Allocation query, which saves the allocated ip details +# +allocate_update = "\ + UPDATE ${ippool_table} \ + SET \ + gateway = '%{DHCP-Gateway-IP-Address}', \ + pool_key = '${pool_key}', \ + expiry_time = datetime(strftime('%%s', 'now') + ${offer_duration}, 'unixepoch') \ + WHERE framedipaddress = '%I'" + + +# **************** +# * DHCP REQUEST * +# **************** + +# +# This query revokes any active offers for addresses that a client is not +# requesting when a DHCP REQUEST packet arrives +# +start_update = "\ + UPDATE ${ippool_table} \ + SET \ + gateway = '', \ + pool_key = '', \ + expiry_time = datetime('now') \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND pool_key = '${pool_key}' \ + AND framedipaddress <> '%{DHCP-Requested-IP-Address}' \ + AND expiry_time > datetime('now') \ + AND ${ippool_table}.status_id IN \ + (SELECT status_id FROM dhcpstatus WHERE status = 'dynamic')" + +# +# This query extends an existing lease (or offer) when a DHCP REQUEST packet +# arrives. This query must update a row when a lease is succesfully requested +# - queries that update no rows will result in a "notfound" response to +# the module which by default will give a DHCP-NAK reply. In this example +# incrementing "counter" is used to achieve this. +# +alive_update = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = datetime(strftime('%%s', 'now') + ${lease_duration}, 'unixepoch'), \ + counter = counter + 1 \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND pool_key = '${pool_key}' \ + AND framedipaddress = '%{%{DHCP-Requested-IP-Address}:-%{DHCP-Client-IP-Address}}'" + + +# **************** +# * DHCP RELEASE * +# **************** + +# +# This query frees an IP address when a DHCP RELEASE packet arrives +# +stop_clear = "\ + UPDATE ${ippool_table} \ + SET \ + gateway = '', \ + pool_key = '', \ + expiry_time = datetime('now') \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND pool_key = '${pool_key}' \ + AND framedipaddress = '%{DHCP-Client-IP-Address}' \ + AND ${ippool_table}.status_id IN \ + (SELECT status_id FROM dhcpstatus WHERE status = 'dynamic')" + + +# +# This query is not applicable to DHCP +# +on_clear = "" + + +# **************** +# * DHCP DECLINE * +# **************** + +# +# This query marks an IP address as declined when a DHCP Decline +# packet arrives +# +off_clear = "\ + UPDATE ${ippool_table} \ + SET status_id = (SELECT status_id FROM dhcpstatus WHERE status = 'declined') \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND pool_key = '${pool_key}' \ + AND framedipaddress = '%{DHCP-Requested-IP-Address}'" + diff --git a/raddb/mods-config/sql/ippool-dhcp/sqlite/schema.sql b/raddb/mods-config/sql/ippool-dhcp/sqlite/schema.sql new file mode 100644 index 0000000..339d58d --- /dev/null +++ b/raddb/mods-config/sql/ippool-dhcp/sqlite/schema.sql @@ -0,0 +1,25 @@ +-- +-- Table structure for table 'dhcpippool' +-- +CREATE TABLE dhcpstatus ( + status_id int PRIMARY KEY, + status varchar(10) NOT NULL +); + +INSERT INTO dhcpstatus (status_id, status) VALUES (1, 'dynamic'), (2, 'static'), (3, 'declined'), (4, 'disabled'); + +CREATE TABLE dhcpippool ( + id int(11) PRIMARY KEY, + pool_name varchar(30) NOT NULL, + framedipaddress varchar(15) NOT NULL default '', + pool_key varchar(30) NOT NULL default '', + gateway varchar(15) NOT NULL default '', + expiry_time DATETIME NOT NULL default (DATETIME('now')), + status_id int NOT NULL default 1, + counter int NOT NULL default 0, + FOREIGN KEY(status_id) REFERENCES dhcpstatus(status_id) +); + +CREATE INDEX dhcpippool_poolname_expire ON dhcpippool(pool_name, expiry_time); +CREATE INDEX dhcpippool_framedipaddress ON dhcpippool(framedipaddress); +CREATE INDEX dhcpippool_poolname_poolkey_ipaddress ON dhcpippool(pool_name, pool_key, framedipaddress); diff --git a/raddb/mods-config/sql/ippool/mongo/queries.conf b/raddb/mods-config/sql/ippool/mongo/queries.conf new file mode 100644 index 0000000..9d7d070 --- /dev/null +++ b/raddb/mods-config/sql/ippool/mongo/queries.conf @@ -0,0 +1,109 @@ +# -*- text -*- +# +# ippool/mongo/queries.conf -- Mongo queries for rlm_sqlippool +# +# $Id$ + +# +# The IP Pool queries expect a result like: +# +# { +# pool_key: "bob" +# pool_name: "my_pool" +# expiry_time: xxx +# value: "192.168.1.1" +# } +# +# i.e. the results are in "value", and not "framed_ip_address". +# +# When using dynamic expansions such as "%{sql:... mongo query ...}", +# Mongo uses a lot of curly brackets, {..}. Any closing braces have +# to be escaped as %}. Sorry, that is a limitation of the FreeRADIUS +# parser. +# + +# +# TBD +# +on_begin = "" +off_begin = "" + +allocate_begin = "" + +# +# This query allocates an IP address from the Pool +# +allocate_find = "db.mypool_collection.findAndModify( \ + { \ + 'query': {' \ + '$and': [ \ + { \ + 'pool_name': '%{control:Pool-Name}' \ + }, \ + { \ + 'nas_ip': '%{Nas-IP-Address}' \ + }, \ + { \ + '$or': [ \ + { \ + 'calling_station_id': '%{Calling-Station-Id}' \ + }, \ + { \ + 'locked': 0 \ + } \ + ] \ + } \ + ] \ + }, \ + 'update': { \ + 'locked': 1', \ + 'calling_station_id': '%{Calling-Station-Id'}' \ + }, \ + 'fields': { \ + '_id': 0, 'framed_ip_address': 1 \ + } \ + })" + +allocate_update = "" + +allocate_clear = "db.mypool_collection.findAndModify( \ + { \ + 'query': { \ + '$and': [ \ + { \ + 'pool_name': '%{Control:Pool-Name}' \ + }, \ + { \ + 'nas_ip': '%{Nas-IP-Address}' \ + }, \ + { \ + 'calling_station_id': '%{Calling-Station-Id}' \ + }, \ + { \ + 'locked': 1 \ + } \ + ] \ + }, \ + 'update': { \ + 'locked': 0, \ + 'calling_station_id': '' \ + } \ + })" + +allocate_commit = "" + +start_begin = "" +start_update = "" +start_commit = "" + +stop_begin = "" +stop_clear = "" +stop_commit = "" + +alive_begin = "" +alive_update = "" +alive_commit = "" + +on_clear = "" +off_clear = "" + diff --git a/raddb/mods-config/sql/ippool/mssql/procedure.sql b/raddb/mods-config/sql/ippool/mssql/procedure.sql new file mode 100644 index 0000000..5c621fb --- /dev/null +++ b/raddb/mods-config/sql/ippool/mssql/procedure.sql @@ -0,0 +1,137 @@ +-- +-- A stored procedure to reallocate a user's previous address, otherwise +-- provide a free address. +-- +-- Using this SP reduces the usual set dialogue of queries to a single +-- query: +-- +-- BEGIN TRAN; "SELECT FOR UPDATE"; UPDATE; COMMIT TRAN; -> EXEC sp +-- +-- The stored procedure is executed on an database instance within a single +-- round trip which often leads to reduced deadlocking and significant +-- performance improvements especially on multi-master clusters, perhaps even +-- by an order of magnitude or more. +-- +-- To use this stored procedure the corresponding queries.conf statements must +-- be configured as follows: +-- +-- allocate_begin = "" +-- allocate_find = "\ +-- EXEC fr_allocate_previous_or_new_framedipaddress \ +-- @v_pool_name = '%{control:${pool_name}}', \ +-- @v_username = '%{User-Name}', \ +-- @v_callingstationid = '%{Calling-Station-Id}', \ +-- @v_nasipaddress = '%{NAS-IP-Address}', \ +-- @v_pool_key = '${pool_key}', \ +-- @v_lease_duration = ${lease_duration} \ +-- " +-- allocate_update = "" +-- allocate_commit = "" +-- + +CREATE INDEX UserName_CallingStationId ON radippool(pool_name,UserName,CallingStationId) +GO + +CREATE OR ALTER PROCEDURE fr_allocate_previous_or_new_framedipaddress + @v_pool_name VARCHAR(64), + @v_username VARCHAR(64), + @v_callingstationid VARCHAR(64), + @v_nasipaddress VARCHAR(15), + @v_pool_key VARCHAR(64), + @v_lease_duration INT +AS + BEGIN + + -- MS SQL lacks a "SELECT FOR UPDATE" statement, and its table + -- hints do not provide a direct means to implement the row-level + -- read lock needed to guarentee that concurrent queries do not + -- select the same Framed-IP-Address for allocation to distinct + -- users. + -- + -- The "WITH cte AS ( SELECT ... ) UPDATE cte ... OUTPUT INTO" + -- patterns in this procedure body compensate by wrapping + -- the SELECT in a synthetic UPDATE which locks the row. + + DECLARE @r_address_tab TABLE(id VARCHAR(15)); + DECLARE @r_address VARCHAR(15); + + BEGIN TRAN; + + -- Reissue an existing IP address lease when re-authenticating a session + -- + WITH cte AS ( + SELECT TOP(1) FramedIPAddress + FROM radippool WITH (xlock rowlock readpast) + WHERE pool_name = @v_pool_name + AND expiry_time > CURRENT_TIMESTAMP + AND NASIPAddress = @v_nasipaddress AND pool_key = @v_pool_key + ) + UPDATE cte + SET FramedIPAddress = FramedIPAddress + OUTPUT INSERTED.FramedIPAddress INTO @r_address_tab; + SELECT @r_address = id FROM @r_address_tab; + + -- Reissue an user's previous IP address, provided that the lease is + -- available (i.e. enable sticky IPs) + -- + -- When using this SELECT you should delete the one above. You must also + -- set allocate_clear = "" in queries.conf to persist the associations + -- for expired leases. + -- + -- WITH cte AS ( + -- SELECT TOP(1) FramedIPAddress + -- FROM radippool WITH (xlock rowlock readpast) + -- WHERE pool_name = @v_pool_name + -- AND NASIPAddress = @v_nasipaddress AND pool_key = @v_pool_key + -- ) + -- UPDATE cte + -- SET FramedIPAddress = FramedIPAddress + -- OUTPUT INSERTED.FramedIPAddress INTO @r_address_tab; + -- SELECT @r_address = id FROM @r_address_tab; + + -- If we didn't reallocate a previous address then pick the least + -- recently used address from the pool which maximises the likelihood + -- of re-assigning the other addresses to their recent user + -- + IF @r_address IS NULL + BEGIN + WITH cte AS ( + SELECT TOP(1) FramedIPAddress + FROM radippool WITH (xlock rowlock readpast) + WHERE pool_name = @v_pool_name + AND expiry_time < CURRENT_TIMESTAMP + ORDER BY + expiry_time + ) + UPDATE cte + SET FramedIPAddress = FramedIPAddress + OUTPUT INSERTED.FramedIPAddress INTO @r_address_tab; + SELECT @r_address = id FROM @r_address_tab; + END + + -- Return nothing if we failed to allocated an address + -- + IF @r_address IS NULL + BEGIN + COMMIT TRAN; + RETURN; + END + + -- Update the pool having allocated an IP address + -- + UPDATE radippool + SET + NASIPAddress = @v_nasipaddress, + pool_key = @v_pool_key, + CallingStationId = @v_callingstationid, + UserName = @v_username, + expiry_time = DATEADD(SECOND,@v_lease_duration,CURRENT_TIMESTAMP) + WHERE framedipaddress = @r_address; + + COMMIT TRAN; + + -- Return the address that we allocated + SELECT @r_address; + + END +GO diff --git a/raddb/mods-config/sql/ippool/mssql/queries.conf b/raddb/mods-config/sql/ippool/mssql/queries.conf new file mode 100644 index 0000000..8105dcc --- /dev/null +++ b/raddb/mods-config/sql/ippool/mssql/queries.conf @@ -0,0 +1,175 @@ +# -*- text -*- +# +# ippool/mssql/queries.conf -- MSSQL queries for rlm_sqlippool +# +# $Id$ + +# +# MSSQL-specific syntax - required if finding the address and updating +# it are separate queries +# +#allocate_begin = "BEGIN TRAN" +#allocate_commit = "COMMIT TRAN" + +allocate_begin = "" +allocate_commit = "" + +# +# This series of queries allocates an IP address +# + +# +# Attempt to allocate the address a client previously had. This is based on pool_key +# and nasipaddress. Change the criteria if the identifier for "stickyness" is different. +# If different criteria are used, check the indexes on the IP pool table to ensure the fields +# are appropriately indexed. To disable stickyness comment out this query. +# +allocate_existing = "\ + WITH cte AS ( \ + SELECT TOP(1) FramedIPAddress, CallingStationId, UserName, expiry_time \ + FROM ${ippool_table} WITH (xlock rowlock readpast) \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND NASIPAddress = '%{NAS-IP-Address}' AND pool_key = '${pool_key}' \ + ORDER BY expiry_time DESC \ + ) \ + UPDATE cte \ + SET \ + CallingStationId = '%{Calling-Station-Id}', \ + UserName = '%{User-Name}', expiry_time = DATEADD(SECOND,${lease_duration},CURRENT_TIMESTAMP) \ + OUTPUT INSERTED.FramedIPAddress" + +# +# Find a free IP address from the pool, choosing the oldest expired one. +# +allocate_find = "\ + WITH cte AS ( \ + SELECT TOP(1) FramedIPAddress, NASIPAddress, pool_key, \ + CallingStationId, UserName, expiry_time \ + FROM ${ippool_table} WITH (xlock rowlock readpast) \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND expiry_time < CURRENT_TIMESTAMP \ + ORDER BY expiry_time \ + ) \ + UPDATE cte \ + SET \ + NASIPAddress = '%{NAS-IP-Address}', pool_key = '${pool_key}', \ + CallingStationId = '%{Calling-Station-Id}', \ + UserName = '%{User-Name}', expiry_time = DATEADD(SECOND,${lease_duration},CURRENT_TIMESTAMP) \ + OUTPUT INSERTED.FramedIPAddress" + +# +# If you prefer to allocate a random IP address every time, use this query instead. +# Note: This is very slow if you have a lot of free IPs. +# +#allocate_find = "\ +# WITH cte AS ( \ +# SELECT TOP(1) FramedIPAddress, NASIPAddress, pool_key, \ +# CallingStationId, UserName, expiry_time \ +# FROM ${ippool_table} \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND expiry_time < CURRENT_TIMESTAMP \ +# ORDER BY newid() \ +# ) \ +# UPDATE cte WITH (rowlock, readpast) \ +# SET \ +# NASIPAddress = '%{NAS-IP-Address}', pool_key = '${pool_key}', \ +# CallingStationId = '%{Calling-Station-Id}', \ +# UserName = '%{User-Name}', expiry_time = DATEADD(SECOND,${lease_duration},CURRENT_TIMESTAMP) \ +# OUTPUT INSERTED.FramedIPAddress" + +# +# If an IP could not be allocated, check to see if the pool exists or not +# This allows the module to differentiate between a full pool and no pool +# Note: If you are not running redundant pool modules this query may be +# commented out to save running this query every time an ip is not allocated. +# +pool_check = "\ + SELECT TOP(1) id \ + FROM ${ippool_table} \ + WHERE pool_name='%{control:${pool_name}}'" + +# +# This is the final IP Allocation query, which saves the allocated ip details. +# Only needed if allocate_existing / allocate_find do not also update the pool. +# +#allocate_update = "\ +# UPDATE ${ippool_table} \ +# SET \ +# NASIPAddress = '%{NAS-IP-Address}', pool_key = '${pool_key}', \ +# CallingStationId = '%{Calling-Station-Id}', \ +# UserName = '%{User-Name}', expiry_time = DATEADD(SECOND,${lease_duration},CURRENT_TIMESTAMP) \ +# WHERE FramedIPAddress = '%I'" + +# +# Use a stored procedure to find AND allocate the address. Read and customise +# `procedure.sql` in this directory to determine the optimal configuration. +# +#allocate_begin = "" +#allocate_find = "\ +# EXEC fr_allocate_previous_or_new_framedipaddress \ +# @v_pool_name = '%{control:${pool_name}}', \ +# @v_username = '%{User-Name}', \ +# @v_callingstationid = '%{Calling-Station-Id}', \ +# @v_nasipaddress = '%{NAS-IP-Address}', \ +# @v_pool_key = '${pool_key}', \ +# @v_lease_duration = ${lease_duration} \ +# " +#allocate_update = "" +#allocate_commit = "" + +# +# This series of queries frees an IP number when an accounting START record arrives. +# +start_update = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = DATEADD(SECOND,${lease_duration},CURRENT_TIMESTAMP) \ + WHERE NASIPAddress = '%{NAS-IP-Address}' \ + AND pool_key = '${pool_key}' \ + AND UserName = '%{User-Name}' \ + AND CallingStationId = '%{Calling-Station-Id}' \ + AND FramedIPAddress = '%{${attribute_name}}'" + +# +# Expire an IP when an accounting STOP record arrives +# +stop_clear = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = CURRENT_TIMESTAMP \ + WHERE NASIPAddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}' \ + AND pool_key = '${pool_key}' \ + AND UserName = '%{User-Name}' \ + AND CallingStationId = '%{Calling-Station-Id}' \ + AND FramedIPAddress = '%{${attribute_name}}'" + +# +# Update the expiry time for an IP when an accounting ALIVE record arrives +# +alive_update = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = DATEADD(SECOND,${lease_duration},CURRENT_TIMESTAMP) \ + WHERE NASIPAddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}' \ + AND pool_key = '${pool_key}' \ + AND UserName = '%{User-Name}' \ + AND CallingStationId = '%{Calling-Station-Id}' \ + AND FramedIPAddress = '%{${attribute_name}}'" + +# +# Expires all IPs allocated to a NAS when an accounting ON record arrives +# +on_clear = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = CURRENT_TIMESTAMP \ + WHERE NASIPAddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}'" + +# +# Expires all IPs allocated to a NAS when an accounting OFF record arrives +# +off_clear = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = CURRENT_TIMESTAMP \ + WHERE NASIPAddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}'" diff --git a/raddb/mods-config/sql/ippool/mssql/schema.sql b/raddb/mods-config/sql/ippool/mssql/schema.sql new file mode 100644 index 0000000..d4bff44 --- /dev/null +++ b/raddb/mods-config/sql/ippool/mssql/schema.sql @@ -0,0 +1,25 @@ +-- +-- Table structure for table 'radippool' +-- +CREATE TABLE radippool ( + id int IDENTITY (1,1) NOT NULL, + pool_name varchar(30) NOT NULL, + FramedIPAddress varchar(15) NOT NULL default '', + NASIPAddress varchar(15) NOT NULL default '', + CalledStationId VARCHAR(32) NOT NULL default '', + CallingStationId VARCHAR(30) NOT NULL default '', + expiry_time DATETIME NOT NULL default CURRENT_TIMESTAMP, + UserName varchar(64) NOT NULL default '', + pool_key varchar(30) NOT NULL default '', + PRIMARY KEY (id) +) +GO + +CREATE INDEX poolname_expire ON radippool(pool_name, expiry_time) +GO + +CREATE INDEX FramedIPAddress ON radippool(FramedIPAddress) +GO + +CREATE INDEX NASIPAddress_poolkey_FramedIPAddress ON radippool(NASIPAddress, pool_key, FramedIPAddress) +GO diff --git a/raddb/mods-config/sql/ippool/mysql/procedure-no-skip-locked.sql b/raddb/mods-config/sql/ippool/mysql/procedure-no-skip-locked.sql new file mode 100644 index 0000000..1c88446 --- /dev/null +++ b/raddb/mods-config/sql/ippool/mysql/procedure-no-skip-locked.sql @@ -0,0 +1,149 @@ +-- +-- A stored procedure to reallocate a user's previous address, otherwise +-- provide a free address. +-- +-- NOTE: This version of the SP is intended for MySQL variants that do not +-- support the SKIP LOCKED pragma, i.e. MariaDB and versions of MySQL +-- prior to 8.0. It should be a lot faster than using the default SP +-- without the SKIP LOCKED pragma under highly concurrent workloads +-- and not result in thread starvation. +-- +-- It is however a *useful hack* which should not be used if SKIP +-- LOCKED is available. +-- +-- WARNING: This query uses server-local, "user locks" (GET_LOCK and +-- RELEASE_LOCK), without the need for a transaction, to emulate +-- row locking with locked-row skipping. User locks are not +-- supported on clusters such as Galera and MaxScale. +-- +-- Using this SP reduces the usual set dialogue of queries to a single +-- query: +-- +-- START TRANSACTION; SELECT FOR UPDATE; UPDATE; COMMIT; -> CALL sp() +-- +-- The stored procedure is executed within a single round trip which often +-- leads to reduced deadlocking and significant performance improvements. +-- +-- To use this stored procedure the corresponding queries.conf statements must +-- be configured as follows: +-- +-- allocate_begin = "" +-- allocate_find = "\ +-- CALL fr_allocate_previous_or_new_framedipaddress( \ +-- '%{control:${pool_name}}', \ +-- '%{User-Name}', \ +-- '%{Calling-Station-Id}', \ +-- '%{NAS-IP-Address}', \ +-- '${pool_key}', \ +-- ${lease_duration} \ +-- )" +-- allocate_update = "" +-- allocate_commit = "" +-- + +CREATE INDEX poolname_username_callingstationid ON radippool(pool_name,username,callingstationid); + +DELIMITER $$ + +DROP PROCEDURE IF EXISTS fr_allocate_previous_or_new_framedipaddress; +CREATE PROCEDURE fr_allocate_previous_or_new_framedipaddress ( + IN v_pool_name VARCHAR(64), + IN v_username VARCHAR(64), + IN v_callingstationid VARCHAR(64), + IN v_nasipaddress VARCHAR(15), + IN v_pool_key VARCHAR(64), + IN v_lease_duration INT +) +SQL SECURITY INVOKER +proc:BEGIN + DECLARE r_address VARCHAR(15); + + -- Reissue an existing IP address lease when re-authenticating a session + -- + SELECT framedipaddress INTO r_address + FROM radippool + WHERE pool_name = v_pool_name + AND expiry_time > NOW() + AND nasipaddress = v_nasipaddress + AND pool_key = v_pool_key + LIMIT 1; + + -- Reissue an user's previous IP address, provided that the lease is + -- available (i.e. enable sticky IPs) + -- + -- When using this SELECT you should delete the one above. You must also + -- set allocate_clear = "" in queries.conf to persist the associations + -- for expired leases. + -- + -- SELECT framedipaddress INTO r_address + -- FROM radippool + -- WHERE pool_name = v_pool_name + -- AND nasipaddress = v_nasipaddress + -- AND pool_key = v_pool_key + -- LIMIT 1; + + IF r_address IS NOT NULL THEN + UPDATE radippool + SET + nasipaddress = v_nasipaddress, + pool_key = v_pool_key, + callingstationid = v_callingstationid, + username = v_username, + expiry_time = NOW() + INTERVAL v_lease_duration SECOND + WHERE + framedipaddress = r_address; + SELECT r_address; + LEAVE proc; + END IF; + + REPEAT + + -- If we didn't reallocate a previous address then pick the least + -- recently used address from the pool which maximises the likelihood + -- of re-assigning the other addresses to their recent user + -- + SELECT framedipaddress INTO r_address + FROM radippool + WHERE pool_name = v_pool_name + AND expiry_time < NOW() + -- + -- WHERE ... GET_LOCK(...,0) = 1 is a poor man's SKIP LOCKED that simulates + -- a row-level lock using a "user lock" that allows the locked "rows" to be + -- skipped. After the user lock is acquired and the SELECT retired it does + -- not mean that the entirety of the WHERE clause is still true: Another + -- thread may have updated the expiry time and released the lock after we + -- checked the expiry_time but before we acquired the lock since SQL is free + -- to reorder the WHERE condition. Therefore we must recheck the condition + -- in the UPDATE statement below to detect this race. + -- + AND GET_LOCK(CONCAT('radippool_', framedipaddress), 0) = 1 + LIMIT 1; + + IF r_address IS NULL THEN + DO RELEASE_LOCK(CONCAT('radippool_', r_address)); + LEAVE proc; + END IF; + + UPDATE radippool + SET + nasipaddress = v_nasipaddress, + pool_key = v_pool_key, + callingstationid = v_callingstationid, + username = v_username, + expiry_time = NOW() + INTERVAL v_lease_duration SECOND + WHERE + framedipaddress = r_address + -- + -- Here we re-evaluate the original condition for selecting the address + -- to detect a race, in which case we try again... + -- + AND expiry_time<NOW(); + + UNTIL ROW_COUNT() <> 0 END REPEAT; + + DO RELEASE_LOCK(CONCAT('radippool_', r_address)); + SELECT r_address; + +END$$ + +DELIMITER ; diff --git a/raddb/mods-config/sql/ippool/mysql/procedure.sql b/raddb/mods-config/sql/ippool/mysql/procedure.sql new file mode 100644 index 0000000..2a52566 --- /dev/null +++ b/raddb/mods-config/sql/ippool/mysql/procedure.sql @@ -0,0 +1,139 @@ +-- +-- A stored procedure to reallocate a user's previous address, otherwise +-- provide a free address. +-- +-- Using this SP reduces the usual set dialogue of queries to a single +-- query: +-- +-- START TRANSACTION; SELECT FOR UPDATE; UPDATE; COMMIT; -> CALL sp() +-- +-- The stored procedure is executed on an database instance within a single +-- round trip which often leads to reduced deadlocking and significant +-- performance improvements especially on multi-master clusters, perhaps even +-- by an order of magnitude or more. +-- +-- To use this stored procedure the corresponding queries.conf statements must +-- be configured as follows: +-- +-- allocate_begin = "" +-- allocate_find = "\ +-- CALL fr_allocate_previous_or_new_framedipaddress( \ +-- '%{control:${pool_name}}', \ +-- '%{User-Name}', \ +-- '%{Calling-Station-Id}', \ +-- '%{Called-Station-Id}', \ +-- '%{NAS-IP-Address}', \ +-- '${pool_key}', \ +-- ${lease_duration} \ +-- )" +-- allocate_update = "" +-- allocate_commit = "" +-- + +CREATE INDEX poolname_username_callingstationid ON radippool(pool_name,username,callingstationid); + +DELIMITER $$ + +DROP PROCEDURE IF EXISTS fr_allocate_previous_or_new_framedipaddress; +CREATE PROCEDURE fr_allocate_previous_or_new_framedipaddress ( + IN v_pool_name VARCHAR(64), + IN v_username VARCHAR(64), + IN v_callingstationid VARCHAR(64), + IN v_calledstationid VARCHAR(64), + IN v_nasipaddress VARCHAR(15), + IN v_pool_key VARCHAR(64), + IN v_lease_duration INT +) +SQL SECURITY INVOKER +proc:BEGIN + DECLARE r_address VARCHAR(15); + + DECLARE EXIT HANDLER FOR SQLEXCEPTION + BEGIN + ROLLBACK; + RESIGNAL; + END; + + SET TRANSACTION ISOLATION LEVEL READ COMMITTED; + + START TRANSACTION; + + -- Reissue an existing IP address lease when re-authenticating a session + -- + SELECT framedipaddress INTO r_address + FROM radippool + WHERE pool_name = v_pool_name + AND expiry_time > NOW() + AND nasipaddress = v_nasipaddress + AND pool_key = v_pool_key + LIMIT 1 + FOR UPDATE; +-- FOR UPDATE SKIP LOCKED; -- Better performance, but limited support + + -- NOTE: You should enable SKIP LOCKED here (as well as any other + -- instances) if your database server supports it. If it is not + -- supported and you are not running a multi-master cluster (e.g. + -- Galera or MaxScale) then you should instead consider using the + -- SP in procedure-no-skip-locked.sql which will be faster and + -- less likely to result in thread starvation under highly + -- concurrent load. + + -- Reissue an user's previous IP address, provided that the lease is + -- available (i.e. enable sticky IPs) + -- + -- When using this SELECT you should delete the one above. You must also + -- set allocate_clear = "" in queries.conf to persist the associations + -- for expired leases. + -- + -- SELECT framedipaddress INTO r_address + -- FROM radippool + -- WHERE pool_name = v_pool_name + -- AND nasipaddress = v_nasipaddress + -- AND pool_key = v_pool_key + -- LIMIT 1 + -- FOR UPDATE; + -- -- FOR UPDATE SKIP LOCKED; -- Better performance, but limited support + + -- If we didn't reallocate a previous address then pick the least + -- recently used address from the pool which maximises the likelihood + -- of re-assigning the other addresses to their recent user + -- + IF r_address IS NULL THEN + SELECT framedipaddress INTO r_address + FROM radippool + WHERE pool_name = v_pool_name + AND expiry_time < NOW() + ORDER BY + expiry_time + LIMIT 1 + FOR UPDATE; +-- FOR UPDATE SKIP LOCKED; -- Better performance, but limited support + END IF; + + -- Return nothing if we failed to allocated an address + -- + IF r_address IS NULL THEN + COMMIT; + LEAVE proc; + END IF; + + -- Update the pool having allocated an IP address + -- + UPDATE radippool + SET + nasipaddress = v_nasipaddress, + pool_key = v_pool_key, + callingstationid = v_callingstationid, + calledstationid = v_calledstationid, + username = v_username, + expiry_time = NOW() + INTERVAL v_lease_duration SECOND + WHERE framedipaddress = r_address; + + COMMIT; + + -- Return the address that we allocated + SELECT r_address; + +END$$ + +DELIMITER ; diff --git a/raddb/mods-config/sql/ippool/mysql/queries.conf b/raddb/mods-config/sql/ippool/mysql/queries.conf new file mode 100644 index 0000000..c421020 --- /dev/null +++ b/raddb/mods-config/sql/ippool/mysql/queries.conf @@ -0,0 +1,156 @@ +# -*- text -*- +# +# ippool/mysql/queries.conf -- MySQL queries for rlm_sqlippool +# +# $Id$ + + +# Using SKIP LOCKED speeds up selection queries +# However, it requires MySQL >= 8.0.1 or MariaDB >= 10.6. +# Uncomment the following if you are running a suitable +# version of MySQL +# +#skip_locked = "SKIP LOCKED" +skip_locked = "" + +# +# This series of queries allocates an IP address +# + +# +# Attempt to allocate the address a client previously had. This is based on pool_key +# and nasipaddress. Change the criteria if the identifier for "stickyness" is different. +# If different criteria are used, check the indexes on the IP pool table to ensure the fields +# are appropriately indexed. To disable stickyness comment out this query. +# +allocate_existing = "\ + SELECT framedipaddress FROM ${ippool_table} \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND nasipaddress = '%{NAS-IP-Address}' AND pool_key = '${pool_key}' \ + ORDER BY expiry_time DESC \ + LIMIT 1 \ + FOR UPDATE ${skip_locked}" + +# +# Find a free IP address from the pool, choosing the oldest expired one. +# +allocate_find = "\ + SELECT framedipaddress FROM ${ippool_table} \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND expiry_time < NOW() \ + ORDER BY expiry_time \ + LIMIT 1 \ + FOR UPDATE ${skip_locked}" + +# +# If you prefer to allocate a random IP address every time, use this query instead. +# Note: This is very slow if you have a lot of free IPs. +# +#allocate_find = "\ +# SELECT framedipaddress FROM ${ippool_table} \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND expiry_time < NOW() \ +# ORDER BY \ +# RAND() \ +# LIMIT 1 \ +# FOR UPDATE ${skip_locked}" + +# +# If an IP could not be allocated, check to see if the pool exists or not +# This allows the module to differentiate between a full pool and no pool +# Note: If you are not running redundant pool modules this query may be +# commented out to save running this query every time an ip is not allocated. +# +pool_check = "\ + SELECT id \ + FROM ${ippool_table} \ + WHERE pool_name='%{control:${pool_name}}' \ + LIMIT 1" + +# +# This is the final IP Allocation query, which saves the allocated ip details. +# +allocate_update = "\ + UPDATE ${ippool_table} \ + SET \ + nasipaddress = '%{NAS-IP-Address}', pool_key = '${pool_key}', \ + callingstationid = '%{Calling-Station-Id}', \ + username = '%{User-Name}', expiry_time = NOW() + INTERVAL ${lease_duration} SECOND \ + WHERE framedipaddress = '%I'" + +# +# Use a stored procedure to find AND allocate the address. Read and customise +# `procedure.sql` in this directory to determine the optimal configuration. +# +#allocate_begin = "" +#allocate_find = "\ +# CALL fr_allocate_previous_or_new_framedipaddress( \ +# '%{control:${pool_name}}', \ +# '%{User-Name}', \ +# '%{Calling-Station-Id}', \ +# '%{Called-Station-Id}', \ +# '%{NAS-IP-Address}', \ +# '${pool_key}', \ +# ${lease_duration} \ +# )" +#allocate_update = "" +#allocate_commit = "" + +# +# This series of queries frees an IP number when an accounting START record arrives. +# +start_update = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = NOW() + INTERVAL ${lease_duration} SECOND \ + WHERE nasipaddress = '%{NAS-IP-Address}' \ + AND pool_key = '${pool_key}' \ + AND username = '%{User-Name}' \ + AND callingstationid = '%{Calling-Station-Id}' \ + AND framedipaddress = '%{${attribute_name}}'" + +# +# This query expires an IP number when an accounting STOP record arrives. +# +stop_clear = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = NOW() \ + WHERE nasipaddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}' \ + AND pool_key = '${pool_key}' \ + AND username = '%{User-Name}' \ + AND callingstationid = '%{Calling-Station-Id}' \ + AND framedipaddress = '%{${attribute_name}}'" + +# +# This series of queries frees an IP number when an accounting ALIVE record arrives. +# +alive_update = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = NOW() + INTERVAL ${lease_duration} SECOND \ + WHERE nasipaddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}' \ + AND pool_key = '${pool_key}' \ + AND username = '%{User-Name}' \ + AND callingstationid = '%{Calling-Station-Id}' \ + AND framedipaddress = '%{${attribute_name}}'" + +# +# This series of queries expires the IP numbers allocate to a +# NAS when an accounting ON record arrives +# +on_clear = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = NOW() \ + WHERE nasipaddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}'" + +# +# This series of queries expires the IP numbers allocate to a +# NAS when an accounting OFF record arrives +# +off_clear = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = NOW() \ + WHERE nasipaddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}'" diff --git a/raddb/mods-config/sql/ippool/mysql/schema.sql b/raddb/mods-config/sql/ippool/mysql/schema.sql new file mode 100644 index 0000000..f79d1b1 --- /dev/null +++ b/raddb/mods-config/sql/ippool/mysql/schema.sql @@ -0,0 +1,18 @@ +# +# Table structure for table 'radippool' +# +CREATE TABLE IF NOT EXISTS radippool ( + id int(11) unsigned NOT NULL auto_increment, + pool_name varchar(30) NOT NULL, + framedipaddress varchar(15) NOT NULL default '', + nasipaddress varchar(15) NOT NULL default '', + calledstationid VARCHAR(30) NOT NULL default '', + callingstationid VARCHAR(30) NOT NULL default '', + expiry_time DATETIME NOT NULL default NOW(), + username varchar(64) NOT NULL default '', + pool_key varchar(30) NOT NULL default '', + PRIMARY KEY (id), + KEY radippool_poolname_expire (pool_name, expiry_time), + KEY framedipaddress (framedipaddress), + KEY radippool_nasip_poolkey_ipaddress (nasipaddress, pool_key, framedipaddress) +) ENGINE=InnoDB; diff --git a/raddb/mods-config/sql/ippool/oracle/procedure.sql b/raddb/mods-config/sql/ippool/oracle/procedure.sql new file mode 100644 index 0000000..e483236 --- /dev/null +++ b/raddb/mods-config/sql/ippool/oracle/procedure.sql @@ -0,0 +1,129 @@ +-- +-- A stored procedure to reallocate a user's previous address, otherwise +-- provide a free address. +-- +-- Using this SP reduces the usual set dialogue of queries to a single +-- query: +-- +-- BEGIN; SELECT FOR UPDATE; UPDATE; COMMIT; -> SELECT sp() FROM dual +-- +-- The stored procedure is executed on an database instance within a single +-- round trip which often leads to reduced deadlocking and significant +-- performance improvements especially on multi-master clusters, perhaps even +-- by an order of magnitude or more. +-- +-- To use this stored procedure the corresponding queries.conf statements must +-- be configured as follows: +-- +-- allocate_begin = "" +-- allocate_find = "\ +-- SELECT fr_allocate_previous_or_new_framedipaddress( \ +-- '%{control:${pool_name}}', \ +-- '%{User-Name}', \ +-- '%{%{Calling-Station-Id}:-0}', \ +-- '%{NAS-IP-Address}', \ +-- '${pool_key}', \ +-- ${lease_duration} \ +-- ) FROM dual" +-- allocate_update = "" +-- allocate_commit = "" +-- + +CREATE OR REPLACE FUNCTION fr_allocate_previous_or_new_framedipaddress ( + v_pool_name IN VARCHAR2, + v_username IN VARCHAR2, + v_callingstationid IN VARCHAR2, + v_nasipaddress IN VARCHAR2, + v_pool_key IN VARCHAR2, + v_lease_duration IN INTEGER +) +RETURN varchar2 IS + PRAGMA AUTONOMOUS_TRANSACTION; + r_address varchar2(15); +BEGIN + + -- Reissue an existing IP address lease when re-authenticating a session + -- + BEGIN + SELECT framedipaddress INTO r_address FROM radippool WHERE id IN ( + SELECT id FROM ( + SELECT * + FROM radippool + WHERE pool_name = v_pool_name + AND expiry_time > current_timestamp + AND username = v_username + AND callingstationid = v_callingstationid + ) WHERE ROWNUM <= 1 + ) FOR UPDATE SKIP LOCKED; + EXCEPTION + WHEN NO_DATA_FOUND THEN + r_address := NULL; + END; + + -- Reissue an user's previous IP address, provided that the lease is + -- available (i.e. enable sticky IPs) + -- + -- When using this SELECT you should delete the one above. You must also + -- set allocate_clear = "" in queries.conf to persist the associations + -- for expired leases. + -- + -- BEGIN + -- SELECT framedipaddress INTO r_address FROM radippool WHERE id IN ( + -- SELECT id FROM ( + -- SELECT * + -- FROM radippool + -- WHERE pool_name = v_pool_name + -- AND username = v_username + -- AND callingstationid = v_callingstationid + -- ) WHERE ROWNUM <= 1 + -- ) FOR UPDATE SKIP LOCKED; + -- EXCEPTION + -- WHEN NO_DATA_FOUND THEN + -- r_address := NULL; + -- END; + + -- If we didn't reallocate a previous address then pick the least + -- recently used address from the pool which maximises the likelihood + -- of re-assigning the other addresses to their recent user + -- + IF r_address IS NULL THEN + BEGIN + SELECT framedipaddress INTO r_address FROM radippool WHERE id IN ( + SELECT id FROM ( + SELECT * + FROM radippool + WHERE pool_name = v_pool_name + AND expiry_time < CURRENT_TIMESTAMP + ORDER BY expiry_time + ) WHERE ROWNUM <= 1 + ) FOR UPDATE SKIP LOCKED; + EXCEPTION + WHEN NO_DATA_FOUND THEN + r_address := NULL; + END; + END IF; + + -- Return nothing if we failed to allocated an address + -- + IF r_address IS NULL THEN + COMMIT; + RETURN r_address; + END IF; + + -- Update the pool having allocated an IP address + -- + UPDATE radippool + SET + nasipaddress = v_nasipaddress, + pool_key = v_pool_key, + callingstationid = v_callingstationid, + username = v_username, + expiry_time = CURRENT_TIMESTAMP + v_lease_duration * INTERVAL '1' SECOND(1) + WHERE framedipaddress = r_address; + + -- Return the address that we allocated + COMMIT; + RETURN r_address; + +END; +/ diff --git a/raddb/mods-config/sql/ippool/oracle/queries.conf b/raddb/mods-config/sql/ippool/oracle/queries.conf new file mode 100644 index 0000000..1a64b28 --- /dev/null +++ b/raddb/mods-config/sql/ippool/oracle/queries.conf @@ -0,0 +1,172 @@ +# -*- text -*- +# +# ippool/oracle/queries.conf -- Oracle queries for rlm_sqlippool +# +# $Id$ + +# Using SKIP LOCKED speeds up selection queries +# However, it requires Oracle > 11g. It MAY work in 9i and 10g +# but is not documented. Uncomment the following if you are +# running a suitable version of Oracle +# +#skip_locked = "SKIP LOCKED" +skip_locked = "" + +allocate_begin = "commit" +start_begin = "commit" +alive_begin = "commit" +stop_begin = "commit" +on_begin = "commit" +off_begin = "commit" + +# +# Attempt to allocate the address a client previously had. This is based on pool_key +# and nasipaddress. Change the criteria if the identifier for "stickyness" is different. +# If different criteria are used, check the indexes on the IP pool table to ensure the fields +# are appropriately indexed. To disable stickyness comment out this query. +# +allocate_existing = "\ + SELECT framedipaddress FROM ${ippool_table} WHERE id IN ( \ + SELECT id FROM ( \ + SELECT * \ + FROM ${ippool_table} \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND nasipaddress = '%{NAS-IP-Address}' AND pool_key = '${pool_key}' \ + ) \ + ORDER BY expiry_time DESC \ + ) WHERE ROWNUM <= 1 \ + ) FOR UPDATE ${skip_locked}" + +# +# Find a free IP address from the pool, choosing the oldest expired one. +# +allocate_find = "\ + SELECT framedipaddress FROM ${ippool_table} WHERE id IN ( \ + SELECT id FROM ( \ + SELECT * \ + FROM ${ippool_table} \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND expiry_time < current_timestamp \ + ) \ + ORDER BY expiry_time \ + ) WHERE ROWNUM <= 1 \ + ) FOR UPDATE ${skip_locked}" + +# +# If you prefer to allocate a random IP address every time, use this query instead +# Note: This is very slow if you have a lot of free IPs. +# +#allocate_find = "\ +# SELECT framedipaddress FROM ${ippool_table} WHERE id IN ( \ +# SELECT id FROM ( \ +# SELECT * \ +# FROM ${ippool_table} \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND expiry_time < current_timestamp \ +# ORDER BY DBMS_RANDOM.VALUE \ +# ) WHERE ROWNUM <= 1 \ +# ) FOR UPDATE ${skip_locked}" + +# +# If an IP could not be allocated, check to see whether the pool exists or not +# This allows the module to differentiate between a full pool and no pool +# Note: If you are not running redundant pool modules this query may be commented +# out to save running this query every time an ip is not allocated. +# +pool_check = "\ + SELECT id \ + FROM (\ + SELECT id \ + FROM ${ippool_table} \ + WHERE pool_name='%{control:${pool_name}}'\ + ) \ + WHERE ROWNUM = 1" + +# +# This query marks the IP address handed out by "allocate-find" as used +# for the period of "lease_duration" after which time it may be reused. +# +allocate_update = "\ + UPDATE ${ippool_table} \ + SET \ + nasipaddress = '%{NAS-IP-Address}', \ + pool_key = '${pool_key}', \ + callingstationid = '%{%{Calling-Station-Id}:-0}', \ + username = '%{SQL-User-Name}', \ + expiry_time = current_timestamp + INTERVAL '${lease_duration}' second(1) \ + WHERE framedipaddress = '%I'" + +# +# Use a stored procedure to find AND allocate the address. Read and customise +# `procedure.sql` in this directory to determine the optimal configuration. +# +#allocate_begin = "" +#allocate_find = "\ +# SELECT fr_allocate_previous_or_new_framedipaddress( \ +# '%{control:${pool_name}}', \ +# '%{SQL-User-Name}', \ +# '%{%{Calling-Station-Id}:-0}', \ +# '%{NAS-IP-Address}', \ +# '${pool_key}', \ +# '${lease_duration}' \ +# )" +#allocate_update = "" +#allocate_commit = "" + +# +# This query extends an IP address lease by "lease_duration" when an accounting +# START record arrives +# +start_update = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = current_timestamp + INTERVAL '${lease_duration}' second(1) \ + WHERE nasipaddress = '%{NAS-IP-Address}' \ + AND pool_key = '${pool_key}'" + +# +# This query expires an IP address when an accounting STOP record arrives +# +stop_clear = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = current_timestamp - INTERVAL '1' second(1) \ + WHERE nasipaddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}' \ + AND pool_key = '${pool_key}' \ + AND username = '%{SQL-User-Name}' \ + AND callingstationid = '%{%{Calling-Station-Id}:-0}' \ + AND framedipaddress = '%{${attribute_name}}'" + +# +# This query extends an IP address lease by "lease_duration" when an accounting +# ALIVE record arrives +# +alive_update = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = current_timestamp + INTERVAL '${lease_duration}' second(1) \ + WHERE nasipaddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}' \ + AND pool_key = '${pool_key}' \ + AND framedipaddress = '%{${attribute_name}}' \ + AND username = '%{SQL-User-Name}' \ + AND callingstationid = '%{%{Calling-Station-Id}:-0}'" + +# +# This query expires all IP addresses allocated to a NAS when an +# accounting ON record arrives from that NAS +# +on_clear = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = current_timestamp - INTERVAL '1' second(1) \ + WHERE nasipaddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}'" + +# +# This query expires all IP addresses allocated to a NAS when an +# accounting OFF record arrives from that NAS +# +off_clear = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = current_timestamp - INTERVAL '1' second(1) \ + WHERE nasipaddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}'" diff --git a/raddb/mods-config/sql/ippool/oracle/schema.sql b/raddb/mods-config/sql/ippool/oracle/schema.sql new file mode 100644 index 0000000..adf1419 --- /dev/null +++ b/raddb/mods-config/sql/ippool/oracle/schema.sql @@ -0,0 +1,27 @@ +CREATE TABLE radippool ( + id INT PRIMARY KEY, + pool_name VARCHAR(30) NOT NULL, + framedipaddress VARCHAR(15) NOT NULL, + nasipaddress VARCHAR(15) NOT NULL, + pool_key VARCHAR(30) DEFAULT '', + CalledStationId VARCHAR(64) DEFAULT '', + CallingStationId VARCHAR(64) DEFAULT '', + expiry_time timestamp(0) DEFAULT CURRENT_TIMESTAMP, + username VARCHAR(64) DEFAULT '' +); + +CREATE INDEX radippool_poolname_expire ON radippool (pool_name, expiry_time); +CREATE INDEX radippool_framedipaddress ON radippool (framedipaddress); +CREATE INDEX radippool_nasip_poolkey_ipaddress ON radippool (nasipaddress, pool_key, framedipaddress); + +CREATE SEQUENCE radippool_seq START WITH 1 INCREMENT BY 1; + +CREATE OR REPLACE TRIGGER radippool_serialnumber + BEFORE INSERT OR UPDATE OF id ON radippool + FOR EACH ROW + BEGIN + if ( :new.id = 0 or :new.id is null ) then + SELECT radippool_seq.nextval into :new.id from dual; + end if; + END; +/ diff --git a/raddb/mods-config/sql/ippool/postgresql/procedure.sql b/raddb/mods-config/sql/ippool/postgresql/procedure.sql new file mode 100644 index 0000000..b1d580c --- /dev/null +++ b/raddb/mods-config/sql/ippool/postgresql/procedure.sql @@ -0,0 +1,111 @@ +-- +-- A stored procedure to reallocate a user's previous address, otherwise +-- provide a free address. +-- +-- Using this SP reduces the usual set dialogue of queries to a single +-- query: +-- +-- START TRANSACTION; SELECT FOR UPDATE; UPDATE; COMMIT; -> SELECT sp() +-- +-- The stored procedure is executed on an database instance within a single +-- round trip which often leads to reduced deadlocking and significant +-- performance improvements especially on multi-master clusters, perhaps even +-- by an order of magnitude or more. +-- +-- To use this stored procedure the corresponding queries.conf statements must +-- be configured as follows: +-- +-- allocate_begin = "" +-- allocate_find = "\ +-- SELECT fr_allocate_previous_or_new_framedipaddress( \ +-- '%{control:${pool_name}}', \ +-- '%{User-Name}', \ +-- '%{Calling-Station-Id}', \ +-- '%{NAS-IP-Address}', \ +-- '${pool_key}', \ +-- ${lease_duration} \ +-- )" +-- allocate_update = "" +-- allocate_commit = "" +-- + +CREATE INDEX radippool_poolname_username_callingstationid ON radippool(pool_name,username,callingstationid); + +CREATE OR REPLACE FUNCTION fr_allocate_previous_or_new_framedipaddress ( + v_pool_name VARCHAR(64), + v_username VARCHAR(64), + v_callingstationid VARCHAR(64), + v_nasipaddress VARCHAR(16), + v_pool_key VARCHAR(64), + v_lease_duration INT +) +RETURNS inet +LANGUAGE plpgsql +AS $$ +DECLARE + r_address inet; +BEGIN + + -- Reissue an existing IP address lease when re-authenticating a session + -- + SELECT framedipaddress INTO r_address + FROM radippool + WHERE pool_name = v_pool_name + AND expiry_time > NOW() + AND username = v_username + AND callingstationid = v_callingstationid + LIMIT 1 + FOR UPDATE SKIP LOCKED; + + -- Reissue an user's previous IP address, provided that the lease is + -- available (i.e. enable sticky IPs) + -- + -- When using this SELECT you should delete the one above. You must also + -- set allocate_clear = "" in queries.conf to persist the associations + -- for expired leases. + -- + -- SELECT framedipaddress INTO r_address + -- FROM radippool + -- WHERE pool_name = v_pool_name + -- AND username = v_username + -- AND callingstationid = v_callingstationid + -- LIMIT 1 + -- FOR UPDATE SKIP LOCKED; + + -- If we didn't reallocate a previous address then pick the least + -- recently used address from the pool which maximises the likelihood + -- of re-assigning the other addresses to their recent user + -- + IF r_address IS NULL THEN + SELECT framedipaddress INTO r_address + FROM radippool + WHERE pool_name = v_pool_name + AND expiry_time < NOW() + ORDER BY + expiry_time + LIMIT 1 + FOR UPDATE SKIP LOCKED; + END IF; + + -- Return nothing if we failed to allocated an address + -- + IF r_address IS NULL THEN + RETURN r_address; + END IF; + + -- Update the pool having allocated an IP address + -- + UPDATE radippool + SET + nasipaddress = v_nasipaddress, + pool_key = v_pool_key, + callingstationid = v_callingstationid, + username = v_username, + expiry_time = NOW() + v_lease_duration * interval '1 sec' + WHERE framedipaddress = r_address; + + -- Return the address that we allocated + RETURN r_address; + +END +$$; diff --git a/raddb/mods-config/sql/ippool/postgresql/queries.conf b/raddb/mods-config/sql/ippool/postgresql/queries.conf new file mode 100644 index 0000000..ce6f355 --- /dev/null +++ b/raddb/mods-config/sql/ippool/postgresql/queries.conf @@ -0,0 +1,207 @@ +# -*- text -*- +# +# ippool/postgresql/queries.conf -- PostgreSQL queries for rlm_sqlippool +# +# $Id$ + + +# Using SKIP LOCKED speeds up selection queries +# However, it requires PostgreSQL >= 9.5 Uncomment the +# following if you are running a suitable version of PostgreSQL +# +#skip_locked = "SKIP LOCKED" +skip_locked = "" + +# +# This series of queries allocates an IP address +# + +# +# The suggested queries locate IPs and update them in one query +# so no need for transaction wrappers +# +allocate_begin = "" +allocate_commit = "" + +# +# Attempt to allocate the address a client previously had. This is based on pool_key +# and nasipaddress. Change the criteria if the identifier for "stickyness" is different. +# If different criteria are used, check the indexes on the IP pool table to ensure the fields +# are appropriately indexed. To disable stickyness comment out this query. +# +allocate_existing = "\ + WITH cte AS ( \ + SELECT framedipaddress FROM ${ippool_table} \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND nasipaddress = '%{NAS-IP-Address}' AND pool_key = '${pool_key}' \ + ORDER BY expiry_time DESC \ + LIMIT 1 \ + FOR UPDATE ${skip_locked} \ + ) \ + UPDATE ${ippool_table} \ + SET \ + nasipaddress = '%{NAS-IP-Address}', \ + pool_key = '${pool_key}', \ + callingstationid = '%{Calling-Station-Id}', \ + username = '%{SQL-User-Name}', \ + expiry_time = 'now'::timestamp(0) + '${lease_duration} second'::interval \ + FROM cte WHERE cte.framedipaddress = ${ippool_table}.framedipaddress \ + RETURNING cte.framedipaddress" + +# +# Find a free IP address from the pool, choosing the oldest expired one. +# +allocate_find = "\ + WITH cte AS ( \ + SELECT framedipaddress FROM ${ippool_table} \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND expiry_time < 'now'::timestamp(0) \ + ORDER BY expiry_time \ + LIMIT 1 \ + FOR UPDATE ${skip_locked} \ + ) \ + UPDATE ${ippool_table} \ + SET \ + nasipaddress = '%{NAS-IP-Address}', \ + pool_key = '${pool_key}', \ + callingstationid = '%{Calling-Station-Id}', \ + username = '%{SQL-User-Name}', \ + expiry_time = 'now'::timestamp(0) + '${lease_duration} second'::interval \ + FROM cte WHERE cte.framedipaddress = ${ippool_table}.framedipaddress \ + RETURNING cte.framedipaddress" + +# +# If you prefer to allocate a random IP address every time, use this query instead +# Note: This is very slow if you have a lot of free IPs. +# +#allocate_find = "\ +# WITH cte AS ( \ +# SELECT framedipaddress FROM ${ippool_table} \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND expiry_time < 'now'::timestamp(0) \ +# ORDER BY RANDOM() \ +# LIMIT 1 \ +# FOR UPDATE ${skip_locked} \ +# ) \ +# UPDATE ${ippool_table} \ +# SET \ +# nasipaddress = '%{NAS-IP-Address}', \ +# pool_key = '${pool_key}', \ +# callingstationid = '%{Calling-Station-Id}', \ +# username = '%{SQL-User-Name}', \ +# expiry_time = 'now'::timestamp(0) + '${lease_duration} second'::interval \ +# FROM cte WHERE cte.framedipaddress = ${ippool_table}.framedipaddress \ +# RETURNING cte.framedipaddress" + +# +# If an IP could not be allocated, check to see whether the pool exists or not +# This allows the module to differentiate between a full pool and no pool +# Note: If you are not running redundant pool modules this query may be commented +# out to save running this query every time an ip is not allocated. +# +pool_check = "\ + SELECT id \ + FROM ${ippool_table} \ + WHERE pool_name='%{control:${pool_name}}' \ + LIMIT 1" + +# +# This query marks the IP address handed out by "allocate-find" as used +# for the period of "lease_duration" after which time it may be reused. +# This is only needed if the allocate_existing / allocate_find queries +# do not update the pool +# +#allocate_update = "\ +# UPDATE ${ippool_table} \ +# SET \ +# nasipaddress = '%{NAS-IP-Address}', \ +# pool_key = '${pool_key}', \ +# callingstationid = '%{Calling-Station-Id}', \ +# username = '%{SQL-User-Name}', \ +# expiry_time = 'now'::timestamp(0) + '${lease_duration} second'::interval \ +# WHERE framedipaddress = '%I'" + +# +# Use a stored procedure to find AND allocate the address. Read and customise +# `procedure.sql` in this directory to determine the optimal configuration. +# +# This requires PostgreSQL >= 9.5 as SKIP LOCKED is used. +# +# The "NO LOAD BALANCE" comment is included here to indicate to a PgPool +# system that this needs to be a write transaction. PgPool itself cannot +# detect this from the statement alone. If you are using PgPool and do not +# have this comment, the query may go to a read only server, and will fail. +# This has no negative effect if you are not using PgPool. +# +#allocate_begin = "" +#allocate_find = "\ +# /*NO LOAD BALANCE*/ \ +# SELECT fr_allocate_previous_or_new_framedipaddress( \ +# '%{control:${pool_name}}', \ +# '%{SQL-User-Name}', \ +# '%{Calling-Station-Id}', \ +# '%{NAS-IP-Address}', \ +# '${pool_key}', \ +# '${lease_duration}' \ +# )" +#allocate_update = "" +#allocate_commit = "" + +# +# This query extends an IP address lease by "lease_duration" when an accounting +# START record arrives +# +start_update = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = 'now'::timestamp(0) + '${lease_duration} second'::interval \ + WHERE nasipaddress = '%{NAS-IP-Address}' \ + AND pool_key = '${pool_key}'" + +# +# This query expires an IP address when an accounting +# STOP record arrives +# +stop_clear = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = 'now'::timestamp(0) - '1 second'::interval \ + WHERE nasipaddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}' \ + AND pool_key = '${pool_key}' \ + AND username = '%{SQL-User-Name}' \ + AND callingstationid = '%{Calling-Station-Id}' \ + AND framedipaddress = '%{${attribute_name}}'" + +# +# This query extends an IP address lease by "lease_duration" when an accounting +# ALIVE record arrives +# +alive_update = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = 'now'::timestamp(0) + '${lease_duration} seconds'::interval \ + WHERE nasipaddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}' \ + AND pool_key = '${pool_key}' \ + AND framedipaddress = '%{${attribute_name}}' \ + AND username = '%{SQL-User-Name}' \ + AND callingstationid = '%{Calling-Station-Id}'" + +# +# This query expires all IP addresses allocated to a NAS when an +# accounting ON record arrives from that NAS +# +on_clear = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = 'now'::timestamp(0) - '1 second'::interval \ + WHERE nasipaddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}'" + +# +# This query expires all IP addresses allocated to a NAS when an +# accounting OFF record arrives from that NAS +# +off_clear = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = 'now'::timestamp(0) - '1 second'::interval \ + WHERE nasipaddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}'" diff --git a/raddb/mods-config/sql/ippool/postgresql/schema.sql b/raddb/mods-config/sql/ippool/postgresql/schema.sql new file mode 100644 index 0000000..1ef57b7 --- /dev/null +++ b/raddb/mods-config/sql/ippool/postgresql/schema.sql @@ -0,0 +1,22 @@ +-- +-- Table structure for table 'radippool' +-- +-- See also "procedure.sql" in this directory for additional +-- indices and a stored procedure that is much faster. +-- + +CREATE TABLE radippool ( + id BIGSERIAL PRIMARY KEY, + pool_name text NOT NULL, + FramedIPAddress INET NOT NULL, + NASIPAddress text NOT NULL default '', + pool_key text NOT NULL default '', + CalledStationId text NOT NULL default '', + CallingStationId text NOT NULL default ''::text, + expiry_time TIMESTAMP(0) without time zone NOT NULL default NOW(), + username text DEFAULT ''::text +); + +CREATE INDEX radippool_poolname_expire ON radippool USING btree (pool_name, expiry_time); +CREATE INDEX radippool_framedipaddress ON radippool USING btree (framedipaddress); +CREATE INDEX radippool_nasip_poolkey_ipaddress ON radippool USING btree (nasipaddress, pool_key, framedipaddress); diff --git a/raddb/mods-config/sql/ippool/sqlite/queries.conf b/raddb/mods-config/sql/ippool/sqlite/queries.conf new file mode 100644 index 0000000..46ce58e --- /dev/null +++ b/raddb/mods-config/sql/ippool/sqlite/queries.conf @@ -0,0 +1,148 @@ +# -*- text -*- +# +# ippool/sqlite/queries.conf -- SQLite queries for rlm_sqlippool +# +# $Id$ + +# +# SQLite does not implement SELECT FOR UPDATE which is normally used to place +# an exclusive lock over rows to prevent the same address from being +# concurrently selected for allocation to multiple users. +# +# The most granular read-blocking lock that SQLite has is an exclusive lock +# over the database, so that's what we use. All locking in SQLite is performed +# over the entire database and we perform a row update for any IP that we +# allocate, requiring an exclusive lock. Taking the exclusive lock from the +# start of the transaction (even if it were not required to guard the SELECT) +# is actually quicker than if we deferred it causing SQLite to "upgrade" the +# automatic shared lock for the transaction to an exclusive lock for the +# subsequent UPDATE. +# +allocate_begin = "BEGIN EXCLUSIVE" +allocate_commit = "COMMIT" + +# +# This series of queries allocates an IP address +# + +# +# Attempt to allocate the address a client previously had. This is based on pool_key +# and nasipaddress. Change the criteria if the identifier for "stickyness" is different. +# If different criteria are used, check the indexes on the IP pool table to ensure the fields +# are appropriately indexed. To disable stickyness comment out this query. +# +allocate_existing = "\ + SELECT framedipaddress \ + FROM ${ippool_table} \ + WHERE pool_name = '%{control:${pool_name}}' \ + AND nasipaddress = '%{NAS-IP-Address}' AND pool_key = '${pool_key}' \ + ORDER BY expiry_time DESC \ + LIMIT 1" + +# +# Find a free IP address from the pool, choosing the oldest expired one. +# +allocate_find = "\ + SELECT framedipaddress \ + FROM ${ippool_table} \ + WHERE pool_name = '%{control:${pool_name}}' \ + expiry_time < datetime('now') \ + ORDER BY expiry_time \ + LIMIT 1" + +# +# If you prefer to allocate a random IP address every time, i +# use this query instead +# Note: This is very slow if you have a lot of free IPs. +# + +#allocate_find = "\ +# SELECT framedipaddress \ +# FROM ${ippool_table} \ +# WHERE pool_name = '%{control:${pool_name}}' \ +# AND expiry_time IS NULL \ +# ORDER BY RAND() \ +# LIMIT 1" + +# +# If an IP could not be allocated, check to see if the pool exists or not +# This allows the module to differentiate between a full pool and no pool +# Note: If you are not running redundant pool modules this query may be +# commented out to save running this query every time an ip is not allocated. +# +pool_check = "\ + SELECT id \ + FROM ${ippool_table} \ + WHERE pool_name='%{control:${pool_name}}' \ + LIMIT 1" + +# +# This is the final IP Allocation query, which saves the allocated ip details +# +allocate_update = "\ + UPDATE ${ippool_table} \ + SET \ + nasipaddress = '%{NAS-IP-Address}', \ + pool_key = '${pool_key}', \ + callingstationid = '%{Calling-Station-Id}', \ + username = '%{User-Name}', \ + expiry_time = datetime(strftime('%%s', 'now') + ${lease_duration}, 'unixepoch') \ + WHERE framedipaddress = '%I'" + +# +# Extend an IP expiry time when an accounting START record arrives +# +start_update = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = datetime(strftime('%%s', 'now') + ${lease_duration}, 'unixepoch') \ + WHERE nasipaddress = '%{NAS-IP-Address}' \ + AND pool_key = '${pool_key}' \ + AND username = '%{User-Name}' \ + AND callingstationid = '%{Calling-Station-Id}' \ + AND framedipaddress = '%{${attribute_name}}'" + +# +# Expire an IP when an accounting STOP record arrives +# +stop_clear = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = datetime('now') \ + WHERE nasipaddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}' \ + AND pool_key = '${pool_key}' \ + AND username = '%{User-Name}' \ + AND callingstationid = '%{Calling-Station-Id}' \ + AND framedipaddress = '%{${attribute_name}}'" + +# +# Update the expiry time for an IP when an accounting ALIVE record arrives +# +alive_update = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = datetime(strftime('%%s', 'now') + ${lease_duration}, 'unixepoch') \ + WHERE nasipaddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}' \ + AND pool_key = '${pool_key}' \ + AND username = '%{User-Name}' \ + AND callingstationid = '%{Calling-Station-Id}' \ + AND framedipaddress = '%{${attribute_name}}'" + +# +# Expires all IPs allocated to a NAS when an accounting ON record arrives +# +on_clear = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = datetime('now') \ + WHERE nasipaddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}'" + +# +# Expires all IPs allocated to a NAS when an accounting OFF record arrives +# +off_clear = "\ + UPDATE ${ippool_table} \ + SET \ + expiry_time = datetime('now') \ + WHERE nasipaddress = '%{%{Nas-IP-Address}:-%{Nas-IPv6-Address}}'" + diff --git a/raddb/mods-config/sql/ippool/sqlite/schema.sql b/raddb/mods-config/sql/ippool/sqlite/schema.sql new file mode 100644 index 0000000..b020c62 --- /dev/null +++ b/raddb/mods-config/sql/ippool/sqlite/schema.sql @@ -0,0 +1,18 @@ +-- +-- Table structure for table 'radippool' +-- +CREATE TABLE radippool ( + id int(11) PRIMARY KEY, + pool_name varchar(30) NOT NULL, + framedipaddress varchar(15) NOT NULL default '', + nasipaddress varchar(15) NOT NULL default '', + calledstationid VARCHAR(30) NOT NULL default '', + callingstationid VARCHAR(30) NOT NULL default '', + expiry_time DATETIME NOT NULL default (DATETIME('now')), + username varchar(64) NOT NULL default '', + pool_key varchar(30) NOT NULL default '' +); + +CREATE INDEX radippool_poolname_expire ON radippool(pool_name, expiry_time); +CREATE INDEX radippool_framedipaddress ON radippool(framedipaddress); +CREATE INDEX radippool_nasip_poolkey_ipaddress ON radippool(nasipaddress, pool_key, framedipaddress); diff --git a/raddb/mods-config/sql/main/mongo/queries.conf b/raddb/mods-config/sql/main/mongo/queries.conf new file mode 100644 index 0000000..732e1e8 --- /dev/null +++ b/raddb/mods-config/sql/main/mongo/queries.conf @@ -0,0 +1,289 @@ +# -*- text -*- +# +# main/mongo/queries.conf -- Mongo configuration queries +# +# Note that as Mongo is a "schemaless" database, there is no +# default schema. +# +# Note also that the Mongo driver is a work in progress. If it works +# for you, great. If the queries do not work, please send a patch. +# But the FreeRADIUS team are not experts in Mongo, and cannot help +# with creating Mongo queries. +# +# $Id$ + +####################################################################### +# Query config: Username +####################################################################### +# This is the username that will get substituted, escaped, and added +# as attribute 'SQL-User-Name'. '%{SQL-User-Name}' should be used +# below everywhere a username substitution is needed so you you can +# be sure the username passed from the client is escaped properly. +# +# Uncomment the next line, if you want the sql_user_name to mean: +# +# Use Stripped-User-Name, if it's there. +# Else use User-Name, if it's there, +# Else use hard-coded string "none" as the user name. +# +#sql_user_name = "%{%{Stripped-User-Name}:-%{%{User-Name}:-none}}" + +sql_user_name = "%{User-Name}" + +####################################################################### +# Authorization Queries +####################################################################### +# These queries compare the check items for the user +# in ${authcheck_table} and setup the reply items in +# ${authreply_table}. You can use any query/tables +# you want, but the return data for each row MUST +# be in the following order: +# +# 0. Row ID (currently unused) +# 1. UserName/GroupName +# 2. Item Attr Name +# 3. Item Attr Value +# 4. Item Attr Operation +####################################################################### + +# +# Aggregate query that return like for SQL standard N rows with columns <id>,<username>,<attribute>,<value>,<op> +# +# Example of Result: +# +# { "id" : 0, "username": "bob", "attribute" : "User-Name", "Value" : "pippo", "op" : "==" } +# { "id" : 0, "username": "bob", "attribute" : "ClearText-Password", "value" : "pwd1", "op" : ":=" } +# { "id" : 0, "username": "bob", "attribute" : "Cache-TTL", "value" : 1000, "op" : ":=" } +# +authorize_check_query = "db.${authcheck_table}.aggregate([ \ + { \ + '$match': { \ + 'calling_station_id': '%{Calling-Station-id}', \ + 'auth_blocked': 'false' \ + } \ + }, \ + { \ + '$addFields': { \ + 'attributes.User-Name': '$usr', \ + 'attributes.ClearText-Password': '$pwd', \ + 'attributes.Cache-TTL': '$ttlcache', \ + 'attributes.Enable-Roaming': '$roaming', \ + 'attributes.Pool-Name': '$pool_name' \ + } \ + }, \ + { \ + '$project': { \ + 'calling_station_id': 1, \ + 'attributes': { \ + '$objectToArray': '$attributes' \ + } \ + } \ + }, \ + { \ + '$unwind': '$attributes' \ + }, \ + { \ + '$project': { \ + '_id': 0, \ + 'username': '', \ + 'attribute': '$attributes.k', \ + 'value': '$attributes.v', \ + 'op': ':=' \ + } \ + } \ +])" \ + +# TBD: fill in things here +authorize_reply_query = "" + +################################################################## + +# +# TBD: fill in things here +# +#authorize_group_check_query = "" +#authorize_group_reply_query = "" + +####################################################################### +# Group Membership Queries +####################################################################### +# group_membership_query - Check user group membership +####################################################################### + +# +# TBD: Fill in things here. +# +#group_membership_query = "" + + +####################################################################### +# Accounting and Post-Auth Queries +####################################################################### +# These queries insert/update accounting and authentication records. +# The query to use is determined by the value of 'reference'. +# This value is used as a configuration path and should resolve to one +# or more 'query's. If reference points to multiple queries, and a query +# fails, the next query is executed. +# +# Behaviour is identical to the old 1.x/2.x module, except we can now +# fail between N queries, and query selection can be based on any +# combination of attributes, or custom 'Acct-Status-Type' values. +####################################################################### + +accounting { + reference = "%{tolower:type.%{Acct-Status-Type}.query}" + + type { + + start { + query = "db.connections.findAndModify({ \ + 'query': { \ + 'calling_station_id': '%{Calling-Station-Id}', \ + 'pgw_node': '%{NAS-Identifier}', \ + 'acct_session_id': '%{Acct-Session-Id}', \ + }, \ + 'update': { \ + '$set': { \ + 'update_date': { '$date': { '$numberLong': '%{expr: (%l * 1000) + (%M / 1000)}' } }, \ + 'ip': '%{Framed-IP-Address}', \ + 'start_time': '%{Packet-Original-Timestamp}', \ + }, \ + '$push': { \ + 'events_data': { \ + 'event_id': '%{sha256:%{tolower:%{Calling-Station-Id}', \ + 'event_type': 'Accounting-Start', \ + 'event_time': '%{Packet-Original-Timestamp}', \ + 'creation_date': { '$date': { '$numberLong': '%{expr: (%l * 1000) + (%M / 1000)}' } } \ + } \ + }, \ + '$setOnInsert': { \ + 'pool_name': '%{Control:Pool-Name}', \ + 'ip': '%{Framed-IP-Address}', \ + 'closed': false, \ + 'update_counter': 0, \ + 'creation_date': { '$date': { '$numberLong': '%{expr: (%l * 1000) + (%M / 1000)}' } } \ + } \ + }, \ + 'upsert': true \ + })" + + query = "db.simultaneous_connections.findAndModify({ \ + 'query': { \ + 'pool_name': '%{Control:Pool-Name}' \ + }, \ + 'update': { \ + '$inc': { \ + 'conns_counter': 1 \ + } \ + '$setOnInsert': { \ + 'pool_name': '%{Control:Pool-Name}', \ + 'conns_counter': 1 \ + }, \ + }, \ + 'upsert': true \ + })" + # End Start + } + + interim-update { + query = "db.connections.findAndModify({ \ + 'query': { \ + 'calling_station_id': '%{Calling-Station-Id}', \ + 'pgw_node': '%{NAS-Identifier}', \ + 'acct_session_id': '%{Acct-Session-Id}' \ + }, \ + 'update': { \ + '$set': { \ + 'update_date': { '$date': { '$numberLong': '%{expr: (%l * 1000) + (%M / 1000)}' } }, \ + 'last_upd_interim': '%{Packet-Original-Timestamp}' \ + }, \ + '$inc': { \ + 'update_counter': 1 \ + }, \ + '$push': { \ + 'events_data': { \ + 'event_id': '%{sha256:%{tolower:%{Calling-Station-Id}', \ + 'event_type': 'Accounting-Interim-Update', \ + 'event_time': '%{Packet-Original-Timestamp}', \ + 'creation_date': { '$date': { '$numberLong': '%{expr: (%l * 1000) + (%M / 1000)}' } } \ + } \ + }, \ + '$setOnInsert': { \ + 'pool_name': '%{Control:Pool-Name}', \ + 'ip': '%{Framed-IP-Address}', \ + 'closed': false, \ + 'creation_date': { '$date': { '$numberLong': '%{expr: (%l * 1000) + (%M / 1000)}' } } \ + } \ + }, + 'upsert': true \ + })" + # End Interim-Update + } + + stop { + query = "db.connections.findAndModify({ \ + 'query': { \ + 'calling_station_id': '%{Calling-Station-Id}', \ + 'pgw_node': '%{NAS-Identifier}', \ + 'acct_session_id': '%{Acct-Session-Id}' \ + }, \ + 'update': { \ + '$set': { \ + 'update_date': { '$date': { '$numberLong': '%{expr: (%l * 1000) + (%M / 1000)}' } }, \ + 'stop_time': '%{Packet-Original-Timestamp}', \ + 'closed': true \ + }, \ + '$push': { \ + 'events_data': { \ + 'event_id': '%{sha256:%{tolower:%{Calling-Station-Id}', \ + 'event_type': 'Accounting-Stop', \ + 'event_time': '%{Packet-Original-Timestamp}', \ + 'creation_date': { '$date': { '$numberLong': '%{expr: (%l * 1000) + (%M / 1000)}' } } \ + } \ + }, \ + '$setOnInsert': { \ + 'pool_name': '%{Control:Pool-Name}', \ + 'ip': '%{Framed-IP-Address}', \ + 'update_counter': 0, \ + 'creation_date': { '$date': { '$numberLong': '%{expr: (%l * 1000) + (%M / 1000)}' } } \ + } \ + }, \ + 'upsert': true \ + })" + + # End Stop + } + + } +} + + +####################################################################### +# Authentication Logging Queries +####################################################################### +# postauth_query - Insert some info after authentication +####################################################################### + +post-auth { + query = "db.post_auth.findAndModify({ \ + 'query': { \ + 'calling_station_id': '%{Calling-Station-Id}', \ + 'nas_ip': '%{NAS-Identifier}' \ + }, \ + 'update': { \ + '$set': { \ + 'update_date': { '$date': { '$numberLong': '%{expr: (%l * 1000) + (%M / 1000)}' } }, \ + 'last_event_ts': '%{Packet-Original-Timestamp}' \ + }, \ + '$inc': { \ + 'reject_counter': 1 \ + }, \ + '$setOnInsert': { \ + 'calling_station_id': '%{Calling-Station-Id}', \ + 'nas_ip': '%{NAS-Identifier}', \ + 'creation_date': { '$date': { '$numberLong': '%{expr: (%l * 1000) + (%M / 1000)}' } } \ + } \ + }, \ + 'upsert': true \ + })" +} diff --git a/raddb/mods-config/sql/main/mssql/process-radacct.sql b/raddb/mods-config/sql/main/mssql/process-radacct.sql new file mode 100644 index 0000000..a3a6445 --- /dev/null +++ b/raddb/mods-config/sql/main/mssql/process-radacct.sql @@ -0,0 +1,151 @@ +# -*- text -*- +# +# main/mssql/process-radacct.sql -- Schema extensions for processing radacct entries +# +# $Id$ + +-- --------------------------------- +-- - Per-user data usage over time - +-- --------------------------------- +-- +-- An extension to the standard schema to hold per-user data usage statistics +-- for arbitrary periods. +-- +-- The data_usage_by_period table is populated by periodically calling the +-- fr_new_data_usage_period stored procedure. +-- +-- This table can be queried in various ways to produce reports of aggregate +-- data use over time. For example, if the fr_new_data_usage_period SP is +-- invoked one per day just after midnight, to produce usage data with daily +-- granularity, then a reasonably accurate monthly bandwidth summary for a +-- given user could be obtained with: +-- +-- SELECT +-- FORMAT(period_start, 'yyyy-MMMM') AS month, +-- SUM(acctinputoctets)/1000/1000/1000 AS GB_in, +-- SUM(acctoutputoctets)/1000/1000/1000 AS GB_out +-- FROM +-- data_usage_by_period +-- WHERE +-- username='bob' AND +-- period_end <> 0 +-- GROUP BY +-- FORMAT(period_start, 'yyyy-MMMM'); +-- +-- +----------------+----------+-----------+ +-- | month | GB_in | GB_out | +-- +----------------+----------+-----------+ +-- | 2019-July | 5.782279 | 50.545664 | +-- | 2019-August | 4.230543 | 48.523096 | +-- | 2019-September | 4.847360 | 48.631835 | +-- | 2019-October | 6.456763 | 51.686231 | +-- | 2019-November | 6.362537 | 52.385710 | +-- | 2019-December | 4.301524 | 50.762240 | +-- | 2020-January | 5.436280 | 49.067775 | +-- +----------------+----------+-----------+ +-- +CREATE TABLE data_usage_by_period ( + username VARCHAR(64) NOT NULL, + period_start DATETIME NOT NULL, + period_end DATETIME NOT NULL, + acctinputoctets NUMERIC(19), + acctoutputoctets NUMERIC(19), + PRIMARY KEY (username, period_start) +); +GO + +CREATE INDEX idx_data_usage_by_period_period_end ON data_usage_by_period(period_end); +GO + +-- +-- Stored procedure that when run with some arbitrary frequency, say +-- once per day by cron, will process the recent radacct entries to extract +-- time-windowed data containing acct{input,output}octets ("data usage") per +-- username, per period. +-- +-- Each invocation will create new rows in the data_usage_by_period tables +-- containing the data used by each user since the procedure was last invoked. +-- The intervals do not need to be identical but care should be taken to +-- ensure that the start/end of each period aligns well with any intended +-- reporting intervals. +-- +-- It can be invoked by running: +-- +-- EXEC fr_new_data_usage_period; +-- +-- +CREATE OR ALTER PROCEDURE fr_new_data_usage_period +AS +BEGIN + + DECLARE @v_start DATETIME; + DECLARE @v_end DATETIME; + + SELECT @v_start = COALESCE(DATEADD(ss, 1, MAX(period_end)), CAST('1970-01-01' AS DATETIME)) FROM data_usage_by_period; + SELECT @v_end = CAST(CURRENT_TIMESTAMP AS DATETIME2(0)); + + BEGIN TRAN; + + -- + -- Add the data usage for the sessions that were active in the current + -- period to the table. Include all sessions that finished since the start + -- of this period as well as those still ongoing. + -- + MERGE INTO data_usage_by_period d + USING ( + SELECT + username, + @v_start AS period_start, + @v_end AS period_end, + SUM(acctinputoctets) AS acctinputoctets, + SUM(acctoutputoctets) AS acctoutputoctets + FROM + radacct + WHERE + acctstoptime > @v_start OR + acctstoptime=0 + GROUP BY + username + ) s + ON ( d.username = s.username AND d.period_start = s.period_start ) + WHEN MATCHED THEN + UPDATE SET + acctinputoctets = d.acctinputoctets + s.acctinputoctets, + acctoutputoctets = d.acctoutputoctets + s.acctoutputoctets, + period_end = @v_end + WHEN NOT MATCHED THEN + INSERT + (username, period_start, period_end, acctinputoctets, acctoutputoctets) + VALUES + (s.username, s.period_start, s.period_end, s.acctinputoctets, s.acctoutputoctets); + + -- + -- Create an open-ended "next period" for all ongoing sessions and carry a + -- negative value of their data usage to avoid double-accounting when we + -- process the next period. Their current data usage has already been + -- allocated to the current and possibly previous periods. + -- + -- MSSQL doesn't allow a DATETIME to be NULL so we use "0" (1900-01-01) to + -- indicate the open-ended interval. + -- + INSERT INTO data_usage_by_period (username, period_start, period_end, acctinputoctets, acctoutputoctets) + SELECT * + FROM ( + SELECT + username, + DATEADD(ss,1,@v_end) AS period_start, + 0 AS period_end, + 0 - SUM(acctinputoctets) AS acctinputoctets, + 0 - SUM(acctoutputoctets) AS acctoutputoctets + FROM + radacct + WHERE + acctstoptime=0 + GROUP BY + username + ) s; + + COMMIT; + +END +GO diff --git a/raddb/mods-config/sql/main/mssql/queries.conf b/raddb/mods-config/sql/main/mssql/queries.conf new file mode 100644 index 0000000..5518edd --- /dev/null +++ b/raddb/mods-config/sql/main/mssql/queries.conf @@ -0,0 +1,611 @@ +# -*- text -*- +# +# main/mssql/queries.conf -- MSSQL configuration for default schema (schema.sql) +# +# $Id$ + +# Safe characters list for sql queries. Everything else is replaced +# with their mime-encoded equivalents. +# The default list should be ok +#safe_characters = "@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_: /" + +####################################################################### +# Query config: Username +####################################################################### +# This is the username that will get substituted, escaped, and added +# as attribute 'SQL-User-Name'. '%{SQL-User-Name}' should be used +# below everywhere a username substitution is needed so you you can +# be sure the username passed from the client is escaped properly. +# +# Uncomment the next line, if you want the sql_user_name to mean: +# +# Use Stripped-User-Name, if it's there. +# Else use User-Name, if it's there, +# Else use hard-coded string "none" as the user name. +#sql_user_name = "%{%{Stripped-User-Name}:-%{%{User-Name}:-none}}" +# +sql_user_name = "%{User-Name}" + +####################################################################### +# Query config: Event-Timestamp +####################################################################### +# event_timestamp_epoch is the basis for the time inserted into +# accounting records. Typically this will be the Event-Timestamp of the +# accounting request, which is usually provided by a NAS. +# +# Uncomment the next line, if you want the timestamp to be based on the +# request reception time recorded by this server, for example if you +# distrust the provided Event-Timestamp. +#event_timestamp_epoch = "%l" + +event_timestamp_epoch = "%{%{integer:Event-Timestamp}:-%l}" + +# event_timestamp is the SQL snippet for converting an epoch timestamp +# to an SQL date. + +event_timestamp = "DATEADD(SS, ${event_timestamp_epoch}, '19700101')" + +####################################################################### +# Query config: Class attribute +####################################################################### +# +# 3.0.22 and later have a "class" column in the accounting table. +# +# However, we do NOT want to break existing configurations by adding +# the Class attribute to the default queries. If we did that, then +# systems using newer versions of the server would fail, because +# there is no "class" column in their accounting tables. +# +# The solution to that is the following "class" subsection. If your +# database has a "class" column for the various tables, then you can +# uncomment the configuration items here. The queries below will +# then automatically insert the Class attribute into radacct, +# radpostauth, etc. +# +class { + # + # Delete the '#' character from each of the configuration + # items in this section. This change puts the Class + # attribute into the various tables. Leave the double-quoted + # string there, as the value for the configuration item. + # + # See also policy.d/accounting, and the "insert_acct_class" + # policy. You will need to list (or uncomment) + # "insert_acct_class" in the "post-auth" section in order to + # create a Class attribute. + # + column_name = # ", class" + packet_xlat = # ", '%{Class}'" + reply_xlat = # ", '%{reply:Class}'" +} + +####################################################################### +# Authorization Queries +####################################################################### +# These queries compare the check items for the user +# in ${authcheck_table} and setup the reply items in +# ${authreply_table}. You can use any query/tables +# you want, but the return data for each row MUST +# be in the following order: +# +# 0. Row ID (currently unused) +# 1. UserName/GroupName +# 2. Item Attr Name +# 3. Item Attr Value +# 4. Item Attr Operation +####################################################################### +# Query for case sensitive usernames was removed. Please contact with me, +# if you know analog of STRCMP functions for MS SQL. + +authorize_check_query = "\ + SELECT id, UserName, Attribute, Value, op \ + FROM ${authcheck_table} \ + WHERE Username = '%{SQL-User-Name}' \ + ORDER BY id" + +authorize_reply_query = "\ + SELECT id, UserName, Attribute, Value, op \ + FROM ${authreply_table} \ + WHERE Username = '%{SQL-User-Name}' \ + ORDER BY id" + +authorize_group_check_query = "\ + SELECT \ + ${groupcheck_table}.id,${groupcheck_table}.GroupName, \ + ${groupcheck_table}.Attribute,${groupcheck_table}.Value, \ + ${groupcheck_table}.op \ + FROM ${groupcheck_table},${usergroup_table} \ + WHERE ${usergroup_table}.Username = '%{SQL-User-Name}' \ + AND ${usergroup_table}.GroupName = ${groupcheck_table}.GroupName \ + ORDER BY ${groupcheck_table}.id" + +authorize_group_reply_query = "\ + SELECT \ + ${groupreply_table}.id, ${groupreply_table}.GroupName, \ + ${groupreply_table}.Attribute,${groupreply_table}.Value, \ + ${groupreply_table}.op \ + FROM ${groupreply_table},${usergroup_table} \ + WHERE ${usergroup_table}.Username = '%{SQL-User-Name}' \ + AND ${usergroup_table}.GroupName = ${groupreply_table}.GroupName \ + ORDER BY ${groupreply_table}.id" + +group_membership_query = "\ + SELECT groupname \ + FROM ${usergroup_table} \ + WHERE username = '%{SQL-User-Name}' \ + ORDER BY priority" + +####################################################################### +# Simultaneous Use Checking Queries +####################################################################### +# simul_count_query - query for the number of current connections +# - If this is not defined, no simultaneous use checking +# - will be performed by this module instance +# simul_verify_query - query to return details of current connections +# for verification +# - Leave blank or commented out to disable verification step +# - Note that the returned field order should not be changed. +####################################################################### + +simul_count_query = "\ + SELECT COUNT(*) \ + FROM ${acct_table1} \ + WHERE UserName = '%{SQL-User-Name}' \ + AND AcctStopTime = 0" + +simul_verify_query = "\ + SELECT \ + RadAcctId, AcctSessionId, UserName, NASIPAddress, NASPortId, FramedIPAddress, \ + CallingStationId, FramedProtocol \ + FROM ${acct_table1} \ + WHERE UserName = '%{SQL-User-Name}' \ + AND AcctStopTime = 0" + +####################################################################### +# Accounting and Post-Auth Queries +####################################################################### +# These queries insert/update accounting and authentication records. +# The query to use is determined by the value of 'reference'. +# This value is used as a configuration path and should resolve to one +# or more 'query's. If reference points to multiple queries, and a query +# fails, the next query is executed. +# +# Behaviour is identical to the old 1.x/2.x module, except we can now +# fail between N queries, and query selection can be based on any +# combination of attributes, or custom 'Acct-Status-Type' values. +####################################################################### +accounting { + reference = "%{tolower:type.%{%{Acct-Status-Type}:-%{Request-Processing-Stage}}.query}" + + # Write SQL queries to a logfile. This is potentially useful for bulk inserts + # when used with the rlm_sql_null driver. +# logfile = ${logdir}/accounting.sql + + type { + accounting-on { + query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctStopTime=${....event_timestamp}, \ + AcctSessionTime=${....event_timestamp_epoch} - \ + DATEDIFF(SS, '1970-01-01', AcctStartTime), \ + AcctTerminateCause='%{%{Acct-Terminate-Cause}:-NAS-Reboot}', \ + AcctStopDelay = %{%{Acct-Delay-Time}:-0} \ + WHERE AcctStopTime = 0 \ + AND NASIPAddress = '%{NAS-IP-Address}' \ + AND AcctStartTime <= ${....event_timestamp}" + } + + accounting-off { + query = "${..accounting-on.query}" + } + + # + # Implement the "sql_session_start" policy. + # See raddb/policy.d/accounting for more details. + # + # You also need to fix the other queries as + # documented below. Look for "sql_session_start". + # + post-auth { + query = "\ + INSERT INTO ${....acct_table1} \ + INSERT INTO ${....acct_table1} ( \ + AcctSessionId, \ + AcctUniqueId, \ + UserName, \ + Realm, \ + NASIPAddress, \ + NASPortId, \ + NASPortType, \ + AcctStartTime, \ + AcctSessionTime, \ + AcctAuthentic, \ + ConnectInfo_start, \ + ConnectInfo_stop, \ + AcctInputOctets, \ + AcctOutputOctets, \ + CalledStationId, \ + CallingStationId, \ + AcctTerminateCause, \ + ServiceType, \ + FramedProtocol, \ + FramedIPAddress, \ + FramedIPv6Address, \ + FramedIPv6Prefix, \ + FramedInterfaceId, \ + DelegatedIPv6Prefix \ + ${....class.column_name}) \ + VALUES(\ + '%{Acct-Session-Id}', \ + '%{Acct-Unique-Session-Id}', \ + '%{SQL-User-Name}', \ + '%{Realm}', \ + '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}', \ + '%{%{NAS-Port-ID}:-%{NAS-Port}}', \ + '%{NAS-Port-Type}', \ + '%S', \ + 0, \ + '', \ + '%{Connect-Info}', \ + '', \ + 0, \ + 0, \ + '%{Called-Station-Id}', \ + '%{Calling-Station-Id}', \ + '', \ + '%{Service-Type}', \ + '', \ + '', \ + '', \ + '', \ + '', \ + '' \ + ${....class.packet_xlat})" + + query = "\ + UPDATE ${....acct_table1} SET \ + AcctStartTime = '%S', \ + ConnectInfo_start = '%{Connect-Info}', \ + AcctSessionId = '%{Acct-Session-Id}' \ + WHERE UserName = '%{SQL-User-Name}' \ + AND NASIPAddress = '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}' \ + AND NASPortId = '%{%{NAS-Port-ID}:-%{NAS-Port}}' \ + AND NASPortType = '%{NAS-Port-Type}' \ + AND AcctStopTime = 0" + } + + start { + query = "\ + INSERT INTO ${....acct_table1} ( \ + AcctSessionId, \ + AcctUniqueId, \ + UserName, \ + Realm, \ + NASIPAddress, \ + NASPortId, \ + NASPortType, \ + AcctStartTime, \ + AcctSessionTime, \ + AcctAuthentic, \ + ConnectInfo_start, \ + ConnectInfo_stop, \ + AcctInputOctets, \ + AcctOutputOctets, \ + CalledStationId, \ + CallingStationId, \ + AcctTerminateCause, \ + ServiceType, \ + FramedProtocol, \ + FramedIPAddress, \ + FramedIPv6Address, \ + FramedIPv6Prefix, \ + FramedInterfaceId, \ + DelegatedIPv6Prefix, \ + AcctStartDelay, \ + AcctStopDelay \ + ${....class.column_name}) \ + VALUES(\ + '%{Acct-Session-Id}', \ + '%{Acct-Unique-Session-Id}', \ + '%{SQL-User-Name}', \ + '%{Realm}', \ + '%{NAS-IP-Address}', \ + '%{%{NAS-Port-ID}:-%{NAS-Port}}', \ + '%{NAS-Port-Type}', \ + ${....event_timestamp}, \ + '0', \ + '%{Acct-Authentic}', \ + '%{Connect-Info}', \ + '', \ + '0', \ + '0', \ + '%{Called-Station-Id}', \ + '%{Calling-Station-Id}', \ + '', \ + '%{Service-Type}', \ + '%{Framed-Protocol}', \ + '%{Framed-IP-Address}', \ + '%{Framed-IPv6-Address}', \ + '%{Framed-IPv6-Prefix}', \ + '%{Framed-Interface-Id}', \ + '%{Delegated-IPv6-Prefix}', \ + '%{Acct-Delay-Time}', \ + '0' \ + ${....class.packet_xlat})" + + # + # When using "sql_session_start", you should comment out + # the previous query, and enable this one. + # + # Just change the previous query to "-query", + # and this one to "query". The previous one + # will be ignored, and this one will be + # enabled. + # + -query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctSessionId = '%{Acct-Session-Id}', \ + AcctUniqueId = '%{Acct-Unique-Session-Id}', \ + AcctAuthentic = '%{Acct-Authentic}', \ + ConnectInfo_start = '%{Connect-Info}', \ + ServiceType = '%{Service-Type}', \ + FramedProtocol = '%{Framed-Protocol}', \ + FramedIpAddress = '%{Framed-IP-Address}', \ + FramedIpv6Address = '%{Framed-IPv6-Address}', \ + FramedIpv6Prefix = '%{Framed-IPv6-Prefix}', \ + FramedInterfaceId = '%{Framed-Interface-Id}', \ + DelegatedIpv6Prefix = '%{Delegated-IPv6-Prefix}', \ + AcctStartTime = '%S' \ + WHERE UserName = '%{SQL-User-Name}' \ + AND NASIPAddress = '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}' \ + AND NASPortId = '%{%{NAS-Port-ID}:-%{NAS-Port}}' \ + AND NASPortType = '%{NAS-Port-Type}' \ + AND AcctStopTime = 0" + + query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctStartTime = ${....event_timestamp}, \ + AcctStartDelay = '%{%{Acct-Delay-Time}:-0}', \ + ConnectInfo_start = '%{Connect-Info}' \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-ID}' \ + AND AcctStopTime = 0" + } + + interim-update { + query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctInterval = DATEDIFF(second, CASE WHEN AcctUpdateTime > 0 THEN AcctUpdateTime ELSE AcctStartTime END, ${....event_timestamp}), \ + AcctUpdateTime = ${....event_timestamp}, \ + AcctSessionTime = '%{Acct-Session-Time}', \ + AcctInputOctets = convert(bigint, '%{%{Acct-Input-Gigawords}:-0}' * POWER(2.0, 32)) | '%{%{Acct-Input-Octets}:-0}', \ + AcctOutputOctets = convert(bigint, '%{%{Acct-Output-Gigawords}:-0}' * POWER(2.0, 32)) | '%{%{Acct-Output-Octets}:-0}', \ + FramedIPAddress = '%{Framed-IP-Address}', \ + FramedIPv6Address = '%{Framed-IPv6-Address}', \ + FramedIPv6Prefix = '%{Framed-IPv6-Prefix}', \ + FramedInterfaceId = '%{Framed-Interface-Id}', \ + DelegatedIPv6Prefix = '%{Delegated-IPv6-Prefix}' \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-ID}' \ + AND AcctStopTime = 0" + + query = "\ + INSERT INTO ${....acct_table1} ( \ + AcctSessionId, \ + AcctUniqueId, \ + UserName, \ + Realm, \ + NASIPAddress, \ + NASPortId, \ + NASPortType, \ + AcctStartTime, \ + AcctUpdateTime, \ + AcctSessionTime, \ + AcctAuthentic, \ + ConnectInfo_start, \ + AcctInputOctets, \ + AcctOutputOctets, \ + CalledStationId, \ + CallingStationId, \ + ServiceType, \ + FramedProtocol, \ + FramedIPAddress, \ + FramedIPv6Address, \ + FramedIPv6Prefix, \ + FramedInterfaceId, \ + DelegatedIPv6Prefix, \ + AcctStartDelay \ + ${....class.column_name}) \ + VALUES(\ + '%{Acct-Session-Id}', \ + '%{Acct-Unique-Session-Id}', \ + '%{SQL-User-Name}', \ + '%{Realm}', \ + '%{NAS-IP-Address}', \ + '%{%{NAS-Port-ID}:-%{NAS-Port}}', \ + '%{NAS-Port-Type}', \ + ${....event_timestamp}, \ + ${....event_timestamp}, \ + '%{Acct-Session-Time}', \ + '%{Acct-Authentic}', \ + '', \ + '%{Acct-Input-Octets}', \ + '%{Acct-Output-Octets}', \ + '%{Called-Station-Id}', \ + '%{Calling-Station-Id}', \ + '%{Service-Type}', \ + '%{Framed-Protocol}', \ + '%{Framed-IP-Address}', \ + '%{Framed-IPv6-Address}', \ + '%{Framed-IPv6-Prefix}', \ + '%{Framed-Interface-Id}', \ + '%{Delegated-IPv6-Prefix}', \ + '0' \ + ${....class.packet_xlat})" + + # + # When using "sql_session_start", you should comment out + # the previous query, and enable this one. + # + # Just change the previous query to "-query", + # and this one to "query". The previous one + # will be ignored, and this one will be + # enabled. + # + -query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctSessionId = '%{Acct-Session-Id}', \ + AcctUniqueId = '%{Acct-Unique-Session-Id}', \ + AcctAuthentic = '%{Acct-Authentic}', \ + ConnectInfo_start = '%{Connect-Info}', \ + ServiceType = '%{Service-Type}', \ + FramedProtocol = '%{Framed-Protocol}', \ + FramedIPAddress = '%{Framed-IP-Address}', \ + FramedIPv6Address = '%{Framed-IPv6-Address}', \ + FramedIPv6Prefix = '%{Framed-IPv6-Prefix}', \ + FramedInterfaceId = '%{Framed-Interface-Id}', \ + DelegatedIPv6Prefix = '%{Delegated-IPv6-Prefix}', \ + AcctInputOctets = convert(bigint, '%{%{Acct-Input-Gigawords}:-0}' * POWER(2.0, 32)) | '%{%{Acct-Input-Octets}:-0}', \ + AcctOutputOctets = convert(bigint, '%{%{Acct-Output-Gigawords}:-0}' * POWER(2.0, 32)) | '%{%{Acct-Output-Octets}:-0}' \ + WHERE UserName = '%{SQL-User-Name}' \ + AND NASIPAddress = '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}' \ + AND NASPortId = '%{%{NAS-Port-ID}:-%{NAS-Port}}' \ + AND NASPortType = '%{NAS-Port-Type}' \ + AND AcctStopTime = 0" + } + + stop { + query = "\ + UPDATE ${....acct_table2} \ + SET \ + AcctStopTime = ${....event_timestamp}, \ + AcctSessionTime = '%{Acct-Session-Time}', \ + AcctInputOctets = convert(bigint, '%{%{Acct-Input-Gigawords}:-0}' * POWER(2.0, 32)) | '%{%{Acct-Input-Octets}:-0}', \ + AcctOutputOctets = convert(bigint, '%{%{Acct-Output-Gigawords}:-0}' * POWER(2.0, 32)) | '%{%{Acct-Output-Octets}:-0}', \ + AcctTerminateCause = '%{Acct-Terminate-Cause}', \ + AcctStopDelay = '%{%{Acct-Delay-Time}:-0}', \ + ConnectInfo_stop = '%{Connect-Info}' \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-ID}' \ + AND AcctStopTime = 0" + + query = "\ + INSERT into ${....acct_table2} (\ + AcctSessionId, \ + AcctUniqueId, \ + UserName, \ + Realm, \ + NASIPAddress, \ + NASPortID, \ + NASPortType, \ + AcctStopTime, \ + AcctSessionTime, \ + AcctAuthentic, \ + ConnectInfo_start, \ + ConnectInfo_stop, \ + AcctInputOctets, \ + AcctOutputOctets, \ + CalledStationId, \ + CallingStationId, \ + AcctTerminateCause, \ + ServiceType, \ + FramedProtocol, \ + FramedIPAddress, \ + FramedIPv6Address, \ + FramedIPv6Prefix, \ + FramedInterfaceId, \ + DelegatedIPv6Prefix, \ + AcctStartDelay, \ + AcctStopDelay \ + ${....class.column_name}) \ + VALUES(\ + '%{Acct-Session-Id}', \ + '%{Acct-Unique-Session-Id}', \ + '%{SQL-User-Name}', \ + '%{Realm}', \ + '%{NAS-IP-Address}', \ + '%{%{NAS-Port-ID}:-%{NAS-Port}}', \ + '%{NAS-Port-Type}', \ + ${....event_timestamp}, \ + '%{Acct-Session-Time}', \ + '%{Acct-Authentic}', \ + '', \ + '%{Connect-Info}', \ + convert(bigint, '%{%{Acct-Input-Gigawords}:-0}' * POWER(2.0, 32)) | '%{%{Acct-Input-Octets}:-0}', \ + convert(bigint, '%{%{Acct-Output-Gigawords}:-0}' * POWER(2.0, 32)) | '%{%{Acct-Output-Octets}:-0}', \ + '%{Called-Station-Id}', \ + '%{Calling-Station-Id}', \ + '%{Acct-Terminate-Cause}', \ + '%{Service-Type}', \ + '%{Framed-Protocol}', \ + '%{Framed-IP-Address}', \ + '%{Framed-IPv6-Address}', \ + '%{Framed-IPv6-Prefix}', \ + '%{Framed-Interface-Id}', \ + '%{Delegated-IPv6-Prefix}', \ + '0', \ + '%{%{Acct-Delay-Time}:-0}' \ + ${....class.packet_xlat})" + + # + # When using "sql_session_start", you should comment out + # the previous query, and enable this one. + # + # Just change the previous query to "-query", + # and this one to "query". The previous one + # will be ignored, and this one will be + # enabled. + # + -query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctSessionId = '%{Acct-Session-Id}', \ + AcctUniqueId = '%{Acct-Unique-Session-Id}', \ + AcctAuthentic = '%{Acct-Authentic}', \ + ConnectInfo_start = '%{Connect-Info}', \ + ServiceType = '%{Service-Type}', \ + FramedProtocol = '%{Framed-Protocol}', \ + FramedIPAddress = '%{Framed-IP-Address}', \ + FramedIPv6Address = '%{Framed-IPv6-Address}', \ + FramedIPv6Prefix = '%{Framed-IPv6-Prefix}', \ + FramedInterfaceId = '%{Framed-Interface-Id}', \ + DelegatedIPv6Prefix = '%{Delegated-IPv6-Prefix}', \ + AcctStopTime = '%S', \ + AcctSessionTime = %{Acct-Session-Time}, \ + AcctInputOctets = convert(bigint, '%{%{Acct-Input-Gigawords}:-0}' * POWER(2.0, 32)) | '%{%{Acct-Input-Octets}:-0}', \ + AcctOutputOctets = convert(bigint, '%{%{Acct-Output-Gigawords}:-0}' * POWER(2.0, 32)) | '%{%{Acct-Output-Octets}:-0}', \ + AcctTerminateCause = '%{Acct-Terminate-Cause}', \ + ConnectInfo_stop = '%{Connect-Info}' \ + WHERE UserName = '%{SQL-User-Name}' \ + AND NASIPAddress = '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}' \ + AND NASPortId = '%{%{NAS-Port-ID}:-%{NAS-Port}}' \ + AND NASPortType = '%{NAS-Port-Type}' \ + AND AcctStopTime = 0" + } + + # + # No Acct-Status-Type == ignore the packet + # + accounting { + query = "SELECT true" + } + } +} + +post-auth { + # Write SQL queries to a logfile. This is potentially useful for bulk inserts + # when used with the rlm_sql_null driver. +# logfile = ${logdir}/post-auth.sql + + query = "\ + INSERT INTO ${..postauth_table} \ + (userName, pass, reply, authdate ${..class.column_name}) \ + VALUES(\ + '%{User-Name}', \ + '%{%{User-Password}:-CHAP-PASSWORD}', \ + '%{reply:Packet-Type}', \ + '%S.%{expr:%M / 1000}' \ + ${..class.reply_xlat})" +} diff --git a/raddb/mods-config/sql/main/mssql/schema.sql b/raddb/mods-config/sql/main/mssql/schema.sql new file mode 100644 index 0000000..7f6d633 --- /dev/null +++ b/raddb/mods-config/sql/main/mssql/schema.sql @@ -0,0 +1,302 @@ +-- $Id$d$ +-- +-- schela.sql rlm_sql - FreeRADIUS SQL Module +-- +-- Database schema for MSSQL rlm_sql module +-- +-- To load: +-- isql -S db_ip_addr -d db_name -U db_login -P db_passwd -i db_mssql.sql +-- +-- Based on: db_mysql.sql (Mike Machado <mike@innercite.com>) +-- +-- Dmitri Ageev <d_ageev@ortcc.ru> +-- + + +-- +-- Table structure for table 'radacct' +-- + +CREATE TABLE [radacct] ( + [RadAcctId] [numeric](21, 0) IDENTITY (1, 1) NOT NULL, + [AcctSessionId] [varchar] (64) NOT NULL, + [AcctUniqueId] [varchar] (32) NOT NULL, + [UserName] [varchar] (64) NOT NULL, + [GroupName] [varchar] (64) NOT NULL, + [Realm] [varchar] (64) NOT NULL, + [NASIPAddress] [varchar] (15) NOT NULL, + [NASPortId] [varchar] (32) NULL, + [NASPortType] [varchar] (32) NULL, + [AcctStartTime] [datetime] NOT NULL, + [AcctUpdateTime] [datetime] NOT NULL, + [AcctStopTime] [datetime] NOT NULL, + [AcctInterval] [bigint] NULL, + [AcctSessionTime] [bigint] NULL, + [AcctAuthentic] [varchar] (32) NULL, + [ConnectInfo_start] [varchar] (128) NULL, + [ConnectInfo_stop] [varchar] (128) NULL, + [AcctInputOctets] [bigint] NULL, + [AcctOutputOctets] [bigint] NULL, + [CalledStationId] [varchar] (50) NOT NULL, + [CallingStationId] [varchar] (50) NOT NULL, + [AcctTerminateCause] [varchar] (32) NOT NULL, + [ServiceType] [varchar] (32) NULL, + [FramedProtocol] [varchar] (32) NULL, + [FramedIPAddress] [varchar] (15) NOT NULL, + [FramedIPv6Address] [varchar] (45) NOT NULL, + [FramedIPv6Prefix] [varchar] (45) NOT NULL, + [FramedInterfaceId] [varchar] (44) NOT NULL, + [DelegatedIPv6Prefix] [varchar] (45) NOT NULL, + [AcctStartDelay] [int] NULL, + [AcctStopDelay] [int] NULL, + [Class] [varchar] (64) NULL +) ON [PRIMARY] +GO + +ALTER TABLE [radacct] WITH NOCHECK ADD + CONSTRAINT [DF_radacct_GroupName] DEFAULT ('') FOR [GroupName], + CONSTRAINT [DF_radacct_AcctSessionId] DEFAULT ('') FOR [AcctSessionId], + CONSTRAINT [DF_radacct_AcctUniqueId] DEFAULT ('') FOR [AcctUniqueId], + CONSTRAINT [DF_radacct_UserName] DEFAULT ('') FOR [UserName], + CONSTRAINT [DF_radacct_Realm] DEFAULT ('') FOR [Realm], + CONSTRAINT [DF_radacct_NASIPAddress] DEFAULT ('') FOR [NASIPAddress], + CONSTRAINT [DF_radacct_NASPortId] DEFAULT (null) FOR [NASPortId], + CONSTRAINT [DF_radacct_NASPortType] DEFAULT (null) FOR [NASPortType], + CONSTRAINT [DF_radacct_AcctStartTime] DEFAULT ('1900-01-01 00:00:00') FOR [AcctStartTime], + CONSTRAINT [DF_radacct_AcctUpdateTime] DEFAULT ('1900-01-01 00:00:00') FOR [AcctUpdateTime], + CONSTRAINT [DF_radacct_AcctStopTime] DEFAULT ('1900-01-01 00:00:00') FOR [AcctStopTime], + CONSTRAINT [DF_radacct_AcctSessionTime] DEFAULT (null) FOR [AcctSessionTime], + CONSTRAINT [DF_radacct_AcctAuthentic] DEFAULT (null) FOR [AcctAuthentic], + CONSTRAINT [DF_radacct_ConnectInfo_start] DEFAULT (null) FOR [ConnectInfo_start], + CONSTRAINT [DF_radacct_ConnectInfo_stop] DEFAULT (null) FOR [ConnectInfo_stop], + CONSTRAINT [DF_radacct_AcctInputOctets] DEFAULT (null) FOR [AcctInputOctets], + CONSTRAINT [DF_radacct_AcctOutputOctets] DEFAULT (null) FOR [AcctOutputOctets], + CONSTRAINT [DF_radacct_CalledStationId] DEFAULT ('') FOR [CalledStationId], + CONSTRAINT [DF_radacct_CallingStationId] DEFAULT ('') FOR [CallingStationId], + CONSTRAINT [DF_radacct_AcctTerminateCause] DEFAULT ('') FOR [AcctTerminateCause], + CONSTRAINT [DF_radacct_ServiceType] DEFAULT (null) FOR [ServiceType], + CONSTRAINT [DF_radacct_FramedProtocol] DEFAULT (null) FOR [FramedProtocol], + CONSTRAINT [DF_radacct_FramedIPAddress] DEFAULT ('') FOR [FramedIPAddress], + CONSTRAINT [DF_radacct_FramedIPv6Address] DEFAULT ('') FOR [FramedIPv6Address], + CONSTRAINT [DF_radacct_FramedIPv6Prefix] DEFAULT ('') FOR [FramedIPv6Prefix], + CONSTRAINT [DF_radacct_FramedInterfaceId] DEFAULT ('') FOR [FramedInterfaceId], + CONSTRAINT [DF_radacct_DelegatedIPv6Prefix] DEFAULT ('') FOR [DelegatedIPv6Prefix], + CONSTRAINT [DF_radacct_AcctStartDelay] DEFAULT (null) FOR [AcctStartDelay], + CONSTRAINT [DF_radacct_AcctStopDelay] DEFAULT (null) FOR [AcctStopDelay], + CONSTRAINT [DF_radacct_Class] DEFAULT (null) FOR [Class], + CONSTRAINT [PK_radacct] PRIMARY KEY NONCLUSTERED + ( + [RadAcctId] + ) ON [PRIMARY] +GO + +CREATE INDEX [UserName] ON [radacct]([UserName]) ON [PRIMARY] +GO + +CREATE INDEX [FramedIPAddress] ON [radacct]([FramedIPAddress]) ON [PRIMARY] +GO + +CREATE INDEX [FramedIPv6Address] ON [radacct]([FramedIPv6Address]) ON [PRIMARY] +GO + +CREATE INDEX [FramedIPv6Prefix] ON [radacct]([FramedIPv6Prefix]) ON [PRIMARY] +GO + +CREATE INDEX [FramedInterfaceId] ON [radacct]([FramedInterfaceId]) ON [PRIMARY] +GO + +CREATE INDEX [DelegatedIPv6Prefix] ON [radacct]([DelegatedIPv6Prefix]) ON [PRIMARY] +GO + +CREATE INDEX [AcctSessionId] ON [radacct]([AcctSessionId]) ON [PRIMARY] +GO + +CREATE UNIQUE INDEX [AcctUniqueId] ON [radacct]([AcctUniqueId]) ON [PRIMARY] +GO + +CREATE INDEX [AcctStartTime] ON [radacct]([AcctStartTime]) ON [PRIMARY] +GO + +CREATE INDEX [AcctStopTime] ON [radacct]([AcctStopTime]) ON [PRIMARY] +GO + +CREATE INDEX [NASIPAddress] ON [radacct]([NASIPAddress]) ON [PRIMARY] +GO + +CREATE INDEX [Class] ON [radacct]([Class]) ON [PRIMARY] +GO + +/* For use by onoff */ +CREATE INDEX [RadacctBulkClose] ON [radacct]([NASIPAddress],[AcctStartTime]) WHERE [AcctStopTime] IS NULL ON [PRIMARY] +GO + + +-- +-- Table structure for table 'radacct' +-- + +CREATE TABLE [radcheck] ( + [id] [int] IDENTITY (1, 1) NOT NULL , + [UserName] [varchar] (64) NOT NULL , + [Attribute] [varchar] (32) NOT NULL , + [Value] [varchar] (253) NOT NULL , + [op] [char] (2) NULL +) ON [PRIMARY] +GO + +ALTER TABLE [radcheck] WITH NOCHECK ADD + CONSTRAINT [DF_radcheck_UserName] DEFAULT ('') FOR [UserName], + CONSTRAINT [DF_radcheck_Attribute] DEFAULT ('') FOR [Attribute], + CONSTRAINT [DF_radcheck_Value] DEFAULT ('') FOR [Value], + CONSTRAINT [DF_radcheck_op] DEFAULT (null) FOR [op], + CONSTRAINT [PK_radcheck] PRIMARY KEY NONCLUSTERED + ( + [id] + ) ON [PRIMARY] +GO + +CREATE INDEX [UserName] ON [radcheck]([UserName]) ON [PRIMARY] +GO + + +-- +-- Table structure for table 'radacct' +-- + +CREATE TABLE [radgroupcheck] ( + [id] [int] IDENTITY (1, 1) NOT NULL , + [GroupName] [varchar] (64) NOT NULL , + [Attribute] [varchar] (32) NOT NULL , + [Value] [varchar] (253) NOT NULL , + [op] [char] (2) NULL +) ON [PRIMARY] +GO + +ALTER TABLE [radgroupcheck] WITH NOCHECK ADD + CONSTRAINT [DF_radgroupcheck_GroupName] DEFAULT ('') FOR [GroupName], + CONSTRAINT [DF_radgroupcheck_Attribute] DEFAULT ('') FOR [Attribute], + CONSTRAINT [DF_radgroupcheck_Value] DEFAULT ('') FOR [Value], + CONSTRAINT [DF_radgroupcheck_op] DEFAULT (null) FOR [op], + CONSTRAINT [PK_radgroupcheck] PRIMARY KEY NONCLUSTERED + ( + [id] + ) ON [PRIMARY] +GO + +CREATE INDEX [GroupName] ON [radgroupcheck]([GroupName]) ON [PRIMARY] +GO + + +-- +-- Table structure for table 'radacct' +-- + +CREATE TABLE [radgroupreply] ( + [id] [int] IDENTITY (1, 1) NOT NULL , + [GroupName] [varchar] (64) NOT NULL , + [Attribute] [varchar] (32) NOT NULL , + [Value] [varchar] (253) NOT NULL , + [op] [char] (2) NULL , + [prio] [int] NOT NULL +) ON [PRIMARY] +GO + +ALTER TABLE [radgroupreply] WITH NOCHECK ADD + CONSTRAINT [DF_radgroupreply_GroupName] DEFAULT ('') FOR [GroupName], + CONSTRAINT [DF_radgroupreply_Attribute] DEFAULT ('') FOR [Attribute], + CONSTRAINT [DF_radgroupreply_Value] DEFAULT ('') FOR [Value], + CONSTRAINT [DF_radgroupreply_op] DEFAULT (null) FOR [op], + CONSTRAINT [DF_radgroupreply_prio] DEFAULT (0) FOR [prio], + CONSTRAINT [PK_radgroupreply] PRIMARY KEY NONCLUSTERED + ( + [id] + ) ON [PRIMARY] +GO + +CREATE INDEX [GroupName] ON [radgroupreply]([GroupName]) ON [PRIMARY] +GO + + +-- +-- Table structure for table 'radacct' +-- + +CREATE TABLE [radreply] ( + [id] [int] IDENTITY (1, 1) NOT NULL , + [UserName] [varchar] (64) NOT NULL , + [Attribute] [varchar] (32) NOT NULL , + [Value] [varchar] (253) NOT NULL , + [op] [char] (2) NULL +) ON [PRIMARY] +GO + +ALTER TABLE [radreply] WITH NOCHECK ADD + CONSTRAINT [DF_radreply_UserName] DEFAULT ('') FOR [UserName], + CONSTRAINT [DF_radreply_Attribute] DEFAULT ('') FOR [Attribute], + CONSTRAINT [DF_radreply_Value] DEFAULT ('') FOR [Value], + CONSTRAINT [DF_radreply_op] DEFAULT (null) FOR [op], + CONSTRAINT [PK_radreply] PRIMARY KEY NONCLUSTERED + ( + [id] + ) ON [PRIMARY] +GO + +CREATE INDEX [UserName] ON [radreply]([UserName]) ON [PRIMARY] +GO + + +-- +-- Table structure for table 'radacct' +-- + +CREATE TABLE [radusergroup] ( + [id] [int] IDENTITY (1, 1) NOT NULL , + [UserName] [varchar] (64) NOT NULL , + [GroupName] [varchar] (64) NULL , + [Priority] [int] NULL +) ON [PRIMARY] +GO + +ALTER TABLE [radusergroup] WITH NOCHECK ADD + CONSTRAINT [DF_radusergroup_UserName] DEFAULT ('') FOR [UserName], + CONSTRAINT [DF_radusergroup_GroupName] DEFAULT ('') FOR [GroupName], + CONSTRAINT [PK_radusergroup] PRIMARY KEY NONCLUSTERED + ( + [id] + ) ON [PRIMARY] +GO + +CREATE INDEX [UserName] ON [radusergroup]([UserName]) ON [PRIMARY] +GO + + +-- +-- Table structure for table 'radacct' +-- + +CREATE TABLE [radpostauth] ( + [id] [int] IDENTITY (1, 1) NOT NULL , + [userName] [varchar] (64) NOT NULL , + [pass] [varchar] (64) NOT NULL , + [reply] [varchar] (32) NOT NULL , + [authdate] [datetime] NOT NULL, + [Class] [varchar] (64) NULL +) +GO + +CREATE INDEX [userName] ON [radpostauth]([userName]) ON [PRIMARY] +GO + +CREATE INDEX [Class] ON [radpostauth]([Class]) ON [PRIMARY] +GO + +ALTER TABLE [radpostauth] WITH NOCHECK ADD + CONSTRAINT [DF_radpostauth_userName] DEFAULT ('') FOR [userName], + CONSTRAINT [DF_radpostauth_pass] DEFAULT ('') FOR [pass], + CONSTRAINT [DF_radpostauth_reply] DEFAULT ('') FOR [reply], + CONSTRAINT [DF_radpostauth_authdate] DEFAULT (getdate()) FOR [authdate], + CONSTRAINT [PK_radpostauth] PRIMARY KEY NONCLUSTERED + ( + [id] + ) ON [PRIMARY] +GO diff --git a/raddb/mods-config/sql/main/mysql/extras/wimax/queries.conf b/raddb/mods-config/sql/main/mysql/extras/wimax/queries.conf new file mode 100644 index 0000000..2694230 --- /dev/null +++ b/raddb/mods-config/sql/main/mysql/extras/wimax/queries.conf @@ -0,0 +1,40 @@ +# -*- text -*- +## +## wimax.conf -- MySQL configuration for WiMAX keying +## +## $Id$ + +# Safe characters list for sql queries. Everything else is replaced +# with their mime-encoded equivalents. +# The default list should be ok +#safe_characters = "@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_: /" + +####################################################################### +# Query config: Username +####################################################################### +# This is the username that will get substituted, escaped, and added +# as attribute 'SQL-User-Name'. '%{SQL-User-Name}' should be used below +# everywhere a username substitution is needed so you you can be sure +# the username passed from the client is escaped properly. +# +# Uncomment the next line, if you want the sql_user_name to mean: +# +# Use Stripped-User-Name, if it's there. +# Else use User-Name, if it's there, +# Else use hard-coded string "DEFAULT" as the user name. +#sql_user_name = "%{%{Stripped-User-Name}:-%{%{User-Name}:-DEFAULT}}" +# +sql_user_name = "%{User-Name}" + +####################################################################### +# Logging of WiMAX SPI -> key mappings +####################################################################### +# postauth_query - Insert some info after authentication +####################################################################### + +postauth_query = "INSERT INTO wimax \ + (username, authdate, spi, mipkey, lifetime) \ + VALUES ( \ + '%{User-Name}', '%S' \ + '%{%{reply:WiMAX-MN-hHA-MIP4-SPI}:-%{reply:WiMAX-MN-hHA-MIP6-SPI}}', \ + '%{%{reply:WiMAX-MN-hHA-MIP4-Key}:-%{reply:WiMAX-MN-hHA-MIP6-Key}}', '%{%{reply:Session-Timeout}:-86400}' )" diff --git a/raddb/mods-config/sql/main/mysql/extras/wimax/schema.sql b/raddb/mods-config/sql/main/mysql/extras/wimax/schema.sql new file mode 100644 index 0000000..e32224a --- /dev/null +++ b/raddb/mods-config/sql/main/mysql/extras/wimax/schema.sql @@ -0,0 +1,16 @@ +# +# WiMAX Table structure for table 'wimax', +# which replaces the "radpostauth" table. +# + +CREATE TABLE wimax ( + id int(11) NOT NULL auto_increment, + username varchar(64) NOT NULL default '', + authdate timestamp NOT NULL, + spi varchar(16) NOT NULL default '', + mipkey varchar(400) NOT NULL default '', + lifetime int(12) default NULL, + PRIMARY KEY (id), + KEY username (username), + KEY spi (spi) +) ; diff --git a/raddb/mods-config/sql/main/mysql/process-radacct.sql b/raddb/mods-config/sql/main/mysql/process-radacct.sql new file mode 100644 index 0000000..04c1c5b --- /dev/null +++ b/raddb/mods-config/sql/main/mysql/process-radacct.sql @@ -0,0 +1,279 @@ +# -*- text -*- +# +# main/mysql/process-radacct.sql -- Schema extensions for processing radacct entries +# +# $Id$ + +-- --------------------------------- +-- - Per-user data usage over time - +-- --------------------------------- +-- +-- An extension to the standard schema to hold per-user data usage statistics +-- for arbitrary periods. +-- +-- The data_usage_by_period table is populated by periodically calling the +-- fr_new_data_usage_period stored procedure. +-- +-- This table can be queried in various ways to produce reports of aggregate +-- data use over time. For example, if the fr_new_data_usage_period SP is +-- invoked one per day just after midnight, to produce usage data with daily +-- granularity, then a reasonably accurate monthly bandwidth summary for a +-- given user could be obtained with: +-- +-- SELECT +-- DATE_FORMAT(period_start, '%Y-%M') AS month, +-- SUM(acctinputoctets)/1000/1000/1000 AS GB_in, +-- SUM(acctoutputoctets)/1000/1000/1000 AS GB_out +-- FROM +-- data_usage_by_period +-- WHERE +-- username='bob' AND +-- period_end IS NOT NULL +-- GROUP BY +-- YEAR(period_start), MONTH(period_start); +-- +-- +----------------+----------------+-----------------+ +-- | month | GB_in | GB_out | +-- +----------------+----------------+-----------------+ +-- | 2019-July | 5.782279230000 | 50.545664820000 | +-- | 2019-August | 4.230543340000 | 48.523096420000 | +-- | 2019-September | 4.847360590000 | 48.631835480000 | +-- | 2019-October | 6.456763250000 | 51.686231930000 | +-- | 2019-November | 6.362537730000 | 52.385710570000 | +-- | 2019-December | 4.301524440000 | 50.762240270000 | +-- | 2020-January | 5.436280540000 | 49.067775280000 | +-- +----------------+----------------+-----------------+ +-- 7 rows in set (0.000 sec) +-- +CREATE TABLE data_usage_by_period ( + username VARCHAR(64), + period_start DATETIME, + period_end DATETIME, + acctinputoctets BIGINT(20), + acctoutputoctets BIGINT(20), + PRIMARY KEY (username,period_start) +); +CREATE INDEX idx_data_usage_by_period_period_start ON data_usage_by_period (period_start); +CREATE INDEX idx_data_usage_by_period_period_end ON data_usage_by_period (period_end); + + +-- +-- Stored procedure that when run with some arbitrary frequency, say +-- once per day by cron, will process the recent radacct entries to extract +-- time-windowed data containing acct{input,output}octets ("data usage") per +-- username, per period. +-- +-- Each invocation will create new rows in the data_usage_by_period tables +-- containing the data used by each user since the procedure was last invoked. +-- The intervals do not need to be identical but care should be taken to +-- ensure that the start/end of each period aligns well with any intended +-- reporting intervals. +-- +-- It can be invoked by running: +-- +-- CALL fr_new_data_usage_period(); +-- +-- +DELIMITER $$ + +DROP PROCEDURE IF EXISTS fr_new_data_usage_period; +CREATE PROCEDURE fr_new_data_usage_period () +SQL SECURITY INVOKER +BEGIN + + DECLARE v_start DATETIME; + DECLARE v_end DATETIME; + + DECLARE EXIT HANDLER FOR SQLEXCEPTION + BEGIN + ROLLBACK; + RESIGNAL; + END; + + SELECT IFNULL(DATE_ADD(MAX(period_end), INTERVAL 1 SECOND), FROM_UNIXTIME(0)) INTO v_start FROM data_usage_by_period; + SELECT NOW() INTO v_end; + + START TRANSACTION; + + -- + -- Add the data usage for the sessions that were active in the current + -- period to the table. Include all sessions that finished since the start + -- of this period as well as those still ongoing. + -- + INSERT INTO data_usage_by_period (username, period_start, period_end, acctinputoctets, acctoutputoctets) + SELECT * + FROM ( + SELECT + username, + v_start, + v_end, + SUM(acctinputoctets) AS acctinputoctets, + SUM(acctoutputoctets) AS acctoutputoctets + FROM + radacct + WHERE + acctstoptime > v_start OR + acctstoptime IS NULL + GROUP BY + username + ) AS s + ON DUPLICATE KEY UPDATE + acctinputoctets = data_usage_by_period.acctinputoctets + s.acctinputoctets, + acctoutputoctets = data_usage_by_period.acctoutputoctets + s.acctoutputoctets, + period_end = v_end; + + -- + -- Create an open-ended "next period" for all ongoing sessions and carry a + -- negative value of their data usage to avoid double-accounting when we + -- process the next period. Their current data usage has already been + -- allocated to the current and possibly previous periods. + -- + INSERT INTO data_usage_by_period (username, period_start, period_end, acctinputoctets, acctoutputoctets) + SELECT * + FROM ( + SELECT + username, + DATE_ADD(v_end, INTERVAL 1 SECOND), + NULL, + 0 - SUM(acctinputoctets), + 0 - SUM(acctoutputoctets) + FROM + radacct + WHERE + acctstoptime IS NULL + GROUP BY + username + ) AS s; + + COMMIT; + +END$$ + +DELIMITER ; + + +-- ------------------------------------------------------ +-- - "Lightweight" Accounting-On/Off strategy resources - +-- ------------------------------------------------------ +-- +-- The following resources are for use only when the "lightweight" +-- Accounting-On/Off strategy is enabled in queries.conf. +-- +-- Instead of bulk closing the radacct sessions belonging to a reloaded NAS, +-- this strategy leaves them open and records the NAS reload time in the +-- nasreload table. +-- +-- Where applicable, the onus is on the administator to: +-- +-- * Consider the nas reload times when deriving a list of +-- active/inactive sessions, and when determining the duration of sessions +-- interrupted by a NAS reload. (Refer to the view below.) +-- +-- * Close the affected sessions out of band. (Refer to the SP below.) +-- +-- +-- The radacct_with_reloads view presents the radacct table with two additional +-- columns: acctstoptime_with_reloads and acctsessiontime_with_reloads +-- +-- Where the session isn't closed (acctstoptime IS NULL), yet it started before +-- the last reload of the NAS (radacct.acctstarttime < nasreload.reloadtime), +-- the derived columns are set based on the reload time of the NAS (effectively +-- the point in time that the session was interrupted.) +-- +CREATE VIEW radacct_with_reloads AS +SELECT + a.*, + COALESCE(a.acctstoptime, + IF(a.acctstarttime < n.reloadtime, n.reloadtime, NULL) + ) AS acctstoptime_with_reloads, + COALESCE(a.acctsessiontime, + IF(a.acctstoptime IS NULL AND a.acctstarttime < n.reloadtime, + UNIX_TIMESTAMP(n.reloadtime) - UNIX_TIMESTAMP(a.acctstarttime), NULL) + ) AS acctsessiontime_with_reloads +FROM radacct a +LEFT OUTER JOIN nasreload n USING (nasipaddress); + + +-- +-- It may be desirable to periodically "close" radacct sessions belonging to a +-- reloaded NAS, replicating the "bulk close" Accounting-On/Off behaviour, +-- just not in real time. +-- +-- The fr_radacct_close_after_reload SP will set radacct.acctstoptime to +-- nasreload.reloadtime, calculate the corresponding radacct.acctsessiontime, +-- and set acctterminatecause to "NAS reboot" for interrupted sessions. It +-- does so in batches, which avoids long-lived locks on the affected rows. +-- +-- It can be invoked as follows: +-- +-- CALL fr_radacct_close_after_reload(); +-- +-- Note: This SP walks radacct in strides of v_batch_size. It will typically +-- skip closed and ongoing sessions at a rate significantly faster than +-- 100,000 rows per second and process batched updates faster than 20,000 +-- orphaned sessions per second. If this isn't fast enough then you should +-- really consider using a custom schema that includes partitioning by +-- nasipaddress or acct{start,stop}time. +-- +DELIMITER $$ + +DROP PROCEDURE IF EXISTS fr_radacct_close_after_reload; +CREATE PROCEDURE fr_radacct_close_after_reload () +SQL SECURITY INVOKER +BEGIN + + DECLARE v_a BIGINT(21); + DECLARE v_z BIGINT(21); + DECLARE v_updated BIGINT(21) DEFAULT 0; + DECLARE v_last_report DATETIME DEFAULT 0; + DECLARE v_last BOOLEAN DEFAULT FALSE; + DECLARE v_batch_size INT(12); + + -- + -- This works for many circumstances + -- + SET v_batch_size = 2500; + + SELECT MIN(radacctid) INTO v_a FROM radacct WHERE acctstoptime IS NULL; + + update_loop: LOOP + + SET v_z = NULL; + SELECT radacctid INTO v_z FROM radacct WHERE radacctid > v_a ORDER BY radacctid LIMIT v_batch_size,1; + + IF v_z IS NULL THEN + SELECT MAX(radacctid) INTO v_z FROM radacct; + SET v_last = TRUE; + END IF; + + UPDATE radacct a INNER JOIN nasreload n USING (nasipaddress) + SET + acctstoptime = n.reloadtime, + acctsessiontime = UNIX_TIMESTAMP(n.reloadtime) - UNIX_TIMESTAMP(acctstarttime), + acctterminatecause = 'NAS reboot' + WHERE + radacctid BETWEEN v_a AND v_z + AND acctstoptime IS NULL + AND acctstarttime < n.reloadtime; + + SET v_updated = v_updated + ROW_COUNT(); + + SET v_a = v_z + 1; + + -- + -- Periodically report how far we've got + -- + IF v_last_report != NOW() OR v_last THEN + SELECT v_z AS latest_radacctid, v_updated AS sessions_closed; + SET v_last_report = NOW(); + END IF; + + IF v_last THEN + LEAVE update_loop; + END IF; + + END LOOP; + +END$$ + +DELIMITER ; diff --git a/raddb/mods-config/sql/main/mysql/queries.conf b/raddb/mods-config/sql/main/mysql/queries.conf new file mode 100644 index 0000000..4b5f6e2 --- /dev/null +++ b/raddb/mods-config/sql/main/mysql/queries.conf @@ -0,0 +1,694 @@ +# -*- text -*- +# +# main/mysql/queries.conf-- MySQL configuration for default schema (schema.sql) +# +# $Id$ + +# Use the driver specific SQL escape method. +# +# If you enable this configuration item, the "safe_characters" +# configuration is ignored. FreeRADIUS then uses the MySQL escape +# functions to escape input strings. The only downside to making this +# change is that the MySQL escaping method is not the same the one +# used by FreeRADIUS. So characters which are NOT in the +# "safe_characters" list will now be stored differently in the database. +# +#auto_escape = yes + +# Safe characters list for sql queries. Everything else is replaced +# with their mime-encoded equivalents. +# The default list should be ok +# Using 'auto_escape' is preferred +safe_characters = "@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_: /" + +####################################################################### +# Connection config +####################################################################### +# The character set is not configurable. The default character set of +# the mysql client library is used. To control the character set, +# create/edit my.cnf (typically in /etc/mysql/my.cnf or /etc/my.cnf) +# and enter +# [client] +# default-character-set = utf8 +# + +####################################################################### +# Query config: Username +####################################################################### +# This is the username that will get substituted, escaped, and added +# as attribute 'SQL-User-Name'. '%{SQL-User-Name}' should be used below +# everywhere a username substitution is needed so you you can be sure +# the username passed from the client is escaped properly. +# +# Uncomment the next line, if you want the sql_user_name to mean: +# +# Use Stripped-User-Name, if it's there. +# Else use User-Name, if it's there, +# Else use hard-coded string "DEFAULT" as the user name. +#sql_user_name = "%{%{Stripped-User-Name}:-%{%{User-Name}:-DEFAULT}}" +# +sql_user_name = "%{User-Name}" + +####################################################################### +# Query config: Event-Timestamp +####################################################################### +# event_timestamp_epoch is the basis for the time inserted into +# accounting records. Typically this will be the Event-Timestamp of the +# accounting request, which is usually provided by a NAS. +# +# Uncomment the next line, if you want the timestamp to be based on the +# request reception time recorded by this server, for example if you +# distrust the provided Event-Timestamp. +#event_timestamp_epoch = "%l" + +event_timestamp_epoch = "%{%{integer:Event-Timestamp}:-%l}" + +# event_timestamp is the SQL snippet for converting an epoch timestamp +# to an SQL date. + +event_timestamp = "FROM_UNIXTIME(${event_timestamp_epoch})" + +####################################################################### +# Query config: Class attribute +####################################################################### +# +# 3.0.22 and later have a "class" column in the accounting table. +# +# However, we do NOT want to break existing configurations by adding +# the Class attribute to the default queries. If we did that, then +# systems using newer versions of the server would fail, because +# there is no "class" column in their accounting tables. +# +# The solution to that is the following "class" subsection. If your +# database has a "class" column for the various tables, then you can +# uncomment the configuration items here. The queries below will +# then automatically insert the Class attribute into radacct, +# radpostauth, etc. +# +class { + # + # Delete the '#' character from each of the configuration + # items in this section. This change puts the Class + # attribute into the various tables. Leave the double-quoted + # string there, as the value for the configuration item. + # + # See also policy.d/accounting, and the "insert_acct_class" + # policy. You will need to list (or uncomment) + # "insert_acct_class" in the "post-auth" section in order to + # create a Class attribute. + # + column_name = # ", class" + packet_xlat = # ", '%{Class}'" + reply_xlat = # ", '%{reply:Class}'" +} + +####################################################################### +# Default profile +####################################################################### +# This is the default profile. It is found in SQL by group membership. +# That means that this profile must be a member of at least one group +# which will contain the corresponding check and reply items. +# This profile will be queried in the authorize section for every user. +# The point is to assign all users a default profile without having to +# manually add each one to a group that will contain the profile. +# The SQL module will also honor the User-Profile attribute. This +# attribute can be set anywhere in the authorize section (ie the users +# file). It is found exactly as the default profile is found. +# If it is set then it will *overwrite* the default profile setting. +# The idea is to select profiles based on checks on the incoming packets, +# not on user group membership. For example: +# -- users file -- +# DEFAULT Service-Type == Outbound-User, User-Profile := "outbound" +# DEFAULT Service-Type == Framed-User, User-Profile := "framed" +# +# By default the default_user_profile is not set +# +#default_user_profile = "DEFAULT" + +####################################################################### +# NAS Query +####################################################################### +# This query retrieves the radius clients +# +# 0. Row ID (currently unused) +# 1. Name (or IP address) +# 2. Shortname +# 3. Type +# 4. Secret +# 5. Server +####################################################################### + +client_query = "\ + SELECT id, nasname, shortname, type, secret, server \ + FROM ${client_table}" + +####################################################################### +# Authorization Queries +####################################################################### +# These queries compare the check items for the user +# in ${authcheck_table} and setup the reply items in +# ${authreply_table}. You can use any query/tables +# you want, but the return data for each row MUST +# be in the following order: +# +# 0. Row ID (currently unused) +# 1. UserName/GroupName +# 2. Item Attr Name +# 3. Item Attr Value +# 4. Item Attr Operation +####################################################################### +# Use these for case sensitive usernames. + +#authorize_check_query = "\ +# SELECT id, username, attribute, value, op \ +# FROM ${authcheck_table} \ +# WHERE username = BINARY '%{SQL-User-Name}' \ +# ORDER BY id" + +#authorize_reply_query = "\ +# SELECT id, username, attribute, value, op \ +# FROM ${authreply_table} \ +# WHERE username = BINARY '%{SQL-User-Name}' \ +# ORDER BY id" + +# +# The default queries are case insensitive. (for compatibility with +# older versions of FreeRADIUS) +# +authorize_check_query = "\ + SELECT id, username, attribute, value, op \ + FROM ${authcheck_table} \ + WHERE username = '%{SQL-User-Name}' \ + ORDER BY id" + +authorize_reply_query = "\ + SELECT id, username, attribute, value, op \ + FROM ${authreply_table} \ + WHERE username = '%{SQL-User-Name}' \ + ORDER BY id" + +# +# Use these for case sensitive usernames. +# +#group_membership_query = "\ +# SELECT groupname \ +# FROM ${usergroup_table} \ +# WHERE username = BINARY '%{SQL-User-Name}' \ +# ORDER BY priority" + +group_membership_query = "\ + SELECT groupname \ + FROM ${usergroup_table} \ + WHERE username = '%{SQL-User-Name}' \ + ORDER BY priority" + +authorize_group_check_query = "\ + SELECT id, groupname, attribute, \ + Value, op \ + FROM ${groupcheck_table} \ + WHERE groupname = '%{${group_attribute}}' \ + ORDER BY id" + +authorize_group_reply_query = "\ + SELECT id, groupname, attribute, \ + value, op \ + FROM ${groupreply_table} \ + WHERE groupname = '%{${group_attribute}}' \ + ORDER BY id" + +####################################################################### +# Simultaneous Use Checking Queries +####################################################################### +# simul_count_query - query for the number of current connections +# - If this is not defined, no simultaneous use checking +# - will be performed by this module instance +# simul_verify_query - query to return details of current connections +# for verification +# - Leave blank or commented out to disable verification step +# - Note that the returned field order should not be changed. +# +# Note: Sessions that started prior to the most recent reload of their NAS will +# be correctly considered inactive, even if the radacct entry itself is not +# marked as stopped. +# +####################################################################### + +simul_count_query = "\ + SELECT COUNT(*) \ + FROM ${acct_table1} a \ + LEFT OUTER JOIN nasreload n USING (nasipaddress) \ + WHERE username = '%{SQL-User-Name}' \ + AND acctstoptime IS NULL \ + AND (a.acctstarttime > n.reloadtime OR n.reloadtime IS NULL)" + +simul_verify_query = "\ + SELECT \ + radacctid, acctsessionid, username, nasipaddress, nasportid, framedipaddress, \ + callingstationid, framedprotocol \ + FROM ${acct_table1} a \ + LEFT OUTER JOIN nasreload n USING (nasipaddress) \ + WHERE username = '%{SQL-User-Name}' \ + AND acctstoptime IS NULL \ + AND (a.acctstarttime > n.reloadtime OR n.reloadtime IS NULL)" + +####################################################################### +# Accounting and Post-Auth Queries +####################################################################### +# These queries insert/update accounting and authentication records. +# The query to use is determined by the value of 'reference'. +# This value is used as a configuration path and should resolve to one +# or more 'query's. If reference points to multiple queries, and a query +# fails, the next query is executed. +# +# Behaviour is identical to the old 1.x/2.x module, except we can now +# fail between N queries, and query selection can be based on any +# combination of attributes, or custom 'Acct-Status-Type' values. +####################################################################### +accounting { + reference = "%{tolower:type.%{%{Acct-Status-Type}:-%{Request-Processing-Stage}}.query}" + + # Write SQL queries to a logfile. This is potentially useful for bulk inserts + # when used with the rlm_sql_null driver. +# logfile = ${logdir}/accounting.sql + + column_list = "\ + acctsessionid, acctuniqueid, username, \ + realm, nasipaddress, nasportid, \ + nasporttype, acctstarttime, acctupdatetime, \ + acctstoptime, acctsessiontime, acctauthentic, \ + connectinfo_start, connectinfo_stop, acctinputoctets, \ + acctoutputoctets, calledstationid, callingstationid, \ + acctterminatecause, servicetype, framedprotocol, \ + framedipaddress, framedipv6address, framedipv6prefix, \ + framedinterfaceid, delegatedipv6prefix ${..class.column_name}" + + type { + accounting-on { + + # + # "Bulk update" Accounting-On/Off strategy. + # + # Immediately terminate all sessions associated with a + # given NAS. + # + # Note: If a large number of sessions require closing + # then the bulk update may be take a long time to run + # and lock an excessive number of rows. See the + # strategy below for an alternative approach that does + # not touch the radacct session data. + # + query = "\ + UPDATE ${....acct_table1} \ + SET \ + acctstoptime = ${....event_timestamp}, \ + acctsessiontime = '${....event_timestamp_epoch}' \ + - UNIX_TIMESTAMP(acctstarttime), \ + acctterminatecause = '%{%{Acct-Terminate-Cause}:-NAS-Reboot}' \ + WHERE acctstoptime IS NULL \ + AND nasipaddress = '%{NAS-IP-Address}' \ + AND acctstarttime <= ${....event_timestamp}" + + # + # "Lightweight" Accounting-On/Off strategy. + # + # Record the reload time of the NAS and let the + # administrator actually close the sessions in radacct + # out-of-band, if desired. + # + # Implementation advice, together with a stored + # procedure for closing sessions and a view showing + # the effective stop time of each session is provided + # in process-radacct.sql. + # + # To enable this strategy, just change the previous + # query to "-query", and this one to "query". The + # previous one will be ignored, and this one will be + # enabled. + # + -query = "\ + INSERT INTO nasreload \ + SET \ + nasipaddress = '%{NAS-IP-Address}', \ + reloadtime = ${....event_timestamp} \ + ON DUPLICATE KEY UPDATE reloadtime = ${....event_timestamp}" + + } + + accounting-off { + query = "${..accounting-on.query}" + } + + # + # Implement the "sql_session_start" policy. + # See raddb/policy.d/accounting for more details. + # + # You also need to fix the other queries as + # documented below. Look for "sql_session_start". + # + post-auth { + query = "\ + INSERT INTO ${....acct_table1} \ + (${...column_list}) \ + VALUES(\ + '%{Acct-Session-Id}', \ + '%{Acct-Unique-Session-Id}', \ + '%{SQL-User-Name}', \ + '%{Realm}', \ + '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}', \ + NULLIF('%{%{NAS-Port-ID}:-%{NAS-Port}}', ''), \ + '%{NAS-Port-Type}', \ + ${....event_timestamp}, \ + NULL, \ + NULL, \ + 0, \ + '', \ + '%{Connect-Info}', \ + NULL, \ + 0, \ + 0, \ + '%{Called-Station-Id}', \ + '%{Calling-Station-Id}', \ + '', \ + '%{Service-Type}', \ + NULL, \ + '', \ + '', \ + '', \ + '', \ + '' \ + ${....class.packet_xlat})" + + query = "\ + UPDATE ${....acct_table1} SET \ + AcctStartTime = ${....event_timestamp}, \ + AcctUpdateTime = ${....event_timestamp}, \ + ConnectInfo_start = '%{Connect-Info}', \ + AcctSessionId = '%{Acct-Session-Id}' \ + WHERE UserName = '%{SQL-User-Name}' \ + AND NASIPAddress = '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}' \ + AND NASPortId = '%{%{NAS-Port-ID}:-%{NAS-Port}}' \ + AND NASPortType = '%{NAS-Port-Type}' \ + AND AcctStopTime IS NULL" + } + + start { + # + # Insert a new record into the sessions table + # + query = "\ + INSERT INTO ${....acct_table1} \ + (${...column_list}) \ + VALUES \ + ('%{Acct-Session-Id}', \ + '%{Acct-Unique-Session-Id}', \ + '%{SQL-User-Name}', \ + '%{Realm}', \ + '%{NAS-IP-Address}', \ + '%{%{NAS-Port-ID}:-%{NAS-Port}}', \ + '%{NAS-Port-Type}', \ + ${....event_timestamp}, \ + ${....event_timestamp}, \ + NULL, \ + '0', \ + '%{Acct-Authentic}', \ + '%{Connect-Info}', \ + '', \ + '0', \ + '0', \ + '%{Called-Station-Id}', \ + '%{Calling-Station-Id}', \ + '', \ + '%{Service-Type}', \ + '%{Framed-Protocol}', \ + '%{Framed-IP-Address}', \ + '%{Framed-IPv6-Address}', \ + '%{Framed-IPv6-Prefix}', \ + '%{Framed-Interface-Id}', \ + '%{Delegated-IPv6-Prefix}' \ + ${....class.packet_xlat})" + + # + # When using "sql_session_start", you should comment out + # the previous query, and enable this one. + # + # Just change the previous query to "-query", + # and this one to "query". The previous one + # will be ignored, and this one will be + # enabled. + # + -query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctSessionId = '%{Acct-Session-Id}', \ + AcctUniqueId = '%{Acct-Unique-Session-Id}', \ + AcctAuthentic = '%{Acct-Authentic}', \ + ConnectInfo_start = '%{Connect-Info}', \ + ServiceType = '%{Service-Type}', \ + FramedProtocol = '%{Framed-Protocol}', \ + framedipaddress = '%{Framed-IP-Address}', \ + framedipv6address = '%{Framed-IPv6-Address}', \ + framedipv6prefix = '%{Framed-IPv6-Prefix}', \ + framedinterfaceid = '%{Framed-Interface-Id}', \ + delegatedipv6prefix = '%{Delegated-IPv6-Prefix}', \ + AcctStartTime = ${....event_timestamp}, \ + AcctUpdateTime = ${....event_timestamp} \ + WHERE UserName = '%{SQL-User-Name}' \ + AND NASIPAddress = '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}' \ + AND NASPortId = '%{%{NAS-Port-ID}:-%{NAS-Port}}' \ + AND NASPortType = '%{NAS-Port-Type}' \ + AND AcctStopTime IS NULL" + + # + # Key constraints prevented us from inserting a new session, + # use the alternate query to update an existing session. + # + query = "\ + UPDATE ${....acct_table1} SET \ + acctstarttime = ${....event_timestamp}, \ + acctupdatetime = ${....event_timestamp}, \ + connectinfo_start = '%{Connect-Info}' \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-Id}'" + + } + + interim-update { + # + # Update an existing session and calculate the interval + # between the last data we received for the session and this + # update. This can be used to find stale sessions. + # + query = "\ + UPDATE ${....acct_table1} \ + SET \ + acctupdatetime = (@acctupdatetime_old:=acctupdatetime), \ + acctupdatetime = ${....event_timestamp}, \ + acctinterval = ${....event_timestamp_epoch} - \ + UNIX_TIMESTAMP(@acctupdatetime_old), \ + framedipaddress = '%{Framed-IP-Address}', \ + framedipv6address = '%{Framed-IPv6-Address}', \ + framedipv6prefix = '%{Framed-IPv6-Prefix}', \ + framedinterfaceid = '%{Framed-Interface-Id}', \ + delegatedipv6prefix = '%{Delegated-IPv6-Prefix}', \ + acctsessiontime = %{%{Acct-Session-Time}:-NULL}, \ + acctinputoctets = '%{%{Acct-Input-Gigawords}:-0}' \ + << 32 | '%{%{Acct-Input-Octets}:-0}', \ + acctoutputoctets = '%{%{Acct-Output-Gigawords}:-0}' \ + << 32 | '%{%{Acct-Output-Octets}:-0}' \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-Id}'" + + # + # The update condition matched no existing sessions. Use + # the values provided in the update to create a new session. + # + query = "\ + INSERT INTO ${....acct_table1} \ + (${...column_list}) \ + VALUES \ + ('%{Acct-Session-Id}', \ + '%{Acct-Unique-Session-Id}', \ + '%{SQL-User-Name}', \ + '%{Realm}', \ + '%{NAS-IP-Address}', \ + '%{%{NAS-Port-ID}:-%{NAS-Port}}', \ + '%{NAS-Port-Type}', \ + FROM_UNIXTIME(${....event_timestamp_epoch} - %{%{Acct-Session-Time}:-0}), \ + ${....event_timestamp}, \ + NULL, \ + %{%{Acct-Session-Time}:-NULL}, \ + '%{Acct-Authentic}', \ + '%{Connect-Info}', \ + '', \ + '%{%{Acct-Input-Gigawords}:-0}' << 32 | '%{%{Acct-Input-Octets}:-0}', \ + '%{%{Acct-Output-Gigawords}:-0}' << 32 | '%{%{Acct-Output-Octets}:-0}', \ + '%{Called-Station-Id}', \ + '%{Calling-Station-Id}', \ + '', \ + '%{Service-Type}', \ + '%{Framed-Protocol}', \ + '%{Framed-IP-Address}', \ + '%{Framed-IPv6-Address}', \ + '%{Framed-IPv6-Prefix}', \ + '%{Framed-Interface-Id}', \ + '%{Delegated-IPv6-Prefix}' \ + ${....class.packet_xlat})" + + # + # When using "sql_session_start", you should comment out + # the previous query, and enable this one. + # + # Just change the previous query to "-query", + # and this one to "query". The previous one + # will be ignored, and this one will be + # enabled. + # + -query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctSessionId = '%{Acct-Session-Id}', \ + AcctUniqueId = '%{Acct-Unique-Session-Id}', \ + AcctAuthentic = '%{Acct-Authentic}', \ + ConnectInfo_start = '%{Connect-Info}', \ + ServiceType = '%{Service-Type}', \ + FramedProtocol = '%{Framed-Protocol}', \ + framedipaddress = '%{Framed-IP-Address}', \ + framedipv6address = '%{Framed-IPv6-Address}', \ + framedipv6prefix = '%{Framed-IPv6-Prefix}', \ + framedinterfaceid = '%{Framed-Interface-Id}', \ + delegatedipv6prefix = '%{Delegated-IPv6-Prefix}', \ + AcctUpdateTime = ${....event_timestamp}, \ + AcctSessionTime = %{%{Acct-Session-Time}:-NULL}, \ + AcctInputOctets = '%{%{Acct-Input-Gigawords}:-0}' \ + << 32 | '%{%{Acct-Input-Octets}:-0}', \ + AcctOutputOctets = '%{%{Acct-Output-Gigawords}:-0}' \ + << 32 | '%{%{Acct-Output-Octets}:-0}' \ + WHERE UserName = '%{SQL-User-Name}' \ + AND NASIPAddress = '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}' \ + AND NASPortId = '%{%{NAS-Port-ID}:-%{NAS-Port}}' \ + AND NASPortType = '%{NAS-Port-Type}' \ + AND AcctStopTime IS NULL" + + } + + stop { + # + # Session has terminated, update the stop time and statistics. + # + query = "\ + UPDATE ${....acct_table2} SET \ + acctstoptime = ${....event_timestamp}, \ + acctsessiontime = %{%{Acct-Session-Time}:-NULL}, \ + acctinputoctets = '%{%{Acct-Input-Gigawords}:-0}' \ + << 32 | '%{%{Acct-Input-Octets}:-0}', \ + acctoutputoctets = '%{%{Acct-Output-Gigawords}:-0}' \ + << 32 | '%{%{Acct-Output-Octets}:-0}', \ + acctterminatecause = '%{Acct-Terminate-Cause}', \ + connectinfo_stop = '%{Connect-Info}' \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-Id}'" + + # + # The update condition matched no existing sessions. Use + # the values provided in the update to create a new session. + # + query = "\ + INSERT INTO ${....acct_table2} \ + (${...column_list}) \ + VALUES \ + ('%{Acct-Session-Id}', \ + '%{Acct-Unique-Session-Id}', \ + '%{SQL-User-Name}', \ + '%{Realm}', \ + '%{NAS-IP-Address}', \ + '%{%{NAS-Port-ID}:-%{NAS-Port}}', \ + '%{NAS-Port-Type}', \ + FROM_UNIXTIME(${....event_timestamp_epoch} - %{%{Acct-Session-Time}:-0}), \ + ${....event_timestamp}, \ + ${....event_timestamp}, \ + %{%{Acct-Session-Time}:-NULL}, \ + '%{Acct-Authentic}', \ + '', \ + '%{Connect-Info}', \ + '%{%{Acct-Input-Gigawords}:-0}' << 32 | '%{%{Acct-Input-Octets}:-0}', \ + '%{%{Acct-Output-Gigawords}:-0}' << 32 | '%{%{Acct-Output-Octets}:-0}', \ + '%{Called-Station-Id}', \ + '%{Calling-Station-Id}', \ + '%{Acct-Terminate-Cause}', \ + '%{Service-Type}', \ + '%{Framed-Protocol}', \ + '%{Framed-IP-Address}', \ + '%{Framed-IPv6-Address}', \ + '%{Framed-IPv6-Prefix}', \ + '%{Framed-Interface-Id}', \ + '%{Delegated-IPv6-Prefix}' \ + ${....class.packet_xlat})" + + # + # When using "sql_session_start", you should comment out + # the previous query, and enable this one. + # + # Just change the previous query to "-query", + # and this one to "query". The previous one + # will be ignored, and this one will be + # enabled. + # + -query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctSessionId = '%{Acct-Session-Id}', \ + AcctUniqueId = '%{Acct-Unique-Session-Id}', \ + AcctAuthentic = '%{Acct-Authentic}', \ + ConnectInfo_start = '%{Connect-Info}', \ + ServiceType = '%{Service-Type}', \ + FramedProtocol = '%{Framed-Protocol}', \ + framedipaddress = '%{Framed-IP-Address}', \ + framedipv6address = '%{Framed-IPv6-Address}', \ + framedipv6prefix = '%{Framed-IPv6-Prefix}', \ + framedinterfaceid = '%{Framed-Interface-Id}', \ + delegatedipv6prefix = '%{Delegated-IPv6-Prefix}', \ + AcctStopTime = ${....event_timestamp}, \ + AcctUpdateTime = ${....event_timestamp}, \ + AcctSessionTime = %{Acct-Session-Time}, \ + AcctInputOctets = '%{%{Acct-Input-Gigawords}:-0}' \ + << 32 | '%{%{Acct-Input-Octets}:-0}', \ + AcctOutputOctets = '%{%{Acct-Output-Gigawords}:-0}' \ + << 32 | '%{%{Acct-Output-Octets}:-0}', \ + AcctTerminateCause = '%{Acct-Terminate-Cause}', \ + ConnectInfo_stop = '%{Connect-Info}' \ + WHERE UserName = '%{SQL-User-Name}' \ + AND NASIPAddress = '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}' \ + AND NASPortId = '%{%{NAS-Port-ID}:-%{NAS-Port}}' \ + AND NASPortType = '%{NAS-Port-Type}' \ + AND AcctStopTime IS NULL" + + } + + # + # No Acct-Status-Type == ignore the packet + # + accounting { + query = "SELECT true" + } + } +} + + +####################################################################### +# Authentication Logging Queries +####################################################################### +# postauth_query - Insert some info after authentication +####################################################################### + +post-auth { + # Write SQL queries to a logfile. This is potentially useful for bulk inserts + # when used with the rlm_sql_null driver. +# logfile = ${logdir}/post-auth.sql + + query = "\ + INSERT INTO ${..postauth_table} \ + (username, pass, reply, authdate ${..class.column_name}) \ + VALUES ( \ + '%{SQL-User-Name}', \ + '%{%{User-Password}:-%{Chap-Password}}', \ + '%{reply:Packet-Type}', \ + '%S.%M' \ + ${..class.reply_xlat})" +} diff --git a/raddb/mods-config/sql/main/mysql/schema.sql b/raddb/mods-config/sql/main/mysql/schema.sql new file mode 100644 index 0000000..84846b2 --- /dev/null +++ b/raddb/mods-config/sql/main/mysql/schema.sql @@ -0,0 +1,179 @@ +########################################################################### +# $Id$ # +# # +# schema.sql rlm_sql - FreeRADIUS SQL Module # +# # +# Database schema for MySQL rlm_sql module # +# # +# To load: # +# mysql -uroot -prootpass radius < schema.sql # +# # +# Mike Machado <mike@innercite.com> # +########################################################################### +# +# Table structure for table 'radacct' +# + +CREATE TABLE IF NOT EXISTS radacct ( + radacctid bigint(21) NOT NULL auto_increment, + acctsessionid varchar(64) NOT NULL default '', + acctuniqueid varchar(32) NOT NULL default '', + username varchar(64) NOT NULL default '', + realm varchar(64) default '', + nasipaddress varchar(15) NOT NULL default '', + nasportid varchar(32) default NULL, + nasporttype varchar(32) default NULL, + acctstarttime datetime NULL default NULL, + acctupdatetime datetime NULL default NULL, + acctstoptime datetime NULL default NULL, + acctinterval int(12) default NULL, + acctsessiontime int(12) unsigned default NULL, + acctauthentic varchar(32) default NULL, + connectinfo_start varchar(128) default NULL, + connectinfo_stop varchar(128) default NULL, + acctinputoctets bigint(20) default NULL, + acctoutputoctets bigint(20) default NULL, + calledstationid varchar(50) NOT NULL default '', + callingstationid varchar(50) NOT NULL default '', + acctterminatecause varchar(32) NOT NULL default '', + servicetype varchar(32) default NULL, + framedprotocol varchar(32) default NULL, + framedipaddress varchar(15) NOT NULL default '', + framedipv6address varchar(45) NOT NULL default '', + framedipv6prefix varchar(45) NOT NULL default '', + framedinterfaceid varchar(44) NOT NULL default '', + delegatedipv6prefix varchar(45) NOT NULL default '', + class varchar(64) default NULL, + PRIMARY KEY (radacctid), + UNIQUE KEY acctuniqueid (acctuniqueid), + KEY username (username), + KEY framedipaddress (framedipaddress), + KEY framedipv6address (framedipv6address), + KEY framedipv6prefix (framedipv6prefix), + KEY framedinterfaceid (framedinterfaceid), + KEY delegatedipv6prefix (delegatedipv6prefix), + KEY acctsessionid (acctsessionid), + KEY acctsessiontime (acctsessiontime), + KEY acctstarttime (acctstarttime), + KEY acctinterval (acctinterval), + KEY acctstoptime (acctstoptime), + KEY nasipaddress (nasipaddress), + KEY class (class) +) ENGINE = INNODB; + +# +# Table structure for table 'radcheck' +# + +CREATE TABLE IF NOT EXISTS radcheck ( + id int(11) unsigned NOT NULL auto_increment, + username varchar(64) NOT NULL default '', + attribute varchar(64) NOT NULL default '', + op char(2) NOT NULL DEFAULT '==', + value varchar(253) NOT NULL default '', + PRIMARY KEY (id), + KEY username (username(32)) +); + +# +# Table structure for table 'radgroupcheck' +# + +CREATE TABLE IF NOT EXISTS radgroupcheck ( + id int(11) unsigned NOT NULL auto_increment, + groupname varchar(64) NOT NULL default '', + attribute varchar(64) NOT NULL default '', + op char(2) NOT NULL DEFAULT '==', + value varchar(253) NOT NULL default '', + PRIMARY KEY (id), + KEY groupname (groupname(32)) +); + +# +# Table structure for table 'radgroupreply' +# + +CREATE TABLE IF NOT EXISTS radgroupreply ( + id int(11) unsigned NOT NULL auto_increment, + groupname varchar(64) NOT NULL default '', + attribute varchar(64) NOT NULL default '', + op char(2) NOT NULL DEFAULT '=', + value varchar(253) NOT NULL default '', + PRIMARY KEY (id), + KEY groupname (groupname(32)) +); + +# +# Table structure for table 'radreply' +# + +CREATE TABLE IF NOT EXISTS radreply ( + id int(11) unsigned NOT NULL auto_increment, + username varchar(64) NOT NULL default '', + attribute varchar(64) NOT NULL default '', + op char(2) NOT NULL DEFAULT '=', + value varchar(253) NOT NULL default '', + PRIMARY KEY (id), + KEY username (username(32)) +); + + +# +# Table structure for table 'radusergroup' +# + +CREATE TABLE IF NOT EXISTS radusergroup ( + id int(11) unsigned NOT NULL auto_increment, + username varchar(64) NOT NULL default '', + groupname varchar(64) NOT NULL default '', + priority int(11) NOT NULL default '1', + PRIMARY KEY (id), + KEY username (username(32)) +); + +# +# Table structure for table 'radpostauth' +# +# Note: MySQL versions since 5.6.4 support fractional precision timestamps +# which we use here. Replace the authdate definition with the following +# if your software is too old: +# +# authdate timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +# +CREATE TABLE IF NOT EXISTS radpostauth ( + id int(11) NOT NULL auto_increment, + username varchar(64) NOT NULL default '', + pass varchar(64) NOT NULL default '', + reply varchar(32) NOT NULL default '', + authdate timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + class varchar(64) default NULL, + PRIMARY KEY (id), + KEY username (username), + KEY class (class) +) ENGINE = INNODB; + +# +# Table structure for table 'nas' +# +CREATE TABLE IF NOT EXISTS nas ( + id int(10) NOT NULL auto_increment, + nasname varchar(128) NOT NULL, + shortname varchar(32), + type varchar(30) DEFAULT 'other', + ports int(5), + secret varchar(60) DEFAULT 'secret' NOT NULL, + server varchar(64), + community varchar(50), + description varchar(200) DEFAULT 'RADIUS Client', + PRIMARY KEY (id), + KEY nasname (nasname) +) ENGINE = INNODB; + +# +# Table structure for table 'nasreload' +# +CREATE TABLE IF NOT EXISTS nasreload ( + nasipaddress varchar(15) NOT NULL, + reloadtime datetime NOT NULL, + PRIMARY KEY (nasipaddress) +) ENGINE = INNODB; diff --git a/raddb/mods-config/sql/main/mysql/setup.sql b/raddb/mods-config/sql/main/mysql/setup.sql new file mode 100755 index 0000000..5ae98cc --- /dev/null +++ b/raddb/mods-config/sql/main/mysql/setup.sql @@ -0,0 +1,40 @@ +# -*- text -*- +## +## setup.sql -- MySQL commands for creating the RADIUS user. +## +## WARNING: You should change 'localhost' and 'radpass' +## to something else. Also update raddb/mods-available/sql +## with the new RADIUS password. +## +## $Id$ + +# +# Create default administrator for RADIUS +# +CREATE USER 'radius'@'localhost' IDENTIFIED BY 'radpass'; + +# +# The server can read the authorisation data +# +GRANT SELECT ON radius.radcheck TO 'radius'@'localhost'; +GRANT SELECT ON radius.radreply TO 'radius'@'localhost'; +GRANT SELECT ON radius.radusergroup TO 'radius'@'localhost'; +GRANT SELECT ON radius.radgroupcheck TO 'radius'@'localhost'; +GRANT SELECT ON radius.radgroupreply TO 'radius'@'localhost'; + +# +# The server can write accounting and post-auth data +# +GRANT SELECT, INSERT, UPDATE ON radius.radacct TO 'radius'@'localhost'; +GRANT SELECT, INSERT, UPDATE ON radius.radpostauth TO 'radius'@'localhost'; + +# +# The server can read the NAS data +# +GRANT SELECT ON radius.nas TO 'radius'@'localhost'; + +# +# In the case of the "lightweight accounting-on/off" strategy, the server also +# records NAS reload times +# +GRANT SELECT, INSERT, UPDATE ON radius.nasreload TO 'radius'@'localhost'; diff --git a/raddb/mods-config/sql/main/ndb/README b/raddb/mods-config/sql/main/ndb/README new file mode 100644 index 0000000..71f5aa3 --- /dev/null +++ b/raddb/mods-config/sql/main/ndb/README @@ -0,0 +1,5 @@ + The SQL schema and 'create admin user" scripts are here in order to +simplify the process of using MySQL cluster. + + The queries are NOT located here, because the database driver for +MySQL cluster is just "mysql", and not "ndb". diff --git a/raddb/mods-config/sql/main/ndb/schema.sql b/raddb/mods-config/sql/main/ndb/schema.sql new file mode 100644 index 0000000..d115d06 --- /dev/null +++ b/raddb/mods-config/sql/main/ndb/schema.sql @@ -0,0 +1,144 @@ +########################################################################### +# $Id$ # +# # +# schema.sql rlm_sql - FreeRADIUS SQL Module # +# # +# Database schema for MySQL Cluster. # +# The only difference between this file and ../mysql/schema.sql # +# is the definition of the storage engine. # +# # +# To load: # +# mysql -uroot -prootpass radius < schema.sql # +# # +# Mike Machado <mike@innercite.com> # +########################################################################### +# +# Table structure for table 'radacct' +# + +CREATE TABLE radacct ( + radacctid bigint(21) NOT NULL auto_increment, + acctsessionid varchar(64) NOT NULL default '', + acctuniqueid varchar(32) NOT NULL default '', + username varchar(64) NOT NULL default '', + realm varchar(64) default '', + nasipaddress varchar(15) NOT NULL default '', + nasportid varchar(32) default NULL, + nasporttype varchar(32) default NULL, + acctstarttime datetime NULL default NULL, + acctupdatetime datetime NULL default NULL, + acctstoptime datetime NULL default NULL, + acctinterval int(12) default NULL, + acctsessiontime int(12) unsigned default NULL, + acctauthentic varchar(32) default NULL, + connectinfo_start varchar(128) default NULL, + connectinfo_stop varchar(128) default NULL, + acctinputoctets bigint(20) default NULL, + acctoutputoctets bigint(20) default NULL, + calledstationid varchar(50) NOT NULL default '', + callingstationid varchar(50) NOT NULL default '', + acctterminatecause varchar(32) NOT NULL default '', + servicetype varchar(32) default NULL, + framedprotocol varchar(32) default NULL, + framedipaddress varchar(15) NOT NULL default '', + framedipv6address varchar(45) NOT NULL default '', + framedipv6prefix varchar(45) NOT NULL default '', + framedinterfaceid varchar(44) NOT NULL default '', + delegatedipv6prefix varchar(45) NOT NULL default '', + class varchar(64) default NULL, + PRIMARY KEY (radacctid), + UNIQUE KEY acctuniqueid (acctuniqueid), + KEY username (username), + KEY framedipaddress (framedipaddress), + KEY framedipv6address (framedipv6address), + KEY framedipv6prefix (framedipv6prefix), + KEY framedinterfaceid (framedinterfaceid), + KEY delegatedipv6prefix (delegatedipv6prefix), + KEY acctsessionid (acctsessionid), + KEY acctsessiontime (acctsessiontime), + KEY acctstarttime (acctstarttime), + KEY acctinterval (acctinterval), + KEY acctstoptime (acctstoptime), + KEY nasipaddress (nasipaddress) +) ENGINE=ndbcluster; + +# +# Table structure for table 'radcheck' +# + +CREATE TABLE radcheck ( + id int(11) unsigned NOT NULL auto_increment, + username varchar(64) NOT NULL default '', + attribute varchar(64) NOT NULL default '', + op char(2) NOT NULL DEFAULT '==', + value varchar(253) NOT NULL default '', + PRIMARY KEY (id), + KEY username (username(32)) +) ENGINE=ndbcluster; + +# +# Table structure for table 'radgroupcheck' +# + +CREATE TABLE radgroupcheck ( + id int(11) unsigned NOT NULL auto_increment, + groupname varchar(64) NOT NULL default '', + attribute varchar(64) NOT NULL default '', + op char(2) NOT NULL DEFAULT '==', + value varchar(253) NOT NULL default '', + PRIMARY KEY (id), + KEY groupname (groupname(32)) +) ENGINE=ndbcluster; + +# +# Table structure for table 'radgroupreply' +# + +CREATE TABLE radgroupreply ( + id int(11) unsigned NOT NULL auto_increment, + groupname varchar(64) NOT NULL default '', + attribute varchar(64) NOT NULL default '', + op char(2) NOT NULL DEFAULT '=', + value varchar(253) NOT NULL default '', + PRIMARY KEY (id), + KEY groupname (groupname(32)) +) ENGINE=ndbcluster; + +# +# Table structure for table 'radreply' +# + +CREATE TABLE radreply ( + id int(11) unsigned NOT NULL auto_increment, + username varchar(64) NOT NULL default '', + attribute varchar(64) NOT NULL default '', + op char(2) NOT NULL DEFAULT '=', + value varchar(253) NOT NULL default '', + PRIMARY KEY (id), + KEY username (username(32)) +) ENGINE=ndbcluster; + + +# +# Table structure for table 'radusergroup' +# + +CREATE TABLE radusergroup ( + username varchar(64) NOT NULL default '', + groupname varchar(64) NOT NULL default '', + priority int(11) NOT NULL default '1', + KEY username (username(32)) +) ENGINE=ndbcluster; + +# +# Table structure for table 'radpostauth' +# + +CREATE TABLE radpostauth ( + id int(11) NOT NULL auto_increment, + username varchar(64) NOT NULL default '', + pass varchar(64) NOT NULL default '', + reply varchar(32) NOT NULL default '', + authdate timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6), + PRIMARY KEY (id) +) ENGINE=ndbcluster; diff --git a/raddb/mods-config/sql/main/ndb/setup.sql b/raddb/mods-config/sql/main/ndb/setup.sql new file mode 100644 index 0000000..003fc10 --- /dev/null +++ b/raddb/mods-config/sql/main/ndb/setup.sql @@ -0,0 +1,25 @@ +# -*- text -*- +## +## admin.sql -- MySQL commands for creating the RADIUS user. +## +## WARNING: You should change 'localhost' and 'radpass' +## to something else. Also update raddb/mods-available/sql +## with the new RADIUS password. +## +## $Id$ + +# +# Create default administrator for RADIUS +# +CREATE USER 'radius'@'localhost'; +SET PASSWORD FOR 'radius'@'localhost' = PASSWORD('radpass'); + +# The server can read any table in SQL +GRANT ALL ON radius.* TO 'radius'@'localhost' identified by 'radpass'; +GRANT ALL ON radius.* TO 'radius'@'radsrvr' identified by 'radpass'; + +# The server can write to the accounting and post-auth logging table. +# +# i.e. +#GRANT ALL on radius.radacct TO 'radius'@'localhost' identified by 'radpass'; +#GRANT ALL on radius.radacct TO 'radius'@'radsrvr' identified by 'radpass'; diff --git a/raddb/mods-config/sql/main/oracle/process-radacct.sql b/raddb/mods-config/sql/main/oracle/process-radacct.sql new file mode 100644 index 0000000..858d946 --- /dev/null +++ b/raddb/mods-config/sql/main/oracle/process-radacct.sql @@ -0,0 +1,147 @@ +# -*- text -*- +# +# main/oracle/process-radacct.sql -- Schema extensions for processing radacct entries +# +# $Id$ + +-- --------------------------------- +-- - Per-user data usage over time - +-- --------------------------------- +-- +-- An extension to the standard schema to hold per-user data usage statistics +-- for arbitrary periods. +-- +-- The data_usage_by_period table is populated by periodically calling the +-- fr_new_data_usage_period stored procedure. +-- +-- This table can be queried in various ways to produce reports of aggregate +-- data use over time. For example, if the fr_new_data_usage_period SP is +-- invoked one per day just after midnight, to produce usage data with daily +-- granularity, then a reasonably accurate monthly bandwidth summary for a +-- given user could be obtained with: +-- +-- SELECT +-- MIN(TO_CHAR(period_start, 'YYYY-Month')) AS month, +-- SUM(acctinputoctets)/1000/1000/1000 AS GB_in, +-- SUM(acctoutputoctets)/1000/1000/1000 AS GB_out +-- FROM +-- data_usage_by_period +-- WHERE +-- username='bob' AND +-- period_end IS NOT NULL +-- GROUP BY +-- TRUNC(period_start,'month'); +-- +-- +----------------+----------------+-----------------+ +-- | MONTH | GB_IN | GB_OUT | +-- +----------------+----------------+-----------------+ +-- | 2019-July | 5.782279230000 | 50.545664820000 | +-- | 2019-August | 4.230543340000 | 48.523096420000 | +-- | 2019-September | 4.847360590000 | 48.631835480000 | +-- | 2019-October | 6.456763250000 | 51.686231930000 | +-- | 2019-November | 6.362537730000 | 52.385710570000 | +-- | 2019-December | 4.301524440000 | 50.762240270000 | +-- | 2020-January | 5.436280540000 | 49.067775280000 | +-- +----------------+----------------+-----------------+ +-- +CREATE TABLE data_usage_by_period ( + id NUMBER GENERATED BY DEFAULT AS IDENTITY, + username VARCHAR(64) NOT NULL, + period_start TIMESTAMP WITH TIME ZONE NOT NULL, + period_end TIMESTAMP WITH TIME ZONE, + acctinputoctets NUMERIC(19), + acctoutputoctets NUMERIC(19), + PRIMARY KEY (id) +); +CREATE UNIQUE INDEX idx_data_usage_by_period_username_period_start ON data_usage_by_period (username,period_start); +CREATE INDEX idx_data_usage_by_period_period_start ON data_usage_by_period (period_start); +CREATE INDEX idx_data_usage_by_period_period_end ON data_usage_by_period (period_end); + +-- +-- Stored procedure that when run with some arbitrary frequency, say +-- once per day by cron, will process the recent radacct entries to extract +-- time-windowed data containing acct{input,output}octets ("data usage") per +-- username, per period. +-- +-- Each invocation will create new rows in the data_usage_by_period tables +-- containing the data used by each user since the procedure was last invoked. +-- The intervals do not need to be identical but care should be taken to +-- ensure that the start/end of each period aligns well with any intended +-- reporting intervals. +-- +-- It can be invoked by running: +-- +-- CALL fr_new_data_usage_period(); +-- +-- +CREATE OR REPLACE PROCEDURE fr_new_data_usage_period +AS + v_start TIMESTAMP WITH TIME ZONE; + v_end TIMESTAMP WITH TIME ZONE; +BEGIN + + SELECT COALESCE(MAX(period_end) + NUMTODSINTERVAL(1,'SECOND'), TO_DATE('1970-01-01','YYYY-MM-DD')) INTO v_start FROM data_usage_by_period; + SELECT CAST(CURRENT_TIMESTAMP AS DATE) INTO v_end FROM dual; + + BEGIN + + -- + -- Add the data usage for the sessions that were active in the current + -- period to the table. Include all sessions that finished since the start + -- of this period as well as those still ongoing. + -- + MERGE INTO data_usage_by_period d + USING ( + SELECT + username, + MIN(v_start) period_start, + MIN(v_end) period_end, + SUM(acctinputoctets) AS acctinputoctets, + SUM(acctoutputoctets) AS acctoutputoctets + FROM + radacct + WHERE + acctstoptime > v_start OR + acctstoptime IS NULL + GROUP BY + username + ) s + ON ( d.username = s.username AND d.period_start = s.period_start ) + WHEN MATCHED THEN + UPDATE SET + acctinputoctets = d.acctinputoctets + s.acctinputoctets, + acctoutputoctets = d.acctoutputoctets + s.acctoutputoctets, + period_end = v_end + WHEN NOT MATCHED THEN + INSERT + (username, period_start, period_end, acctinputoctets, acctoutputoctets) + VALUES + (s.username, s.period_start, s.period_end, s.acctinputoctets, s.acctoutputoctets); + + -- + -- Create an open-ended "next period" for all ongoing sessions and carry a + -- negative value of their data usage to avoid double-accounting when we + -- process the next period. Their current data usage has already been + -- allocated to the current and possibly previous periods. + -- + INSERT INTO data_usage_by_period (username, period_start, period_end, acctinputoctets, acctoutputoctets) + SELECT * + FROM ( + SELECT + username, + v_end + NUMTODSINTERVAL(1,'SECOND'), + NULL, + 0 - SUM(acctinputoctets), + 0 - SUM(acctoutputoctets) + FROM + radacct + WHERE + acctstoptime IS NULL + GROUP BY + username + ) s; + + END; + +END; +/ diff --git a/raddb/mods-config/sql/main/oracle/queries.conf b/raddb/mods-config/sql/main/oracle/queries.conf new file mode 100644 index 0000000..e3dd767 --- /dev/null +++ b/raddb/mods-config/sql/main/oracle/queries.conf @@ -0,0 +1,684 @@ +# -*- text -*- +# +# main/oracle/queries.conf -- Oracle configuration for default schema (schema.sql) +# +# $Id$ + +####################################################################### +# Query config: Username +####################################################################### +# This is the username that will get substituted, escaped, and added +# as attribute 'SQL-User-Name'. '%{SQL-User-Name}' should be used below +# everywhere a username substitution is needed so you you can be sure +# the username passed from the client is escaped properly. +# +# Uncomment the next line, if you want the sql_user_name to mean: +# +# Use Stripped-User-Name, if it's there. +# Else use User-Name, if it's there, +# Else use hard-coded string "DEFAULT" as the user name. +#sql_user_name = "%{%{Stripped-User-Name}:-%{%{User-Name}:-DEFAULT}}" +# +sql_user_name = "%{User-Name}" + +####################################################################### +# Query config: Event-Timestamp +####################################################################### +# event_timestamp_epoch is the basis for the time inserted into +# accounting records. Typically this will be the Event-Timestamp of the +# accounting request, which is provided by a NAS. +# +# Uncomment the next line, if you want the timestamp to be based on the +# request reception time recorded by this server, for example if you +# distrust the provided Event-Timestamp. +#event_timestamp_epoch = "%l" + +event_timestamp_epoch = "%{%{integer:Event-Timestamp}:-%l}" + +# event_timestamp is the SQL snippet for converting an epoch timestamp +# to an SQL date. + +event_timestamp = "TO_DATE('1970-01-01','YYYY-MM-DD') + NUMTODSINTERVAL(${event_timestamp_epoch},'SECOND')" + +####################################################################### +# Query config: Class attribute +####################################################################### +# +# 3.0.22 and later have a "class" column in the accounting table. +# +# However, we do NOT want to break existing configurations by adding +# the Class attribute to the default queries. If we did that, then +# systems using newer versions of the server would fail, because +# there is no "class" column in their accounting tables. +# +# The solution to that is the following "class" subsection. If your +# database has a "class" column for the various tables, then you can +# uncomment the configuration items here. The queries below will +# then automatically insert the Class attribute into radacct, +# radpostauth, etc. +# +class { + # + # Delete the '#' character from each of the configuration + # items in this section. This change puts the Class + # attribute into the various tables. Leave the double-quoted + # string there, as the value for the configuration item. + # + # See also policy.d/accounting, and the "insert_acct_class" + # policy. You will need to list (or uncomment) + # "insert_acct_class" in the "post-auth" section in order to + # create a Class attribute. + # + column_name = # ", class" + packet_xlat = # ", '%{Class}'" + reply_xlat = # ", '%{reply:Class}'" +} + +####################################################################### +# Default profile +####################################################################### +# This is the default profile. It is found in SQL by group membership. +# That means that this profile must be a member of at least one group +# which will contain the corresponding check and reply items. +# This profile will be queried in the authorize section for every user. +# The point is to assign all users a default profile without having to +# manually add each one to a group that will contain the profile. +# The SQL module will also honor the User-Profile attribute. This +# attribute can be set anywhere in the authorize section (ie the users +# file). It is found exactly as the default profile is found. +# If it is set then it will *overwrite* the default profile setting. +# The idea is to select profiles based on checks on the incoming packets, +# not on user group membership. For example: +# -- users file -- +# DEFAULT Service-Type == Outbound-User, User-Profile := "outbound" +# DEFAULT Service-Type == Framed-User, User-Profile := "framed" +# +# By default the default_user_profile is not set +# +#default_user_profile = "DEFAULT" +# +# Determines if we will query the default_user_profile or the User-Profile +# if the user is not found. If the profile is found then we consider the user +# found. By default this is set to 'no'. +# +#query_on_not_found = no + + +####################################################################### +# NAS Query +####################################################################### +# This query retrieves the radius clients +# +# 0. Row ID (currently unused) +# 1. Name (or IP address) +# 2. Shortname +# 3. Type +# 4. Secret +# 5. Virtual server +####################################################################### + +client_query = "\ + SELECT id, nasname, shortname, type, secret, server \ + FROM ${client_table}" + +####################################################################### +# Authorization Queries +####################################################################### +# These queries compare the check items for the user +# in ${authcheck_table} and setup the reply items in +# ${authreply_table}. You can use any query/tables +# you want, but the return data for each row MUST +# be in the following order: +# +# 0. Row ID (currently unused) +# 1. UserName/GroupName +# 2. Item Attr Name +# 3. Item Attr Value +# 4. Item Attr Operation +####################################################################### +# +# WARNING: Oracle is case sensitive +# +# The main difference between MySQL and Oracle queries is the date format. +# You must use the TO_DATE function to transform the radius date format to +# the Oracle date format, and put NULL otherwise '0' in a void date field. +# +####################################################################### + +authorize_check_query = "\ + SELECT id, UserName, Attribute, Value, op \ + FROM ${authcheck_table} \ + WHERE Username = '%{SQL-User-Name}' \ + ORDER BY id" + +authorize_reply_query = "\ + SELECT id, UserName, Attribute, Value, op \ + FROM ${authreply_table} \ + WHERE Username = '%{SQL-User-Name}' \ + ORDER BY id" + +authorize_group_check_query = "\ + SELECT \ + ${groupcheck_table}.id, ${groupcheck_table}.GroupName, ${groupcheck_table}.Attribute, \ + ${groupcheck_table}.Value,${groupcheck_table}.op \ + FROM ${groupcheck_table}, ${usergroup_table} \ + WHERE ${usergroup_table}.Username = '%{SQL-User-Name}' \ + AND ${usergroup_table}.GroupName = ${groupcheck_table}.GroupName \ + ORDER BY ${groupcheck_table}.id" + +authorize_group_reply_query = "\ + SELECT \ + ${groupreply_table}.id, ${groupreply_table}.GroupName, ${groupreply_table}.Attribute, \ + ${groupreply_table}.Value, ${groupreply_table}.op \ + FROM ${groupreply_table}, ${usergroup_table} \ + WHERE ${usergroup_table}.Username = '%{SQL-User-Name}' \ + AND ${usergroup_table}.GroupName = ${groupreply_table}.GroupName \ + ORDER BY ${groupreply_table}.id" + +####################################################################### +# Simultaneous Use Checking Queries +####################################################################### +# simul_count_query - query for the number of current connections +# - If this is not defined, no simultaneous use checking +# - will be performed by this module instance +# simul_verify_query - query to return details of current connections for verification +# - Leave blank or commented out to disable verification step +# - Note that the returned field order should not be changed. +####################################################################### + +simul_count_query = "\ + SELECT COUNT(*) \ + FROM ${acct_table1} \ + WHERE UserName = '%{SQL-User-Name}' \ + AND AcctStopTime IS NULL" + +simul_verify_query = "\ + SELECT \ + RadAcctId, AcctSessionId, UserName, NASIPAddress, NASPortId, \ + FramedIPAddress, CallingStationId, FramedProtocol \ + FROM ${acct_table1} \ + WHERE UserName='%{SQL-User-Name}' \ + AND AcctStopTime IS NULL" + +####################################################################### +# Group Membership Queries +####################################################################### +# group_membership_query - Check user group membership +####################################################################### + +group_membership_query = "\ + SELECT GroupName \ + FROM ${usergroup_table} \ + WHERE UserName='%{SQL-User-Name}'" + +####################################################################### +# Accounting and Post-Auth Queries +####################################################################### +# These queries insert/update accounting and authentication records. +# The query to use is determined by the value of 'reference'. +# This value is used as a configuration path and should resolve to one +# or more 'query's. If reference points to multiple queries, and a query +# fails, the next query is executed. +# +# Behaviour is identical to the old 1.x/2.x module, except we can now +# fail between N queries, and query selection can be based on any +# combination of attributes, or custom 'Acct-Status-Type' values. +####################################################################### +accounting { + reference = "%{tolower:type.%{%{Acct-Status-Type}:-%{Request-Processing-Stage}}.query}" + + # Write SQL queries to a logfile. This is potentially useful for bulk inserts + # when used with the rlm_sql_null driver. +# logfile = ${logdir}/accounting.sql + + type { + accounting-on { + query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctStopTime = ${....event_timestamp}, \ + AcctSessionTime = ROUND((${....event_timestamp} - \ + TO_DATE(TO_CHAR(acctstarttime, 'yyyy-mm-dd hh24:mi:ss'),'yyyy-mm-dd hh24:mi:ss'))*86400), \ + AcctTerminateCause='%{%{Acct-Terminate-Cause}:-NAS-Reboot}', \ + AcctStopDelay = %{%{Acct-Delay-Time}:-0} \ + WHERE AcctStopTime IS NULL \ + AND NASIPAddress = '%{NAS-IP-Address}' \ + AND AcctStartTime <= ${....event_timestamp}" + } + + accounting-off { + query = "${..accounting-on.query}" + } + + # + # Implement the "sql_session_start" policy. + # See raddb/policy.d/accounting for more details. + # + # You also need to fix the other queries as + # documented below. Look for "sql_session_start". + # + post-auth { + query = "\ + INSERT INTO ${....acct_table1} (\ + AcctSessionId, \ + AcctUniqueId, \ + UserName, \ + Realm, \ + NASIPAddress, \ + NASPortId, \ + NASPortType, \ + AcctStartTime, \ + AcctStopTime, \ + AcctSessionTime, \ + AcctAuthentic, \ + ConnectInfo_start, \ + ConnectInfo_stop, \ + AcctInputOctets, \ + AcctOutputOctets, \ + CalledStationId, \ + CallingStationId, \ + AcctTerminateCause, \ + ServiceType, \ + FramedProtocol, \ + FramedIPAddress, \ + FramedIPv6Address, \ + FramedIPv6Prefix, \ + FramedInterfaceId, \ + DelegatedIPv6Prefix) \ + VALUES(\ + '%{Acct-Session-Id}', \ + '%{Acct-Unique-Session-Id}', \ + '%{SQL-User-Name}', \ + '%{Realm}', \ + '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}', \ + '%{%{NAS-Port-ID}:-%{NAS-Port}}', \ + '%{NAS-Port-Type}', \ + TO_DATE('%S','yyyy-mm-dd hh24:mi:ss'), \ + NULL, \ + 0, \ + '', \ + '%{Connect-Info}', \ + NULL, \ + 0, \ + 0, \ + '%{Called-Station-Id}', \ + '%{Calling-Station-Id}', \ + NULL, \ + '%{Service-Type}', \ + NULL, \ + '', \ + '', \ + '', \ + '', \ + '')" + + query = "\ + UPDATE ${....acct_table1} SET \ + AcctStartTime = TO_DATE('%S','yyyy-mm-dd hh24:mi:ss'), \ + ConnectInfo_start = '%{Connect-Info}', \ + AcctSessionId = '%{Acct-Session-Id}' \ + WHERE UserName = '%{SQL-User-Name}' \ + AND NASIPAddress = '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}' \ + AND NASPortId = '%{%{NAS-Port-ID}:-%{NAS-Port}}' \ + AND NASPortType = '%{NAS-Port-Type}' \ + AND AcctStopTime IS NULL" + } + + start { + query = "\ + INSERT INTO ${....acct_table1} (\ + RadAcctId, \ + AcctSessionId, \ + AcctUniqueId, \ + UserName, \ + Realm, \ + NASIPAddress, \ + NASPortId, \ + NASPortType, \ + AcctStartTime, \ + AcctStopTime, \ + AcctSessionTime, \ + AcctAuthentic, \ + ConnectInfo_start, \ + ConnectInfo_stop, \ + AcctInputOctets, \ + AcctOutputOctets, \ + CalledStationId, \ + CallingStationId, \ + AcctTerminateCause, \ + ServiceType, \ + FramedProtocol, \ + FramedIPAddress, \ + FramedIPv6Address, \ + FramedIPv6Prefix, \ + FramedInterfaceId, \ + DelegatedIPv6Prefix, \ + AcctStartDelay, \ + AcctStopDelay, \ + XAscendSessionSvrKey \ + ${....class.column_name}) \ + VALUES(\ + '', \ + '%{Acct-Session-Id}', \ + '%{Acct-Unique-Session-Id}', \ + '%{SQL-User-Name}', \ + '%{Realm}', \ + '%{NAS-IP-Address}', \ + '%{%{NAS-Port-ID}:-%{NAS-Port}}', \ + '%{NAS-Port-Type}', \ + ${....event_timestamp}, \ + NULL, \ + '0', \ + '%{Acct-Authentic}', \ + '%{Connect-Info}', \ + '', \ + '0', \ + '0', \ + '%{Called-Station-Id}', \ + '%{Calling-Station-Id}', \ + '', \ + '%{Service-Type}', \ + '%{Framed-Protocol}', \ + '%{Framed-IP-Address}', \ + '%{Framed-IPv6-Address}', \ + '%{Framed-IPv6-Prefix}', \ + '%{Framed-Interface-Id}', \ + '%{Delegated-IPv6-Prefix}', \ + '%{Acct-Delay-Time}', \ + '0', \ + '%{X-Ascend-Session-Svr-Key}' \ + ${....class.packet_xlat})" + + # + # When using "sql_session_start", you should comment out + # the previous query, and enable this one. + # + # Just change the previous query to "-query", + # and this one to "query". The previous one + # will be ignored, and this one will be + # enabled. + # + -query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctSessionId = '%{Acct-Session-Id}', \ + AcctUniqueId = '%{Acct-Unique-Session-Id}', \ + AcctAuthentic = '%{Acct-Authentic}', \ + ConnectInfo_start = '%{Connect-Info}', \ + ServiceType = '%{Service-Type}', \ + FramedProtocol = '%{Framed-Protocol}', \ + FramedIPAddress = NULLIF('%{Framed-IP-Address}', ''), \ + FramedIPv6Address = NULLIF('%{Framed-IPv6-Address}', ''), \ + FramedIPv6Prefix = NULLIF('%{Framed-IPv6-Prefix}', ''), \ + FramedInterfaceId = NULLIF('%{Framed-Interface-Id}', ''), \ + DelegatedIPv6Prefix = NULLIF('%{Delegated-IPv6-Prefix}', ''), \ + AcctStartTime = TO_DATE('%S','yyyy-mm-dd hh24:mi:ss'), \ + AcctSessionTime = '0' \ + WHERE UserName = '%{SQL-User-Name}' \ + AND NASIPAddress = '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}' \ + AND NASPortId = '%{%{NAS-Port-ID}:-%{NAS-Port}}' \ + AND NASPortType = '%{NAS-Port-Type}' \ + AND AcctStopTime IS NULL" + + query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctStartTime = ${....event_timestamp}, \ + AcctStartDelay = '%{%{Acct-Delay-Time}:-0}', \ + ConnectInfo_start = '%{Connect-Info}' \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-ID}' \ + AND AcctStopTime IS NULL" + } + + interim-update { + query = "\ + UPDATE ${....acct_table1} \ + SET \ + FramedIPAddress = NULLIF('%{Framed-IP-Address}', ''), \ + FramedIPv6Address = NULLIF('%{Framed-IPv6-Address}', ''), \ + FramedIPv6Prefix = NULLIF('%{Framed-IPv6-Prefix}', ''), \ + FramedInterfaceId = NULLIF('%{Framed-Interface-Id}', ''), \ + DelegatedIPv6Prefix = NULLIF('%{Delegated-IPv6-Prefix}', ''), \ + AcctSessionTime = '%{Acct-Session-Time}', \ + AcctInputOctets = '%{Acct-Input-Octets}' + \ + ('%{%{Acct-Input-Gigawords}:-0}' * 4294967296), \ + AcctOutputOctets = '%{Acct-Output-Octets}' + \ + ('%{%{Acct-Output-Gigawords}:-0}' * 4294967296) \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-ID}' \ + AND AcctStopTime IS NULL" + + query = "\ + INSERT into ${....acct_table1} (\ + RadAcctId, \ + AcctSessionId, \ + AcctUniqueId, \ + UserName, \ + Realm, \ + NASIPAddress, \ + NASPortId, \ + NASPortType, \ + AcctStartTime, \ + AcctSessionTime, \ + AcctAuthentic, \ + ConnectInfo_start, \ + AcctInputOctets, \ + AcctOutputOctets, \ + CalledStationId, \ + CallingStationId, \ + ServiceType, \ + FramedProtocol, \ + FramedIPAddress, \ + FramedIPv6Address, \ + FramedIPv6Prefix, \ + FramedInterfaceId, \ + DelegatedIPv6Prefix, \ + AcctStartDelay, \ + XAscendSessionSvrKey \ + ${....class.column_name}) \ + VALUES(\ + '', \ + '%{Acct-Session-Id}', \ + '%{Acct-Unique-Session-Id}', \ + '%{SQL-User-Name}', \ + '%{Realm}', \ + '%{NAS-IP-Address}', \ + '%{%{NAS-Port-ID}:-%{NAS-Port}}', \ + '%{NAS-Port-Type}', \ + NULL, \ + '%{Acct-Session-Time}', \ + '%{Acct-Authentic}', \ + '', \ + '%{Acct-Input-Octets}' + \ + ('%{%{Acct-Input-Gigawords}:-0}' * 4294967296), \ + '%{Acct-Output-Octets}' + \ + ('%{%{Acct-Output-Gigawords}:-0}' * 4294967296), \ + '%{Called-Station-Id}', \ + '%{Calling-Station-Id}', \ + '%{Service-Type}', \ + '%{Framed-Protocol}', \ + '%{Framed-IP-Address}', \ + '%{Framed-IPv6-Address}', \ + '%{Framed-IPv6-Prefix}', \ + '%{Framed-Interface-Id}', \ + '%{Delegated-IPv6-Prefix}', \ + '0', \ + '%{X-Ascend-Session-Svr-Key}' \ + ${....class.packet_xlat})" + + # + # When using "sql_session_start", you should comment out + # the previous query, and enable this one. + # + # Just change the previous query to "-query", + # and this one to "query". The previous one + # will be ignored, and this one will be + # enabled. + # + -query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctSessionId = '%{Acct-Session-Id}', \ + AcctUniqueId = '%{Acct-Unique-Session-Id}', \ + AcctAuthentic = '%{Acct-Authentic}', \ + ConnectInfo_start = '%{Connect-Info}', \ + ServiceType = '%{Service-Type}', \ + FramedProtocol = '%{Framed-Protocol}', \ + FramedIPAddress = NULLIF('%{Framed-IP-Address}', ''), \ + FramedIPv6Address = NULLIF('%{Framed-IPv6-Address}', ''), \ + FramedIPv6Prefix = NULLIF('%{Framed-IPv6-Prefix}', ''), \ + FramedInterfaceId = NULLIF('%{Framed-Interface-Id}', ''), \ + DelegatedIPv6Prefix = NULLIF('%{Delegated-IPv6-Prefix}', ''), \ + AcctSessionTime = '%{Acct-Session-Time}', \ + AcctInputOctets = '%{Acct-Input-Octets}' + \ + ('%{%{Acct-Input-Gigawords}:-0}' * 4294967296), \ + AcctOutputOctets = '%{Acct-Output-Octets}' + \ + ('%{%{Acct-Output-Gigawords}:-0}' * 4294967296) \ + WHERE UserName = '%{SQL-User-Name}' \ + AND NASIPAddress = '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}' \ + AND NASPortId = '%{%{NAS-Port-ID}:-%{NAS-Port}}' \ + AND NASPortType = '%{NAS-Port-Type}' \ + AND AcctStopTime IS NULL" + + } + + stop { + query = "\ + UPDATE ${....acct_table2} \ + SET \ + AcctStopTime = ${....event_timestamp}, \ + AcctSessionTime = '%{Acct-Session-Time}', \ + AcctInputOctets = '%{Acct-Input-Octets}' + \ + ('%{%{Acct-Input-Gigawords}:-0}' * 4294967296), \ + AcctOutputOctets = '%{Acct-Output-Octets}' + \ + ('%{%{Acct-Output-Gigawords}:-0}' * 4294967296), \ + AcctTerminateCause = '%{Acct-Terminate-Cause}', \ + AcctStopDelay = '%{%{Acct-Delay-Time}:-0}', \ + ConnectInfo_stop = '%{Connect-Info}' \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-ID}' \ + AND AcctStopTime IS NULL" + + query = "\ + INSERT into ${....acct_table2} (\ + RadAcctId, \ + AcctSessionId, \ + AcctUniqueId, \ + UserName, \ + Realm, \ + NASIPAddress, \ + NASPortId, \ + NASPortType, \ + AcctStartTime, \ + AcctStopTime, \ + AcctSessionTime, \ + AcctAuthentic, \ + ConnectInfo_start, \ + ConnectInfo_stop, \ + AcctInputOctets, \ + AcctOutputOctets, \ + CalledStationId, \ + CallingStationId, \ + AcctTerminateCause, \ + ServiceType, \ + FramedProtocol, \ + FramedIPAddress, \ + FramedIPv6Address, \ + FramedIPv6Prefix, \ + FramedInterfaceId, \ + DelegatedIPv6Prefix, \ + AcctStartDelay, \ + AcctStopDelay \ + ${....class.column_name}) \ + VALUES(\ + '', \ + '%{Acct-Session-Id}', \ + '%{Acct-Unique-Session-Id}', \ + '%{SQL-User-Name}', \ + '%{Realm}', \ + '%{NAS-IP-Address}', \ + '%{%{NAS-Port-ID}:-%{NAS-Port}}', \ + '%{NAS-Port-Type}', \ + NULL, \ + ${....event_timestamp}, \ + '%{Acct-Session-Time}', \ + '%{Acct-Authentic}', \ + NULL, \ + '%{Connect-Info}', \ + '%{Acct-Input-Octets}' + \ + ('%{%{Acct-Input-Gigawords}:-0}' * 4294967296), \ + '%{Acct-Output-Octets}' + \ + ('%{%{Acct-Output-Gigawords}:-0}' * 4294967296), \ + '%{Called-Station-Id}', \ + '%{Calling-Station-Id}', \ + '%{Acct-Terminate-Cause}', \ + '%{Service-Type}', \ + '%{Framed-Protocol}', \ + '%{Framed-IP-Address}', \ + '%{Framed-IPv6-Address}', \ + '%{Framed-IPv6-Prefix}', \ + '%{Framed-Interface-Id}', \ + '%{Delegated-IPv6-Prefix}', \ + '0', \ + '%{%{Acct-Delay-Time}:-0}' \ + ${....class.packet_xlat})" + + # + # When using "sql_session_start", you should comment out + # the previous query, and enable this one. + # + # Just change the previous query to "-query", + # and this one to "query". The previous one + # will be ignored, and this one will be + # enabled. + # + -query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctSessionId = '%{Acct-Session-Id}', \ + AcctUniqueId = '%{Acct-Unique-Session-Id}', \ + AcctAuthentic = '%{Acct-Authentic}', \ + ConnectInfo_start = '%{Connect-Info}', \ + ServiceType = '%{Service-Type}', \ + FramedProtocol = '%{Framed-Protocol}', \ + FramedIPAddress = NULLIF('%{Framed-IP-Address}', ''), \ + FramedIPv6Address = NULLIF('%{Framed-IPv6-Address}', ''), \ + FramedIPv6Prefix = NULLIF('%{Framed-IPv6-Prefix}', ''), \ + FramedInterfaceId = NULLIF('%{Framed-Interface-Id}', ''), \ + DelegatedIPv6Prefix = NULLIF('%{Delegated-IPv6-Prefix}', ''), \ + AcctStopTime = TO_DATE('%S','yyyy-mm-dd hh24:mi:ss'), \ + AcctSessionTime = '%{Acct-Session-Time}', \ + AcctInputOctets = '%{Acct-Input-Octets}' + \ + ('%{%{Acct-Input-Gigawords}:-0}' * 4294967296), \ + AcctOutputOctets = '%{Acct-Output-Octets}' + \ + ('%{%{Acct-Output-Gigawords}:-0}' * 4294967296), \ + AcctTerminateCause = '%{Acct-Terminate-Cause}', \ + ConnectInfo_stop = '%{Connect-Info}' \ + WHERE UserName = '%{SQL-User-Name}' \ + AND NASIPAddress = '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}' \ + AND NASPortId = '%{%{NAS-Port-ID}:-%{NAS-Port}}' \ + AND NASPortType = '%{NAS-Port-Type}' \ + AND AcctStopTime IS NULL" + + } + } +} + +####################################################################### +# Authentication Logging Queries +####################################################################### +# postauth_query - Insert some info after authentication +####################################################################### + +post-auth { + # Write SQL queries to a logfile. This is potentially useful for bulk inserts + # when used with the rlm_sql_null driver. +# logfile = ${logdir}/post-auth.sql + query = "\ + INSERT INTO ${..postauth_table} \ + (username, pass, reply, authdate ${..class.column_name}) \ + VALUES (\ + '%{User-Name}', \ + '%{%{User-Password}:-%{Chap-Password}}', \ + '%{reply:Packet-Type}', \ + TO_TIMESTAMP('%S.%M','YYYY-MM-DDHH24:MI:SS.FF') \ + ${..class.reply_xlat})" +} diff --git a/raddb/mods-config/sql/main/oracle/schema.sql b/raddb/mods-config/sql/main/oracle/schema.sql new file mode 100644 index 0000000..96cde18 --- /dev/null +++ b/raddb/mods-config/sql/main/oracle/schema.sql @@ -0,0 +1,204 @@ +/* + * $Id$ + * + * Oracle schema for FreeRADIUS + * + * + * NOTE: Which columns are NULLable?? + */ + +/* + * Table structure for table 'radacct' + */ +CREATE TABLE radacct ( + radacctid INT PRIMARY KEY, + acctsessionid VARCHAR(96) NOT NULL, + acctuniqueid VARCHAR(32), + username VARCHAR(64) NOT NULL, + realm VARCHAR(64), + nasipaddress VARCHAR(15) NOT NULL, + nasportid VARCHAR(32), + nasporttype VARCHAR(32), + acctstarttime TIMESTAMP WITH TIME ZONE, + acctstoptime TIMESTAMP WITH TIME ZONE, + acctsessiontime NUMERIC(19), + acctauthentic VARCHAR(32), + connectinfo_start VARCHAR(128), + connectinfo_stop VARCHAR(128), + acctinputoctets NUMERIC(19), + acctoutputoctets NUMERIC(19), + calledstationid VARCHAR(50), + callingstationid VARCHAR(50), + acctterminatecause VARCHAR(32), + servicetype VARCHAR(32), + framedprotocol VARCHAR(32), + framedipaddress VARCHAR(15), + framedipv6address VARCHAR(45), + framedipv6prefix VARCHAR(45), + framedinterfaceid VARCHAR(44), + delegatedipv6prefix VARCHAR(45), + acctstartdelay NUMERIC(12), + acctstopdelay NUMERIC(12), + XAscendSessionSvrKey VARCHAR(10), + Class VARCHAR(64) +); + +CREATE UNIUQE INDEX radacct_idx0 + ON radacct(acctuniqueid); +CREATE UNIQUE INDEX radacct_idx1 + ON radacct(acctsessionid,username,acctstarttime, + acctstoptime,nasipaddress,framedipaddress,framedipv6address,framedipv6prefix,framedinterfaceid,delegatedipv6prefix); +CREATE INDEX radacct_idx2 + ON radacct(class); + +CREATE SEQUENCE radacct_seq START WITH 1 INCREMENT BY 1; + +/* Trigger to emulate a serial # on the primary key */ +CREATE OR REPLACE TRIGGER radacct_serialnumber + BEFORE INSERT OR UPDATE OF radacctid ON radacct + FOR EACH ROW + BEGIN + if ( :new.radacctid = 0 or :new.radacctid is null ) then + SELECT radacct_seq.nextval into :new.radacctid from dual; + end if; + END; +/ + +/* + * Table structure for table 'radcheck' + */ +CREATE TABLE radcheck ( + id INT PRIMARY KEY, + username VARCHAR(30) NOT NULL, + attribute VARCHAR(64), + op VARCHAR(2) NOT NULL, + value VARCHAR(40) +); +CREATE SEQUENCE radcheck_seq START WITH 1 INCREMENT BY 1; + +/* Trigger to emulate a serial # on the primary key */ +CREATE OR REPLACE TRIGGER radcheck_serialnumber + BEFORE INSERT OR UPDATE OF id ON radcheck + FOR EACH ROW + BEGIN + if ( :new.id = 0 or :new.id is null ) then + SELECT radcheck_seq.nextval into :new.id from dual; + end if; + END; +/ + +/* + * Table structure for table 'radgroupcheck' + */ +CREATE TABLE radgroupcheck ( + id INT PRIMARY KEY, + groupname VARCHAR(20) NOT NULL, + attribute VARCHAR(64), + op CHAR(2) NOT NULL, + value VARCHAR(40) +); +CREATE SEQUENCE radgroupcheck_seq START WITH 1 INCREMENT BY 1; + +/* + * Table structure for table 'radgroupreply' + */ +CREATE TABLE radgroupreply ( + id INT PRIMARY KEY, + GroupName VARCHAR(20) NOT NULL, + Attribute VARCHAR(64), + op CHAR(2) NOT NULL, + Value VARCHAR(40) +); +CREATE SEQUENCE radgroupreply_seq START WITH 1 INCREMENT BY 1; + +/* + * Table structure for table 'radreply' + */ +CREATE TABLE radreply ( + id INT PRIMARY KEY, + UserName VARCHAR(30) NOT NULL, + Attribute VARCHAR(64), + op CHAR(2) NOT NULL, + Value VARCHAR(40) +); +CREATE INDEX radreply_idx1 ON radreply(UserName); +CREATE SEQUENCE radreply_seq START WITH 1 INCREMENT BY 1; + +/* Trigger to emulate a serial # on the primary key */ +CREATE OR REPLACE TRIGGER radreply_serialnumber + BEFORE INSERT OR UPDATE OF id ON radreply + FOR EACH ROW + BEGIN + if ( :new.id = 0 or :new.id is null ) then + SELECT radreply_seq.nextval into :new.id from dual; + end if; + END; +/ + +/* + * Table structure for table 'radusergroup' + */ +CREATE TABLE radusergroup ( + id INT PRIMARY KEY, + UserName VARCHAR(30) NOT NULL, + GroupName VARCHAR(30) +); +CREATE SEQUENCE radusergroup_seq START WITH 1 INCREMENT BY 1; + +/* Trigger to emulate a serial # on the primary key */ +CREATE OR REPLACE TRIGGER radusergroup_serialnumber + BEFORE INSERT OR UPDATE OF id ON radusergroup + FOR EACH ROW + BEGIN + if ( :new.id = 0 or :new.id is null ) then + SELECT radusergroup_seq.nextval into :new.id from dual; + end if; + END; +/ + + +CREATE TABLE radpostauth ( + id INT PRIMARY KEY, + UserName VARCHAR(64) NOT NULL, + Pass VARCHAR(64), + Reply VARCHAR(64), + AuthDate TIMESTAMP(6) WITH TIME ZONE, + Class VARCHAR(64) +); +CREATE INDEX radpostauth_idx0 + ON radpostauth(UserName); +CREATE INDEX radpostauth_idx1 + ON radpostauth(class); + +CREATE SEQUENCE radpostauth_seq START WITH 1 INCREMENT BY 1; + +CREATE OR REPLACE TRIGGER radpostauth_TRIG + BEFORE INSERT OR UPDATE OF id ON radpostauth + FOR EACH ROW + BEGIN + if ( :new.id = 0 or :new.id is null ) then + SELECT radpostauth_seq.nextval into :new.id from dual; + end if; + if (:new.AuthDate is null) then + select systimestamp into :new.AuthDate from dual; + end if; + END; + +/ + +/* + * Table structure for table 'nas' + */ +CREATE TABLE nas ( + id INT PRIMARY KEY, + nasname VARCHAR(128), + shortname VARCHAR(32), + type VARCHAR(30), + ports INT, + secret VARCHAR(60), + server VARCHAR(64), + community VARCHAR(50), + description VARCHAR(200) +); +CREATE SEQUENCE nas_seq START WITH 1 INCREMENT BY 1; + diff --git a/raddb/mods-config/sql/main/postgresql/extras/cisco_h323_db_schema.sql b/raddb/mods-config/sql/main/postgresql/extras/cisco_h323_db_schema.sql new file mode 100644 index 0000000..0fabd43 --- /dev/null +++ b/raddb/mods-config/sql/main/postgresql/extras/cisco_h323_db_schema.sql @@ -0,0 +1,295 @@ +/* + * $Id$ + * + * --- Peter Nixon [ codemonkey@peternixon.net ] + * + * This is a custom SQL schema for doing H323 and SIP VoIP accounting + * with FreeRadius and Cisco equipment. It is currently known to work + * with 3640, 5300 and 5350 series as well as CSPS (Cisco SIP Proxy + * Server). It will scale A LOT better than the default radius schema + * which is designed for simple dialup installations of FreeRadius. + * + * For this schema to work properly you MUST use + * raddb/mods-config/sql/postgresql/voip-postpaid.conf rather than + * raddb/mods-config/sql/postgresql/dialup.conf + * + * If you wish to do RADIUS Authentication using the same database, + * you MUST use use raddb/mods-config/sql/postgresql/schema.sql as well as this schema. + */ + +/* + * Table structure for 'Start' tables + */ + +CREATE TABLE StartVoIP ( + RadAcctId BIGSERIAL PRIMARY KEY, + AcctTime TIMESTAMP with time zone NOT NULL, + h323SetupTime TIMESTAMP with time zone, + H323ConnectTime TIMESTAMP with time zone, + UserName VARCHAR(64), + RadiusServerName VARCHAR(32), + NASIPAddress INET NOT NULL, + CalledStationId VARCHAR(80), + CallingStationId VARCHAR(80), + AcctDelayTime INTEGER, + H323GWID VARCHAR(32), + h323CallOrigin VARCHAR(10), + CallID VARCHAR(80) NOT NULL, + processed BOOLEAN DEFAULT false +); +create index startvoipcombo on startvoip (AcctTime, nasipaddress); + + +CREATE TABLE StartTelephony ( + RadAcctId BIGSERIAL PRIMARY KEY, + AcctTime TIMESTAMP with time zone NOT NULL, + h323SetupTime TIMESTAMP with time zone, + H323ConnectTime TIMESTAMP with time zone, + UserName VARCHAR(64), + RadiusServerName VARCHAR(32), + NASIPAddress INET NOT NULL, + CalledStationId VARCHAR(80), + CallingStationId VARCHAR(80), + AcctDelayTime INTEGER, + H323GWID VARCHAR(32), + h323CallOrigin VARCHAR(10), + CallID VARCHAR(80) NOT NULL, + processed BOOLEAN DEFAULT false +); +create index starttelephonycombo on starttelephony (AcctTime, nasipaddress); + + + +/* + * Table structure for 'Stop' tables + */ +CREATE TABLE StopVoIP ( + RadAcctId BIGSERIAL PRIMARY KEY, + AcctTime TIMESTAMP with time zone NOT NULL, + H323SetupTime TIMESTAMP with time zone, + H323ConnectTime TIMESTAMP with time zone, + H323DisconnectTime TIMESTAMP with time zone, + UserName VARCHAR(32), + RadiusServerName VARCHAR(32), + NASIPAddress INET NOT NULL, + AcctSessionTime BIGINT, + AcctInputOctets BIGINT, + AcctOutputOctets BIGINT, + CalledStationId VARCHAR(80), + CallingStationId VARCHAR(80), + AcctDelayTime SMALLINT, + CiscoNASPort VARCHAR(1), + H323GWID VARCHAR(32), + H323CallOrigin VARCHAR(10), + H323DisconnectCause VARCHAR(20), + H323RemoteAddress INET, + H323VoiceQuality INTEGER, + CallID VARCHAR(80) NOT NULL, + processed BOOLEAN DEFAULT false +); +create UNIQUE index stopvoipcombo on stopvoip (AcctTime, nasipaddress, CallID); + + +CREATE TABLE StopTelephony ( + RadAcctId BIGSERIAL PRIMARY KEY, + AcctTime TIMESTAMP with time zone NOT NULL, + H323SetupTime TIMESTAMP with time zone NOT NULL, + H323ConnectTime TIMESTAMP with time zone NOT NULL, + H323DisconnectTime TIMESTAMP with time zone NOT NULL, + UserName VARCHAR(32) DEFAULT '' NOT NULL, + RadiusServerName VARCHAR(32), + NASIPAddress INET NOT NULL, + AcctSessionTime BIGINT, + AcctInputOctets BIGINT, + AcctOutputOctets BIGINT, + CalledStationId VARCHAR(80), + CallingStationId VARCHAR(80), + AcctDelayTime SMALLINT, + CiscoNASPort VARCHAR(16), + H323GWID VARCHAR(32), + H323CallOrigin VARCHAR(10), + H323DisconnectCause VARCHAR(20), + H323RemoteAddress INET, + H323VoiceQuality INTEGER, + CallID VARCHAR(80) NOT NULL, + processed BOOLEAN DEFAULT false +); +-- You can have more than one record that is identical except for CiscoNASPort if you have a dial peer hungroup +-- configured for multiple PRIs. +create UNIQUE index stoptelephonycombo on stoptelephony (AcctTime, nasipaddress, CallID, CiscoNASPort); + +/* + * Table structure for 'gateways' + * + * This table should list the IP addresses, names and locations of all your gateways + * This can be used to make more useful reports. + * + * Note: This table should be removed in favour of using the "nas" table. + */ + +CREATE TABLE gateways ( + gw_ip INET NOT NULL, + gw_name VARCHAR(32) NOT NULL, + gw_city VARCHAR(32) +); + + +/* + * Table structure for 'customers' + * + * This table should list your Customers names and company + * This can be used to make more useful reports. + */ + +CREATE TABLE customers ( + cust_id SERIAL NOT NULL, + company VARCHAR(32), + customer VARCHAR(32) +); + +/* + * Table structure for 'cust_gw' + * + * This table should list the IP addresses and Customer IDs of all your Customers gateways + * This can be used to make more useful reports. + */ + +CREATE TABLE cust_gw ( + cust_gw INET PRIMARY KEY, + cust_id INTEGER NOT NULL, + "location" VARCHAR(32) +); + + +CREATE VIEW customerip AS + SELECT gw.cust_gw AS ipaddr, cust.company, cust.customer, gw."location" FROM customers cust, cust_gw gw WHERE (cust.cust_id = gw.cust_id); + + +-- create plpgsql language (You need to be a database superuser to be able to do this) +CREATE FUNCTION "plpgsql_call_handler" () RETURNS LANGUAGE_HANDLER AS '$libdir/plpgsql' LANGUAGE C; +CREATE TRUSTED LANGUAGE "plpgsql" HANDLER "plpgsql_call_handler"; + +/* + * Function 'strip_dot' + * removes "." from the start of cisco timestamps + * + * From the cisco website: + * "A timestamp that is preceded by an asterisk (*) or a dot (.) may not be accurate. + * An asterisk (*) means that after a gateway reboot, the gateway clock was not manually set + * and the gateway has not synchronized with an NTP server yet. A dot (.) means the gateway + * NTP has lost synchronization with an NTP server." + * + * We therefore do not bother to strip asterisks (*) from timestamps, as you NEED ntp setup + * unless you don't care about billing at all! + * + * * Example useage: + * insert into mytable values (strip_dot('.16:46:02.356 EET Wed Dec 11 2002')); + * + */ + + +CREATE OR REPLACE FUNCTION strip_dot (VARCHAR) RETURNS TIMESTAMPTZ AS ' + DECLARE + original_timestamp ALIAS FOR $1; + BEGIN + IF original_timestamp = '''' THEN + RETURN NULL; + END IF; + IF substring(original_timestamp from 1 for 1) = ''.'' THEN + RETURN substring(original_timestamp from 2); + ELSE + RETURN original_timestamp; + END IF; + END; +' LANGUAGE 'plpgsql'; + + +CREATE OR REPLACE FUNCTION pick_id (VARCHAR, VARCHAR) RETURNS VARCHAR AS ' + DECLARE + h323confid ALIAS FOR $1; + callid ALIAS FOR $2; + BEGIN + IF h323confid <> '''' THEN + RETURN h323confid; + END IF; + IF callid <> '''' THEN + RETURN callid; + END IF; + RETURN NULL; + END; +' LANGUAGE 'plpgsql'; + + + +/* + * Table structure for 'isdn_error_codes' table + * + * Taken from cisco.com this data can be JOINED against h323DisconnectCause to + * give human readable error reports. + * + */ + + +CREATE TABLE isdn_error_codes ( + error_code VARCHAR(2) PRIMARY KEY, + desc_short VARCHAR(90), + desc_long TEXT +); + +/* + * Data for 'isdn_error_codes' table + */ + +INSERT INTO isdn_error_codes VALUES ('1', 'Unallocated (unassigned) number', 'The ISDN number was sent to the switch in the correct format; however, the number is not assigned to any destination equipment.'); +INSERT INTO isdn_error_codes VALUES ('10', 'Normal call clearing', 'Normal call clearing has occurred.'); +INSERT INTO isdn_error_codes VALUES ('11', 'User busy', 'The called system acknowledges the connection request but is unable to accept the call because all B channels are in use.'); +INSERT INTO isdn_error_codes VALUES ('12', 'No user responding', 'The connection cannot be completed because the destination does not respond to the call.'); +INSERT INTO isdn_error_codes VALUES ('13', 'No answer from user (user alerted)', 'The destination responds to the connection request but fails to complete the connection within the prescribed time. The problem is at the remote end of the connection.'); +INSERT INTO isdn_error_codes VALUES ('15', 'Call rejected', 'The destination is capable of accepting the call but rejected the call for an unknown reason.'); +INSERT INTO isdn_error_codes VALUES ('16', 'Number changed', 'The ISDN number used to set up the call is not assigned to any system.'); +INSERT INTO isdn_error_codes VALUES ('1A', 'Non-selected user clearing', 'The destination is capable of accepting the call but rejected the call because it was not assigned to the user.'); +INSERT INTO isdn_error_codes VALUES ('1B', 'Designation out of order', 'The destination cannot be reached because the interface is not functioning correctly, and a signaling message cannot be delivered. This might be a temporary condition, but it could last for an extended period of time. For example, the remote equipment might be turned off.'); +INSERT INTO isdn_error_codes VALUES ('1C', 'Invalid number format', 'The connection could be established because the destination address was presented in an unrecognizable format or because the destination address was incomplete.'); +INSERT INTO isdn_error_codes VALUES ('1D', 'Facility rejected', 'The facility requested by the user cannot be provided by the network.'); +INSERT INTO isdn_error_codes VALUES ('1E', 'Response to STATUS ENQUIRY', 'The status message was generated in direct response to the prior receipt of a status enquiry message.'); +INSERT INTO isdn_error_codes VALUES ('1F', 'Normal, unspecified', 'Reports the occurrence of a normal event when no standard cause applies. No action required.'); +INSERT INTO isdn_error_codes VALUES ('2', 'No route to specified transit network', 'The ISDN exchange is asked to route the call through an unrecognized intermediate network.'); +INSERT INTO isdn_error_codes VALUES ('22', 'No circuit/channel available', 'The connection cannot be established because no appropriate channel is available to take the call.'); +INSERT INTO isdn_error_codes VALUES ('26', 'Network out of order', 'The destination cannot be reached because the network is not functioning correctly, and the condition might last for an extended period of time. An immediate reconnect attempt will probably be unsuccessful.'); +INSERT INTO isdn_error_codes VALUES ('29', 'Temporary failure', 'An error occurred because the network is not functioning correctly. The problem will be resolved shortly.'); +INSERT INTO isdn_error_codes VALUES ('2A', 'Switching equipment congestion', 'The destination cannot be reached because the network switching equipment is temporarily overloaded.'); +INSERT INTO isdn_error_codes VALUES ('2B', 'Access information discarded', 'The network cannot provide the requested access information.'); +INSERT INTO isdn_error_codes VALUES ('2C', 'Requested circuit/channel not available', 'The remote equipment cannot provide the requested channel for an unknown reason. This might be a temporary problem.'); +INSERT INTO isdn_error_codes VALUES ('2F', 'Resources unavailable, unspecified', 'The requested channel or service is unavailable for an unknown reason. This might be a temporary problem.'); +INSERT INTO isdn_error_codes VALUES ('3', 'No route to destination', 'The call was routed through an intermediate network that does not serve the destination address.'); +INSERT INTO isdn_error_codes VALUES ('31', 'Quality of service unavailable', 'The requested quality of service cannot be provided by the network. This might be a subscription problem.'); +INSERT INTO isdn_error_codes VALUES ('32', 'Requested facility not subscribed', 'The remote equipment supports the requested supplementary service by subscription only.'); +INSERT INTO isdn_error_codes VALUES ('39', 'Bearer capability not authorized', 'The user requested a bearer capability that the network provides, but the user is not authorized to use it. This might be a subscription problem.'); +INSERT INTO isdn_error_codes VALUES ('3A', 'Bearer capability not presently available', 'The network normally provides the requested bearer capability, but it is unavailable at the present time. This might be due to a temporary network problem or to a subscription problem.'); +INSERT INTO isdn_error_codes VALUES ('3F', 'Service or option not available, unspecified', 'The network or remote equipment was unable to provide the requested service option for an unspecified reason. This might be a subscription problem.'); +INSERT INTO isdn_error_codes VALUES ('41', 'Bearer capability not implemented', 'The network cannot provide the bearer capability requested by the user.'); +INSERT INTO isdn_error_codes VALUES ('42', 'Channel type not implemented', 'The network or the destination equipment does not support the requested channel type.'); +INSERT INTO isdn_error_codes VALUES ('45', 'Requested facility not implemented', 'The remote equipment does not support the requested supplementary service.'); +INSERT INTO isdn_error_codes VALUES ('46', 'Only restricted digital information bearer capability is available', 'The network is unable to provide unrestricted digital information bearer capability.'); +INSERT INTO isdn_error_codes VALUES ('4F', 'Service or option not implemented, unspecified', 'The network or remote equipment is unable to provide the requested service option for an unspecified reason. This might be a subscription problem.'); +INSERT INTO isdn_error_codes VALUES ('51', 'Invalid call reference value', 'The remote equipment received a call with a call reference that is not currently in use on the user-network interface.'); +INSERT INTO isdn_error_codes VALUES ('52', 'Identified channel does not exist', 'The receiving equipment is requested to use a channel that is not activated on the interface for calls.'); +INSERT INTO isdn_error_codes VALUES ('53', 'A suspended call exists, but this call identity does not', 'The network received a call resume request. The call resume request contained a Call Identify information element that indicates that the call identity is being used for a suspended call.'); +INSERT INTO isdn_error_codes VALUES ('54', 'Call identity in use', 'The network received a call resume request. The call resume request contained a Call Identify information element that indicates that it is in use for a suspended call.'); +INSERT INTO isdn_error_codes VALUES ('55', 'No call suspended', 'The network received a call resume request when there was not a suspended call pending. This might be a transient error that will be resolved by successive call retries.'); +INSERT INTO isdn_error_codes VALUES ('56', 'Call having the requested call identity has been cleared', 'The network received a call resume request. The call resume request contained a Call Identity information element, which once indicated a suspended call. However, the suspended call was cleared either by timeout or by the remote user.'); +INSERT INTO isdn_error_codes VALUES ('58', 'Incompatible destination', 'Indicates that an attempt was made to connect to non-ISDN equipment. For example, to an analog line.'); +INSERT INTO isdn_error_codes VALUES ('5B', 'Invalid transit network selection', 'The ISDN exchange was asked to route the call through an unrecognized intermediate network.'); +INSERT INTO isdn_error_codes VALUES ('5F', 'Invalid message, unspecified', 'An invalid message was received, and no standard cause applies. This is usually due to a D-channel error. If this error occurs systematically, report it to your ISDN service provider.'); +INSERT INTO isdn_error_codes VALUES ('6', 'Channel unacceptable', 'The service quality of the specified channel is insufficient to accept the connection.'); +INSERT INTO isdn_error_codes VALUES ('60', 'Mandatory information element is missing', 'The receiving equipment received a message that did not include one of the mandatory information elements. This is usually due to a D-channel error. If this error occurs systematically, report it to your ISDN service provider.'); +INSERT INTO isdn_error_codes VALUES ('61', 'Message type non-existent or not implemented', 'The receiving equipment received an unrecognized message, either because the message type was invalid or because the message type was valid but not supported. The cause is due to either a problem with the remote configuration or a problem with the local D channel.'); +INSERT INTO isdn_error_codes VALUES ('62', 'Message not compatible with call state or message type non-existent or not implemented', 'The remote equipment received an invalid message, and no standard cause applies. This cause is due to a D-channel error. If this error occurs systematically, report it to your ISDN service provider.'); +INSERT INTO isdn_error_codes VALUES ('63', 'Information element non-existent or not implemented', 'The remote equipment received a message that includes information elements, which were not recognized. This is usually due to a D-channel error. If this error occurs systematically, report it to your ISDN service provider.'); +INSERT INTO isdn_error_codes VALUES ('64', 'Invalid information element contents', 'The remote equipment received a message that includes invalid information in the information element. This is usually due to a D-channel error.'); +INSERT INTO isdn_error_codes VALUES ('65', 'Message not compatible with call state', 'The remote equipment received an unexpected message that does not correspond to the current state of the connection. This is usually due to a D-channel error.'); +INSERT INTO isdn_error_codes VALUES ('66', 'Recovery on timer expires', 'An error-handling (recovery) procedure was initiated by a timer expiry. This is usually a temporary problem.'); +INSERT INTO isdn_error_codes VALUES ('6F', 'Protocol error, unspecified', 'An unspecified D-channel error when no other standard cause applies.'); +INSERT INTO isdn_error_codes VALUES ('7', 'Call awarded and being delivered in an established channel', 'The user is assigned an incoming call that is being connected to an already-established call channel.'); +INSERT INTO isdn_error_codes VALUES ('7F', 'Internetworking, unspecified', 'An event occurred, but the network does not provide causes for the action that it takes. The precise problem is unknown.'); + diff --git a/raddb/mods-config/sql/main/postgresql/extras/voip-postpaid.conf b/raddb/mods-config/sql/main/postgresql/extras/voip-postpaid.conf new file mode 100644 index 0000000..9f1449c --- /dev/null +++ b/raddb/mods-config/sql/main/postgresql/extras/voip-postpaid.conf @@ -0,0 +1,70 @@ +# -*- text -*- +## +## voip-postpaid.conf -- PostgreSQL configuration for H323 VoIP billingx +## (cisco_h323_db_schema.sql) +## +## $Id$ + + + ####################################################################### + # Query config: Username + ####################################################################### + # This is the username that will get substituted, escaped, and added + # as attribute 'SQL-User-Name'. '%{SQL-User-Name}' should be used below + # everywhere a username substitution is needed so you you can be sure + # the username passed from the client is escaped properly. + # + # Uncomment the next line, if you want the sql_user_name to mean: + # + # Use Stripped-User-Name, if it's there. + # Else use User-Name, if it's there, + # Else use hard-coded string "none" as the user name. + # + #sql_user_name = "%{%{Stripped-User-Name}:-%{%{User-Name}:-none}}" + # + sql_user_name = "%{User-Name}" + + accounting { + reference = "%{tolower:type.%{Acct-Status-Type}.query}" + + # Write SQL queries to a logfile. This is potentially useful for bulk inserts + # when used with the rlm_sql_null driver. +# logfile = ${logdir}/accounting.sql + + type { + start { + query = "INSERT INTO ${....acct_table1}%{h323-call-type} \ + (RadiusServerName, UserName, NASIPAddress, AcctTime, CalledStationId, \ + CallingStationId, AcctDelayTime, h323gwid, h323callorigin, \ + h323setuptime, H323ConnectTime, callid) \ + VALUES(\ + '${radius_server_name}', '%{SQL-User-Name}', \ + '%{NAS-IP-Address}', now(), '%{Called-Station-Id}', \ + '%{Calling-Station-Id}', '%{%{Acct-Delay-Time}:-0}', '%{h323-gw-id}', \ + '%{h323-call-origin}', strip_dot('%{h323-setup-time}'), \ + strip_dot('%{h323-connect-time}'), pick_id('%{h323-conf-id}', \ + '%{call-id}'))" + } + + stop { + query = "INSERT INTO $....acct_table2}%{h323-call-type} \ + (RadiusServerName, UserName, NASIPAddress, AcctTime, \ + AcctSessionTime, AcctInputOctets, AcctOutputOctets, CalledStationId, \ + CallingStationId, AcctDelayTime, H323RemoteAddress, H323VoiceQuality, \ + CiscoNASPort, h323callorigin, callid, h323connecttime, \ + h323disconnectcause, h323disconnecttime, h323gwid, h323setuptime) \ + VALUES(\ + '${radius_server_name}', '%{SQL-User-Name}', '%{NAS-IP-Address}', \ + NOW(), '%{%{Acct-Session-Time}:-0}', \ + '%{%{Acct-Input-Octets}:-0}', '%{%{Acct-Output-Octets}:-0}', \ + '%{Called-Station-Id}', '%{Calling-Station-Id}', \ + '%{%{Acct-Delay-Time}:-0}', NULLIF('%{h323-remote-address}', '')::inet, \ + NULLIF('%{h323-voice-quality}','')::integer, \ + NULLIF('%{Cisco-NAS-Port}', ''), \ + '%{h323-call-origin}', pick_id('%{h323-conf-id}', '%{call-id}'), \ + strip_dot('%{h323-connect-time}'), '%{h323-disconnect-cause}', \ + strip_dot('%{h323-disconnect-time}'), '%{h323-gw-id}', \ + strip_dot('%{h323-setup-time}'))" + } + } + } diff --git a/raddb/mods-config/sql/main/postgresql/process-radacct.sql b/raddb/mods-config/sql/main/postgresql/process-radacct.sql new file mode 100644 index 0000000..7a70724 --- /dev/null +++ b/raddb/mods-config/sql/main/postgresql/process-radacct.sql @@ -0,0 +1,278 @@ +# -*- text -*- +# +# main/postgresql/process-radacct.sql -- Schema extensions for processing radacct entries +# +# $Id$ + +-- --------------------------------- +-- - Per-user data usage over time - +-- --------------------------------- +-- +-- An extension to the standard schema to hold per-user data usage statistics +-- for arbitrary periods. +-- +-- The data_usage_by_period table is populated by periodically calling the +-- fr_new_data_usage_period stored procedure. +-- +-- This table can be queried in various ways to produce reports of aggregate +-- data use over time. For example, if the fr_new_data_usage_period SP is +-- invoked one per day just after midnight, to produce usage data with daily +-- granularity, then a reasonably accurate monthly bandwidth summary for a +-- given user could be obtained by queriing this table with: +-- +-- SELECT +-- TO_CHAR(CURRENT_TIMESTAMP, 'YYYY-Month') AS month, +-- TRUNC(SUM(acctinputoctets)/1000/1000/1000,9) AS gb_in, +-- TRUNC(SUM(acctoutputoctets)/1000/1000/1000,9) AS gb_out +-- FROM +-- data_usage_by_period +-- WHERE +-- username='bob' AND +-- period_end IS NOT NULL +-- GROUP BY +-- month; +-- +-- month | gb_in | gb_out +-- ----------------+-------------+-------------- +-- 2019-July | 5.782279231 | 50.545664824 +-- 2019-August | 4.230543344 | 48.523096424 +-- 2019-September | 4.847360599 | 48.631835488 +-- 2019-October | 6.456763254 | 51.686231937 +-- 2019-November | 6.362537735 | 52.385710572 +-- 2019-December | 4.301524442 | 50.762240277 +-- 2020-January | 5.436280545 | 49.067775286 +-- (7 rows) +-- +CREATE TABLE data_usage_by_period ( + username text, + period_start timestamp with time zone, + period_end timestamp with time zone, + acctinputoctets bigint, + acctoutputoctets bigint +); +ALTER TABLE data_usage_by_period ADD CONSTRAINT data_usage_by_period_pkey PRIMARY KEY (username, period_start); +CREATE INDEX data_usage_by_period_pkey_period_end ON data_usage_by_period(period_end); + + +-- +-- Stored procedure that when run with some arbitrary frequency, say +-- once per day by cron, will process the recent radacct entries to extract +-- time-windowed data containing acct{input,output}octets ("data usage") per +-- username, per period. +-- +-- Each invocation will create new rows in the data_usage_by_period tables +-- containing the data used by each user since the procedure was last invoked. +-- The intervals do not need to be identical but care should be taken to +-- ensure that the start/end of each period aligns well with any intended +-- reporting intervals. +-- +-- It can be invoked by running: +-- +-- SELECT fr_new_data_usage_period(); +-- +-- +CREATE OR REPLACE FUNCTION fr_new_data_usage_period () +RETURNS void +LANGUAGE plpgsql +AS $$ +DECLARE v_start timestamp; +DECLARE v_end timestamp; +BEGIN + + SELECT COALESCE(MAX(period_end) + INTERVAL '1 SECOND', TO_TIMESTAMP(0)) INTO v_start FROM data_usage_by_period; + SELECT DATE_TRUNC('second',CURRENT_TIMESTAMP) INTO v_end; + + -- + -- Add the data usage for the sessions that were active in the current + -- period to the table. Include all sessions that finished since the start + -- of this period as well as those still ongoing. + -- + INSERT INTO data_usage_by_period (username, period_start, period_end, acctinputoctets, acctoutputoctets) + SELECT * + FROM ( + SELECT + username, + v_start, + v_end, + SUM(acctinputoctets) AS acctinputoctets, + SUM(acctoutputoctets) AS acctoutputoctets + FROM + radacct + WHERE + acctstoptime > v_start OR + acctstoptime IS NULL + GROUP BY + username + ) AS s + ON CONFLICT ON CONSTRAINT data_usage_by_period_pkey + DO UPDATE + SET + acctinputoctets = data_usage_by_period.acctinputoctets + EXCLUDED.acctinputoctets, + acctoutputoctets = data_usage_by_period.acctoutputoctets + EXCLUDED.acctoutputoctets, + period_end = v_end; + + -- + -- Create an open-ended "next period" for all ongoing sessions and carry a + -- negative value of their data usage to avoid double-accounting when we + -- process the next period. Their current data usage has already been + -- allocated to the current and possibly previous periods. + -- + INSERT INTO data_usage_by_period (username, period_start, period_end, acctinputoctets, acctoutputoctets) + SELECT * + FROM ( + SELECT + username, + v_end + INTERVAL '1 SECOND', + NULL::timestamp, + 0 - SUM(acctinputoctets), + 0 - SUM(acctoutputoctets) + FROM + radacct + WHERE + acctstoptime IS NULL + GROUP BY + username + ) AS s; + +END +$$; + + +-- ------------------------------------------------------ +-- - "Lightweight" Accounting-On/Off strategy resources - +-- ------------------------------------------------------ +-- +-- The following resources are for use only when the "lightweight" +-- Accounting-On/Off strategy is enabled in queries.conf. +-- +-- Instead of bulk closing the radacct sessions belonging to a reloaded NAS, +-- this strategy leaves them open and records the NAS reload time in the +-- nasreload table. +-- +-- Where applicable, the onus is on the administator to: +-- +-- * Consider the nas reload times when deriving a list of +-- active/inactive sessions, and when determining the duration of sessions +-- interrupted by a NAS reload. (Refer to the view below.) +-- +-- * Close the affected sessions out of band. (Refer to the SP below.) +-- +-- +-- The radacct_with_reloads view presents the radacct table with two additional +-- columns: acctstoptime_with_reloads and acctsessiontime_with_reloads +-- +-- Where the session isn't closed (acctstoptime IS NULL), yet it started before +-- the last reload of the NAS (radacct.acctstarttime < nasreload.reloadtime), +-- the derived columns are set based on the reload time of the NAS (effectively +-- the point in time that the session was interrupted.) +-- +CREATE VIEW radacct_with_reloads AS +SELECT + a.*, + COALESCE(a.AcctStopTime, + CASE WHEN a.AcctStartTime < n.ReloadTime THEN n.ReloadTime END + ) AS AcctStopTime_With_Reloads, + COALESCE(a.AcctSessionTime, + CASE WHEN a.AcctStopTime IS NULL AND a.AcctStartTime < n.ReloadTime THEN + EXTRACT(EPOCH FROM (n.ReloadTime - a.AcctStartTime)) + END + ) AS AcctSessionTime_With_Reloads +FROM radacct a +LEFT OUTER JOIN nasreload n USING (nasipaddress); + + +-- +-- It may be desirable to periodically "close" radacct sessions belonging to a +-- reloaded NAS, replicating the "bulk close" Accounting-On/Off behaviour, +-- just not in real time. +-- +-- The fr_radacct_close_after_reload SP will set radacct.acctstoptime to +-- nasreload.reloadtime, calculate the corresponding radacct.acctsessiontime, +-- and set acctterminatecause to "NAS reboot" for interrupted sessions. It +-- does so in batches, which avoids long-lived locks on the affected rows. +-- +-- It can be invoked as follows: +-- +-- CALL fr_radacct_close_after_reload(); +-- +-- Note: This SP requires PostgreSQL >= 11 which was the first version to +-- introduce PROCEDUREs which permit transaction control. This allows COMMIT +-- to be called to incrementally apply successive batch updates prior to the +-- end of the procedure. Prior to version 11 there exists only FUNCTIONs that +-- execute atomically. You can convert this procedure to a function, but by +-- doing so you are really no better off than performing a single, +-- long-running bulk update. +-- +-- Note: This SP walks radacct in strides of v_batch_size. It will typically +-- skip closed and ongoing sessions at a rate significantly faster than +-- 500,000 rows per second and process batched updates faster than 25,000 +-- orphaned sessions per second. If this isn't fast enough then you should +-- really consider using a custom schema that includes partitioning by +-- nasipaddress or acct{start,stop}time. +-- +CREATE OR REPLACE PROCEDURE fr_radacct_close_after_reload () +LANGUAGE plpgsql +AS $$ + +DECLARE v_a bigint; +DECLARE v_z bigint; +DECLARE v_updated bigint DEFAULT 0; +DECLARE v_last_report bigint DEFAULT 0; +DECLARE v_now bigint; +DECLARE v_last boolean DEFAULT false; +DECLARE v_rowcount integer; + +-- +-- This works for many circumstances +-- +DECLARE v_batch_size CONSTANT integer := 2500; + +BEGIN + + SELECT MIN(RadAcctId) INTO v_a FROM radacct WHERE AcctStopTime IS NULL; + + LOOP + + v_z := NULL; + SELECT RadAcctId INTO v_z FROM radacct WHERE RadAcctId > v_a ORDER BY RadAcctId OFFSET v_batch_size LIMIT 1; + + IF v_z IS NULL THEN + SELECT MAX(RadAcctId) INTO v_z FROM radacct; + v_last := true; + END IF; + + UPDATE radacct a + SET + AcctStopTime = n.reloadtime, + AcctSessionTime = EXTRACT(EPOCH FROM (n.ReloadTime - a.AcctStartTime)), + AcctTerminateCause = 'NAS reboot' + FROM nasreload n + WHERE + a.NASIPAddress = n.NASIPAddress + AND RadAcctId BETWEEN v_a AND v_z + AND AcctStopTime IS NULL + AND AcctStartTime < n.ReloadTime; + + GET DIAGNOSTICS v_rowcount := ROW_COUNT; + v_updated := v_updated + v_rowcount; + + COMMIT; -- Make the update visible + + v_a := v_z + 1; + + -- + -- Periodically report how far we've got + -- + SELECT EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) INTO v_now; + IF v_last_report != v_now OR v_last THEN + RAISE NOTICE 'RadAcctID: %; Sessions closed: %', v_z, v_updated; + v_last_report := v_now; + END IF; + + EXIT WHEN v_last; + + END LOOP; + +END +$$; + diff --git a/raddb/mods-config/sql/main/postgresql/queries.conf b/raddb/mods-config/sql/main/postgresql/queries.conf new file mode 100644 index 0000000..d410562 --- /dev/null +++ b/raddb/mods-config/sql/main/postgresql/queries.conf @@ -0,0 +1,742 @@ +# -*- text -*- +# +# main/postgresql/queries.conf -- PostgreSQL configuration for default schema (schema.sql) +# +# $Id$ + +# Use the driver specific SQL escape method. +# +# If you enable this configuration item, the "safe_characters" +# configuration is ignored. FreeRADIUS then uses the PostgreSQL escape +# functions to escape input strings. The only downside to making this +# change is that the PostgreSQL escaping method is not the same the one +# used by FreeRADIUS. So characters which are NOT in the +# "safe_characters" list will now be stored differently in the database. +# +#auto_escape = yes + +# Safe characters list for sql queries. Everything else is replaced +# with their mime-encoded equivalents. +# The default list should be ok +# Using 'auto_escape' is preferred +# safe_characters = "@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_: /" + +####################################################################### +# Query config: Username +####################################################################### +# This is the username that will get substituted, escaped, and added +# as attribute 'SQL-User-Name'. '%{SQL-User-Name}' should be used +# below everywhere a username substitution is needed so you you can +# be sure the username passed from the client is escaped properly. +# +# Uncomment the next line, if you want the sql_user_name to mean: +# +# Use Stripped-User-Name, if it's there. +# Else use User-Name, if it's there, +# Else use hard-coded string "none" as the user name. +# +#sql_user_name = "%{%{Stripped-User-Name}:-%{%{User-Name}:-none}}" + +sql_user_name = "%{User-Name}" + +####################################################################### +# Query config: Event-Timestamp +####################################################################### +# event_timestamp_epoch is the basis for the time inserted into +# accounting records. Typically this will be the Event-Timestamp of the +# accounting request, which is usually provided by a NAS. +# +# Uncomment the next line, if you want the timestamp to be based on the +# request reception time recorded by this server, for example if you +# distrust the provided Event-Timestamp. +#event_timestamp_epoch = "%l" + +event_timestamp_epoch = "%{%{integer:Event-Timestamp}:-%l}" + +# event_timestamp is the SQL snippet for converting an epoch timestamp +# to an SQL date. + +event_timestamp = "TO_TIMESTAMP(${event_timestamp_epoch})" + +####################################################################### +# Query config: Class attribute +####################################################################### +# +# 3.0.22 and later have a "class" column in the accounting table. +# +# However, we do NOT want to break existing configurations by adding +# the Class attribute to the default queries. If we did that, then +# systems using newer versions of the server would fail, because +# there is no "class" column in their accounting tables. +# +# The solution to that is the following "class" subsection. If your +# database has a "class" column for the various tables, then you can +# uncomment the configuration items here. The queries below will +# then automatically insert the Class attribute into radacct, +# radpostauth, etc. +# +class { + # + # Delete the '#' character from each of the configuration + # items in this section. This change puts the Class + # attribute into the various tables. Leave the double-quoted + # string there, as the value for the configuration item. + # + # See also policy.d/accounting, and the "insert_acct_class" + # policy. You will need to list (or uncomment) + # "insert_acct_class" in the "post-auth" section in order to + # create a Class attribute. + # + column_name = # ", Class" + packet_xlat = # ", '%{Class}'" + reply_xlat = # ", '%{reply:Class}'" +} + +####################################################################### +# Default profile +####################################################################### +# This is the default profile. It is found in SQL by group membership. +# That means that this profile must be a member of at least one group +# which will contain the corresponding check and reply items. +# This profile will be queried in the authorize section for every user. +# The point is to assign all users a default profile without having to +# manually add each one to a group that will contain the profile. +# The SQL module will also honor the User-Profile attribute. This +# attribute can be set anywhere in the authorize section (ie the users +# file). It is found exactly as the default profile is found. +# If it is set then it will *overwrite* the default profile setting. +# The idea is to select profiles based on checks on the incoming +# packets, not on user group membership. For example: +# -- users file -- +# DEFAULT Service-Type == Outbound-User, User-Profile := "outbound" +# DEFAULT Service-Type == Framed-User, User-Profile := "framed" +# +# By default the default_user_profile is not set +# +# default_user_profile = "DEFAULT" + +####################################################################### +# Open Query +####################################################################### +# This query is run whenever a new connection is opened. +# It is commented out by default. +# +# If you have issues with connections hanging for too long, uncomment +# the next line, and set the timeout in milliseconds. As a general +# rule, if the queries take longer than a second, something is wrong +# with the database. +#open_query = "set statement_timeout to 1000" + +####################################################################### +# NAS Query +####################################################################### +# This query retrieves the radius clients +# +# 0. Row ID (currently unused) +# 1. Name (or IP address) +# 2. Shortname +# 3. Type +# 4. Secret +# 5. Server +####################################################################### + +client_query = "\ + SELECT id, nasname, shortname, type, secret, server \ + FROM ${client_table}" + +####################################################################### +# Authorization Queries +####################################################################### +# These queries compare the check items for the user +# in ${authcheck_table} and setup the reply items in +# ${authreply_table}. You can use any query/tables +# you want, but the return data for each row MUST +# be in the following order: +# +# 0. Row ID (currently unused) +# 1. UserName/GroupName +# 2. Item Attr Name +# 3. Item Attr Value +# 4. Item Attr Operation +####################################################################### + +# +# Use these for case insensitive usernames. WARNING: Slower queries! +# +#authorize_check_query = "\ +# SELECT id, UserName, Attribute, Value, Op \ +# FROM ${authcheck_table} \ +# WHERE LOWER(UserName) = LOWER('%{SQL-User-Name}') \ +# ORDER BY id" + +#authorize_reply_query = "\ +# SELECT id, UserName, Attribute, Value, Op \ +# FROM ${authreply_table} \ +# WHERE LOWER(UserName) = LOWER('%{SQL-User-Name}') \ +# ORDER BY id" + +authorize_check_query = "\ + SELECT id, UserName, Attribute, Value, Op \ + FROM ${authcheck_table} \ + WHERE Username = '%{SQL-User-Name}' \ + ORDER BY id" + +authorize_reply_query = "\ + SELECT id, UserName, Attribute, Value, Op \ + FROM ${authreply_table} \ + WHERE Username = '%{SQL-User-Name}' \ + ORDER BY id" + +# +# Use these for case insensitive usernames. WARNING: Slower queries! +# +#authorize_group_check_query = "\ +# SELECT \ +# ${groupcheck_table}.id, ${groupcheck_table}.GroupName, ${groupcheck_table}.Attribute, \ +# ${groupcheck_table}.Value, ${groupcheck_table}.Op \ +# FROM ${groupcheck_table}, ${usergroup_table} \ +# WHERE LOWER(${usergroup_table}.UserName) = LOWER('%{SQL-User-Name}') \ +# AND ${usergroup_table}.GroupName = ${groupcheck_table}.GroupName \ +# ORDER BY ${groupcheck_table}.id" + +#authorize_group_reply_query = "\ +# SELECT \ +# ${groupreply_table}.id, ${groupreply_table}.GroupName, \ +# ${groupreply_table}.Attribute, ${groupreply_table}.Value, ${groupreply_table}.Op \ +# FROM ${groupreply_table}, ${usergroup_table} \ +# WHERE LOWER(${usergroup_table}.UserName) = LOWER('%{SQL-User-Name}') \ +# AND ${usergroup_table}.GroupName = ${groupreply_table}.GroupName \ +# ORDER BY ${groupreply_table}.id" + +authorize_group_check_query = "\ + SELECT id, GroupName, Attribute, Value, op \ + FROM ${groupcheck_table} \ + WHERE GroupName = '%{${group_attribute}}' \ + ORDER BY id" + +authorize_group_reply_query = "\ + SELECT id, GroupName, Attribute, Value, op \ + FROM ${groupreply_table} \ + WHERE GroupName = '%{${group_attribute}}' \ + ORDER BY id" + +####################################################################### +# Simultaneous Use Checking Queries +####################################################################### +# simul_count_query - query for the number of current connections +# - If this is not defined, no simultaneous use checking +# - will be performed by this module instance +# simul_verify_query - query to return details of current connections for verification +# - Leave blank or commented out to disable verification step +# - Note that the returned field order should not be changed. +####################################################################### + +simul_count_query = "\ + SELECT COUNT(RadAcctId) \ + FROM ${acct_table1} a \ + LEFT OUTER JOIN nasreload n USING (NASIPAddress) \ + WHERE UserName='%{SQL-User-Name}' \ + AND AcctStopTime IS NULL \ + AND (a.AcctStartTime > n.ReloadTime OR n.ReloadTime IS NULL)" + +simul_verify_query = "\ + SELECT RadAcctId, AcctSessionId, UserName, NASIPAddress, NASPortId, FramedIPAddress, CallingStationId, \ + FramedProtocol \ + FROM ${acct_table1} a \ + LEFT OUTER JOIN nasreload n USING (nasipaddress) \ + WHERE UserName='%{SQL-User-Name}' \ + AND AcctStopTime IS NULL \ + AND (a.AcctStartTime > n.reloadtime OR n.reloadtime IS NULL)" + +####################################################################### +# Group Membership Queries +####################################################################### +# group_membership_query - Check user group membership +####################################################################### + +# Use these for case insensitive usernames. WARNING: Slower queries! +#group_membership_query = "\ +# SELECT GroupName \ +# FROM ${usergroup_table} \ +# WHERE LOWER(UserName) = LOWER('%{SQL-User-Name}') \ +# ORDER BY priority" + +group_membership_query = "\ + SELECT GroupName \ + FROM ${usergroup_table} \ + WHERE UserName='%{SQL-User-Name}' \ + ORDER BY priority" + +####################################################################### +# Accounting and Post-Auth Queries +####################################################################### +# These queries insert/update accounting and authentication records. +# The query to use is determined by the value of 'reference'. +# This value is used as a configuration path and should resolve to one +# or more 'query's. If reference points to multiple queries, and a query +# fails, the next query is executed. +# +# Behaviour is identical to the old 1.x/2.x module, except we can now +# fail between N queries, and query selection can be based on any +# combination of attributes, or custom 'Acct-Status-Type' values. +####################################################################### + +accounting { + reference = "%{tolower:type.%{%{Acct-Status-Type}:-%{Request-Processing-Stage}}.query}" + + # Write SQL queries to a logfile. This is potentially useful for bulk inserts + # when used with the rlm_sql_null driver. +# logfile = ${logdir}/accounting.sql + + column_list = "\ + AcctSessionId, \ + AcctUniqueId, \ + UserName, \ + Realm, \ + NASIPAddress, \ + NASPortId, \ + NASPortType, \ + AcctStartTime, \ + AcctUpdateTime, \ + AcctStopTime, \ + AcctSessionTime, \ + AcctAuthentic, \ + ConnectInfo_start, \ + ConnectInfo_Stop, \ + AcctInputOctets, \ + AcctOutputOctets, \ + CalledStationId, \ + CallingStationId, \ + AcctTerminateCause, \ + ServiceType, \ + FramedProtocol, \ + FramedIpAddress, \ + FramedIpv6Address, \ + FramedIpv6Prefix, \ + FramedInterfaceId, \ + DelegatedIpv6Prefix \ + ${..class.column_name}" + + type { + + accounting-on { + + # + # "Bulk update" Accounting-On/Off strategy. + # + # Immediately terminate all sessions associated with a + # given NAS. + # + # Note: If a large number of sessions require closing + # then the bulk update may be take a long time to run + # and lock an excessive number of rows. See the + # strategy below for an alternative approach that does + # not touch the radacct session data. + # + query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctStopTime = ${....event_timestamp}, \ + AcctUpdateTime = ${....event_timestamp}, \ + AcctSessionTime = (${....event_timestamp_epoch} - EXTRACT(EPOCH FROM(AcctStartTime))), \ + AcctTerminateCause = '%{%{Acct-Terminate-Cause}:-NAS-Reboot}' \ + WHERE AcctStopTime IS NULL \ + AND NASIPAddress= '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}' \ + AND AcctStartTime <= ${....event_timestamp}" + + # + # "Lightweight" Accounting-On/Off strategy. + # + # Record the reload time of the NAS and let the + # administrator actually close the sessions in radacct + # out-of-band, if desired. + # + # Implementation advice, together with a stored + # procedure for closing sessions and a view showing + # the effective stop time of each session is provided + # in process-radacct.sql. + # + # To enable this strategy, just change the previous + # query to "-query", and this one to "query". The + # previous one will be ignored, and this one will be + # enabled. + # + -query = "\ + INSERT INTO nasreload (NASIPAddress, ReloadTime) \ + VALUES ('%{NAS-IP-Address}', ${....event_timestamp}) \ + ON CONFLICT ON (NASIPAddress) \ + DO UPDATE SET \ + ReloadTime = ${....event_timestamp}" + + } + + accounting-off { + query = "${..accounting-on.query}" + } + + # + # Implement the "sql_session_start" policy. + # See raddb/policy.d/accounting for more details. + # + # You also need to fix the other queries as + # documented below. Look for "sql_session_start". + # + post-auth { + query = "\ + INSERT INTO ${....acct_table1} \ + (${...column_list}) \ + VALUES(\ + '%{Acct-Session-Id}', \ + '%{Acct-Unique-Session-Id}', \ + '%{SQL-User-Name}', \ + NULLIF('%{Realm}', ''), \ + '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}', \ + NULLIF('%{%{NAS-Port-ID}:-%{NAS-Port}}', ''), \ + '%{NAS-Port-Type}', \ + ${....event_timestamp}, \ + NULL, \ + NULL, \ + 0, \ + '', \ + '%{Connect-Info}', \ + NULL, \ + 0, \ + 0, \ + '%{Called-Station-Id}', \ + '%{Calling-Station-Id}', \ + NULL, \ + '%{Service-Type}', \ + '', \ + NULL, \ + NULL, \ + NULL, \ + NULL, \ + NULL \ + ${....class.reply_xlat})" + + query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctStartTime = ${....event_timestamp}, \ + AcctUpdateTime = ${....event_timestamp}, \ + ConnectInfo_start = '%{Connect-Info}', \ + AcctSessionId = '%{Acct-Session-Id}' \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-Id}' \ + AND AcctStopTime IS NULL" + } + + start { + query = "\ + INSERT INTO ${....acct_table1} \ + (${...column_list}) \ + VALUES(\ + '%{Acct-Session-Id}', \ + '%{Acct-Unique-Session-Id}', \ + '%{SQL-User-Name}', \ + NULLIF('%{Realm}', ''), \ + '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}', \ + NULLIF('%{%{NAS-Port-ID}:-%{NAS-Port}}', ''), \ + '%{NAS-Port-Type}', \ + ${....event_timestamp}, \ + ${....event_timestamp}, \ + NULL, \ + 0, \ + '%{Acct-Authentic}', \ + '%{Connect-Info}', \ + NULL, \ + 0, \ + 0, \ + '%{Called-Station-Id}', \ + '%{Calling-Station-Id}', \ + NULL, \ + '%{Service-Type}', \ + '%{Framed-Protocol}', \ + NULLIF('%{Framed-IP-Address}', '')::inet, \ + NULLIF('%{Framed-IPv6-Address}', '')::inet, \ + NULLIF('%{Framed-IPv6-Prefix}', '')::inet, \ + NULLIF('%{Framed-Interface-Id}', ''), \ + NULLIF('%{Delegated-IPv6-Prefix}', '')::inet \ + ${....class.packet_xlat} ) \ + ON CONFLICT (AcctUniqueId) \ + DO UPDATE \ + SET \ + AcctStartTime = ${....event_timestamp}, \ + AcctUpdateTime = ${....event_timestamp}, \ + ConnectInfo_start = '%{Connect-Info}' \ + WHERE ${....acct_table1}.AcctUniqueId = '%{Acct-Unique-Session-Id}' \ + AND ${....acct_table1}.AcctStopTime IS NULL" + + # + # When using "sql_session_start", you should comment out + # the previous query, and enable this one. + # + # Just change the previous query to "-query", + # and this one to "query". The previous one + # will be ignored, and this one will be + # enabled. + # + -query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctSessionId = '%{Acct-Session-Id}', \ + AcctUniqueId = '%{Acct-Unique-Session-Id}', \ + AcctAuthentic = '%{Acct-Authentic}', \ + ConnectInfo_start = '%{Connect-Info}', \ + ServiceType = '%{Service-Type}', \ + FramedProtocol = '%{Framed-Protocol}', \ + FramedIPAddress = NULLIF('%{Framed-IP-Address}', '')::inet, \ + FramedIPv6Address = NULLIF('%{Framed-IPv6-Address}', '')::inet, \ + FramedIPv6Prefix = NULLIF('%{Framed-IPv6-Prefix}', '')::inet, \ + FramedInterfaceId = NULLIF('%{Framed-Interface-Id}', ''), \ + DelegatedIPv6Prefix = NULLIF('%{Delegated-IPv6-Prefix}', '')::inet, \ + AcctStartTime = ${....event_timestamp}, \ + AcctUpdateTime = ${....event_timestamp} \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-Id}' \ + AND AcctStopTime IS NULL" + + # and again where we don't have "AND AcctStopTime IS NULL" + query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctStartTime = ${....event_timestamp}, \ + AcctUpdateTime = ${....event_timestamp}, \ + ConnectInfo_start = '%{Connect-Info}' \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-Id}'" + } + + interim-update { + query = "\ + UPDATE ${....acct_table1} \ + SET \ + FramedIPAddress = NULLIF('%{Framed-IP-Address}', '')::inet, \ + FramedIPv6Address = NULLIF('%{Framed-IPv6-Address}', '')::inet, \ + FramedIPv6Prefix = NULLIF('%{Framed-IPv6-Prefix}', '')::inet, \ + FramedInterfaceId = NULLIF('%{Framed-Interface-Id}', ''), \ + DelegatedIPv6Prefix = NULLIF('%{Delegated-IPv6-Prefix}', '')::inet, \ + AcctSessionTime = %{%{Acct-Session-Time}:-NULL}, \ + AcctInterval = (${....event_timestamp_epoch} - EXTRACT(EPOCH FROM (COALESCE(AcctUpdateTime, AcctStartTime)))), \ + AcctUpdateTime = ${....event_timestamp}, \ + AcctInputOctets = (('%{%{Acct-Input-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Input-Octets}:-0}'::bigint), \ + AcctOutputOctets = (('%{%{Acct-Output-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Output-Octets}:-0}'::bigint) \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-Id}' \ + AND AcctStopTime IS NULL" + + query = "\ + INSERT INTO ${....acct_table1} \ + (${...column_list}) \ + VALUES(\ + '%{Acct-Session-Id}', \ + '%{Acct-Unique-Session-Id}', \ + '%{SQL-User-Name}', \ + NULLIF('%{Realm}', ''), \ + '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}', \ + NULLIF('%{%{NAS-Port-ID}:-%{NAS-Port}}', ''), \ + '%{NAS-Port-Type}', \ + ${....event_timestamp}, \ + ${....event_timestamp}, \ + NULL, \ + %{%{Acct-Session-Time}:-NULL}, \ + '%{Acct-Authentic}', \ + '%{Connect-Info}', \ + NULL, \ + (('%{%{Acct-Input-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Input-Octets}:-0}'::bigint), \ + (('%{%{Acct-Output-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Output-Octets}:-0}'::bigint), \ + '%{Called-Station-Id}', \ + '%{Calling-Station-Id}', \ + NULL, \ + '%{Service-Type}', \ + '%{Framed-Protocol}', \ + NULLIF('%{Framed-IP-Address}', '')::inet, \ + NULLIF('%{Framed-IPv6-Address}', '')::inet, \ + NULLIF('%{Framed-IPv6-Prefix}', '')::inet, \ + NULLIF('%{Framed-Interface-Id}', ''), \ + NULLIF('%{Delegated-IPv6-Prefix}', '')::inet \ + ${....class.packet_xlat}) \ + ON CONFLICT (AcctUniqueId) \ + DO NOTHING" + + # + # When using "sql_session_start", you should comment out + # the previous query, and enable this one. + # + # Just change the previous query to "-query", + # and this one to "query". The previous one + # will be ignored, and this one will be + # enabled. + # + -query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctSessionId = '%{Acct-Session-Id}', \ + AcctUniqueId = '%{Acct-Unique-Session-Id}', \ + AcctAuthentic = '%{Acct-Authentic}', \ + ConnectInfo_start = '%{Connect-Info}', \ + ServiceType = '%{Service-Type}', \ + FramedProtocol = '%{Framed-Protocol}', \ + FramedIPAddress = NULLIF('%{Framed-IP-Address}', '')::inet, \ + FramedIPv6Address = NULLIF('%{Framed-IPv6-Address}', '')::inet, \ + FramedIPv6Prefix = NULLIF('%{Framed-IPv6-Prefix}', '')::inet, \ + FramedInterfaceId = NULLIF('%{Framed-Interface-Id}', ''), \ + DelegatedIPv6Prefix = NULLIF('%{Delegated-IPv6-Prefix}', '')::inet, \ + AcctUpdateTime = ${....event_timestamp}, \ + AcctSessionTime = COALESCE(%{%{Acct-Session-Time}:-NULL}, \ + (${....event_timestamp_epoch} - EXTRACT(EPOCH FROM(AcctStartTime)))), \ + AcctInputOctets = (('%{%{Acct-Input-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Input-Octets}:-0}'::bigint), \ + AcctOutputOctets = (('%{%{Acct-Output-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Output-Octets}:-0}'::bigint) \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-Id}' \ + AND AcctStopTime IS NULL" + } + + stop { + query = "\ + UPDATE ${....acct_table2} \ + SET \ + AcctStopTime = ${....event_timestamp}, \ + AcctUpdateTime = ${....event_timestamp}, \ + AcctSessionTime = COALESCE(%{%{Acct-Session-Time}:-NULL}, \ + (${....event_timestamp_epoch} - EXTRACT(EPOCH FROM(AcctStartTime)))), \ + AcctInputOctets = (('%{%{Acct-Input-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Input-Octets}:-0}'::bigint), \ + AcctOutputOctets = (('%{%{Acct-Output-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Output-Octets}:-0}'::bigint), \ + AcctTerminateCause = '%{Acct-Terminate-Cause}', \ + FramedIPAddress = NULLIF('%{Framed-IP-Address}', '')::inet, \ + FramedIPv6Address = NULLIF('%{Framed-IPv6-Address}', '')::inet, \ + FramedIPv6Prefix = NULLIF('%{Framed-IPv6-Prefix}', '')::inet, \ + FramedInterfaceId = NULLIF('%{Framed-Interface-Id}', ''), \ + DelegatedIPv6Prefix = NULLIF('%{Delegated-IPv6-Prefix}', '')::inet, \ + ConnectInfo_stop = '%{Connect-Info}' \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-Id}' \ + AND AcctStopTime IS NULL" + + query = "\ + INSERT INTO ${....acct_table1} \ + (${...column_list}) \ + VALUES(\ + '%{Acct-Session-Id}', \ + '%{Acct-Unique-Session-Id}', \ + '%{SQL-User-Name}', \ + NULLIF('%{Realm}', ''), \ + '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}', \ + NULLIF('%{%{NAS-Port-ID}:-%{NAS-Port}}', ''), \ + '%{NAS-Port-Type}', \ + TO_TIMESTAMP(${....event_timestamp_epoch} - %{%{Acct-Session-Time}:-0}), \ + ${....event_timestamp}, \ + ${....event_timestamp}, \ + NULLIF('%{Acct-Session-Time}', '')::bigint, \ + '%{Acct-Authentic}', \ + '%{Connect-Info}', \ + NULL, \ + (('%{%{Acct-Input-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Input-Octets}:-0}'::bigint), \ + (('%{%{Acct-Output-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Output-Octets}:-0}'::bigint), \ + '%{Called-Station-Id}', \ + '%{Calling-Station-Id}', \ + '%{Acct-Terminate-Cause}', \ + '%{Service-Type}', \ + '%{Framed-Protocol}', \ + NULLIF('%{Framed-IP-Address}', '')::inet, \ + NULLIF('%{Framed-IPv6-Address}', '')::inet, \ + NULLIF('%{Framed-IPv6-Prefix}', '')::inet, \ + NULLIF('%{Framed-Interface-Id}', ''), \ + NULLIF('%{Delegated-IPv6-Prefix}', '')::inet \ + ${....class.packet_xlat}) \ + ON CONFLICT (AcctUniqueId) \ + DO NOTHING" + + # + # When using "sql_session_start", you should comment out + # the previous query, and enable this one. + # + # Just change the previous query to "-query", + # and this one to "query". The previous one + # will be ignored, and this one will be + # enabled. + # + -query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctSessionId = '%{Acct-Session-Id}', \ + AcctUniqueId = '%{Acct-Unique-Session-Id}', \ + AcctAuthentic = '%{Acct-Authentic}', \ + ConnectInfo_start = '%{Connect-Info}', \ + ServiceType = '%{Service-Type}', \ + FramedProtocol = '%{Framed-Protocol}', \ + FramedIPAddress = NULLIF('%{Framed-IP-Address}', '')::inet, \ + FramedIPv6Address = NULLIF('%{Framed-IPv6-Address}', '')::inet, \ + FramedIPv6Prefix = NULLIF('%{Framed-IPv6-Prefix}', '')::inet, \ + FramedInterfaceId = NULLIF('%{Framed-Interface-Id}', ''), \ + DelegatedIPv6Prefix = NULLIF('%{Delegated-IPv6-Prefix}', '')::inet, \ + AcctStopTime = ${....event_timestamp}, \ + AcctUpdateTime = ${....event_timestamp}, \ + AcctSessionTime = COALESCE(%{%{Acct-Session-Time}:-NULL}, \ + (${....event_timestamp_epoch} - EXTRACT(EPOCH FROM(AcctStartTime)))), \ + AcctInputOctets = (('%{%{Acct-Input-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Input-Octets}:-0}'::bigint), \ + AcctOutputOctets = (('%{%{Acct-Output-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Output-Octets}:-0}'::bigint), \ + AcctTerminateCause = '%{Acct-Terminate-Cause}', \ + ConnectInfo_stop = '%{Connect-Info}' \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-Id}' \ + AND AcctStopTime IS NULL" + + # and again where we don't have "AND AcctStopTime IS NULL" + query = "\ + UPDATE ${....acct_table2} \ + SET \ + AcctStopTime = ${....event_timestamp}, \ + AcctUpdateTime = ${....event_timestamp}, \ + AcctSessionTime = COALESCE(%{%{Acct-Session-Time}:-NULL}, \ + (${....event_timestamp_epoch} - EXTRACT(EPOCH FROM(AcctStartTime)))), \ + AcctInputOctets = (('%{%{Acct-Input-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Input-Octets}:-0}'::bigint), \ + AcctOutputOctets = (('%{%{Acct-Output-Gigawords}:-0}'::bigint << 32) + \ + '%{%{Acct-Output-Octets}:-0}'::bigint), \ + AcctTerminateCause = '%{Acct-Terminate-Cause}', \ + FramedIPAddress = NULLIF('%{Framed-IP-Address}', '')::inet, \ + FramedIPv6Address = NULLIF('%{Framed-IPv6-Address}', '')::inet, \ + FramedIPv6Prefix = NULLIF('%{Framed-IPv6-Prefix}', '')::inet, \ + FramedInterfaceId = NULLIF('%{Framed-Interface-Id}', ''), \ + DelegatedIPv6Prefix = NULLIF('%{Delegated-IPv6-Prefix}', '')::inet, \ + ConnectInfo_stop = '%{Connect-Info}' \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-Id}'" + } + + # + # No Acct-Status-Type == ignore the packet + # + accounting { + query = "SELECT true" + } + } +} + + +####################################################################### +# Authentication Logging Queries +####################################################################### +# postauth_query - Insert some info after authentication +####################################################################### + +post-auth { + # Write SQL queries to a logfile. This is potentially useful for bulk inserts + # when used with the rlm_sql_null driver. +# logfile = ${logdir}/post-auth.sql + + query = "\ + INSERT INTO ${..postauth_table} \ + (username, pass, reply, authdate ${..class.column_name}) \ + VALUES(\ + '%{User-Name}', \ + '%{%{User-Password}:-%{Chap-Password}}', \ + '%{reply:Packet-Type}', \ + '%S.%M' \ + ${..class.reply_xlat})" +} diff --git a/raddb/mods-config/sql/main/postgresql/schema.sql b/raddb/mods-config/sql/main/postgresql/schema.sql new file mode 100644 index 0000000..518bc5d --- /dev/null +++ b/raddb/mods-config/sql/main/postgresql/schema.sql @@ -0,0 +1,178 @@ +/* + * $Id$ + * + * PostgreSQL schema for FreeRADIUS + * + */ + +/* + * Table structure for table 'radacct' + * + */ +CREATE TABLE IF NOT EXISTS radacct ( + RadAcctId bigserial PRIMARY KEY, + AcctSessionId text NOT NULL, + AcctUniqueId text NOT NULL UNIQUE, + UserName text, + Realm text, + NASIPAddress inet NOT NULL, + NASPortId text, + NASPortType text, + AcctStartTime timestamp with time zone, + AcctUpdateTime timestamp with time zone, + AcctStopTime timestamp with time zone, + AcctInterval bigint, + AcctSessionTime bigint, + AcctAuthentic text, + ConnectInfo_start text, + ConnectInfo_stop text, + AcctInputOctets bigint, + AcctOutputOctets bigint, + CalledStationId text, + CallingStationId text, + AcctTerminateCause text, + ServiceType text, + FramedProtocol text, + FramedIPAddress inet, + FramedIPv6Address inet, + FramedIPv6Prefix inet, + FramedInterfaceId text, + DelegatedIPv6Prefix inet, + Class text +); +-- This index may be useful.. +-- CREATE UNIQUE INDEX radacct_whoson on radacct (AcctStartTime, nasipaddress); + +-- For use by update-, stop- and simul_* queries +CREATE INDEX radacct_active_session_idx ON radacct (AcctUniqueId) WHERE AcctStopTime IS NULL; + +-- Add if you you regularly have to replay packets +-- CREATE INDEX radacct_session_idx ON radacct (AcctUniqueId); + +-- For backwards compatibility +-- CREATE INDEX radacct_active_user_idx ON radacct (AcctSessionId, UserName, NASIPAddress) WHERE AcctStopTime IS NULL; + +-- For use by onoff- +CREATE INDEX radacct_bulk_close ON radacct (NASIPAddress, AcctStartTime) WHERE AcctStopTime IS NULL; + +-- and for common statistic queries: +CREATE INDEX radacct_start_user_idx ON radacct (AcctStartTime, UserName); + +-- and, optionally +-- CREATE INDEX radacct_stop_user_idx ON radacct (acctStopTime, UserName); + +-- and for Class +CREATE INDEX radacct_calss_idx ON radacct (Class); + + +/* + * Table structure for table 'radcheck' + */ +CREATE TABLE IF NOT EXISTS radcheck ( + id serial PRIMARY KEY, + UserName text NOT NULL DEFAULT '', + Attribute text NOT NULL DEFAULT '', + op VARCHAR(2) NOT NULL DEFAULT '==', + Value text NOT NULL DEFAULT '' +); +create index radcheck_UserName on radcheck (UserName,Attribute); +/* + * Use this index if you use case insensitive queries + */ +-- create index radcheck_UserName_lower on radcheck (lower(UserName),Attribute); + +/* + * Table structure for table 'radgroupcheck' + */ +CREATE TABLE IF NOT EXISTS radgroupcheck ( + id serial PRIMARY KEY, + GroupName text NOT NULL DEFAULT '', + Attribute text NOT NULL DEFAULT '', + op VARCHAR(2) NOT NULL DEFAULT '==', + Value text NOT NULL DEFAULT '' +); +create index radgroupcheck_GroupName on radgroupcheck (GroupName,Attribute); + +/* + * Table structure for table 'radgroupreply' + */ +CREATE TABLE IF NOT EXISTS radgroupreply ( + id serial PRIMARY KEY, + GroupName text NOT NULL DEFAULT '', + Attribute text NOT NULL DEFAULT '', + op VARCHAR(2) NOT NULL DEFAULT '=', + Value text NOT NULL DEFAULT '' +); +create index radgroupreply_GroupName on radgroupreply (GroupName,Attribute); + +/* + * Table structure for table 'radreply' + */ +CREATE TABLE IF NOT EXISTS radreply ( + id serial PRIMARY KEY, + UserName text NOT NULL DEFAULT '', + Attribute text NOT NULL DEFAULT '', + op VARCHAR(2) NOT NULL DEFAULT '=', + Value text NOT NULL DEFAULT '' +); +create index radreply_UserName on radreply (UserName,Attribute); +/* + * Use this index if you use case insensitive queries + */ +-- create index radreply_UserName_lower on radreply (lower(UserName),Attribute); + +/* + * Table structure for table 'radusergroup' + */ +CREATE TABLE IF NOT EXISTS radusergroup ( + id serial PRIMARY KEY, + UserName text NOT NULL DEFAULT '', + GroupName text NOT NULL DEFAULT '', + priority integer NOT NULL DEFAULT 0 +); +create index radusergroup_UserName on radusergroup (UserName); +/* + * Use this index if you use case insensitive queries + */ +-- create index radusergroup_UserName_lower on radusergroup (lower(UserName)); + +-- +-- Table structure for table 'radpostauth' +-- + +CREATE TABLE IF NOT EXISTS radpostauth ( + id bigserial PRIMARY KEY, + username text NOT NULL, + pass text, + reply text, + CalledStationId text, + CallingStationId text, + authdate timestamp with time zone NOT NULL default now(), + Class text +); +CREATE INDEX radpostauth_username_idx ON radpostauth (username); +CREATE INDEX radpostauth_class_idx ON radpostauth (Class); + +/* + * Table structure for table 'nas' + */ +CREATE TABLE IF NOT EXISTS nas ( + id serial PRIMARY KEY, + nasname text NOT NULL, + shortname text NOT NULL, + type text NOT NULL DEFAULT 'other', + ports integer, + secret text NOT NULL, + server text, + community text, + description text +); +create index nas_nasname on nas (nasname); + +/* + * Table structure for table 'nasreload' + */ +CREATE TABLE IF NOT EXISTS nasreload ( + NASIPAddress inet PRIMARY KEY, + ReloadTime timestamp with time zone NOT NULL +); diff --git a/raddb/mods-config/sql/main/postgresql/setup.sql b/raddb/mods-config/sql/main/postgresql/setup.sql new file mode 100644 index 0000000..def5531 --- /dev/null +++ b/raddb/mods-config/sql/main/postgresql/setup.sql @@ -0,0 +1,58 @@ +/* + * setup.sql -- PostgreSQL commands for creating the RADIUS user. + * + * WARNING: You should change 'localhost' and 'radpass' + * to something else. Also update raddb/mods-available/sql + * with the new RADIUS password. + * + * $Id$ + */ + +/* + * Create default administrator for RADIUS + * + */ +CREATE USER radius WITH PASSWORD 'radpass'; + +/* + * The server can read the authorisation data + * + */ +GRANT SELECT ON radcheck TO radius; +GRANT SELECT ON radreply TO radius; +GRANT SELECT ON radusergroup TO radius; +GRANT SELECT ON radgroupcheck TO radius; +GRANT SELECT ON radgroupreply TO radius; + +/* + * The server can write accounting and post-auth data + * + */ +GRANT SELECT, INSERT, UPDATE on radacct TO radius; +GRANT SELECT, INSERT, UPDATE on radpostauth TO radius; + +/* + * The server can read the NAS data + * + */ +GRANT SELECT ON nas TO radius; + +/* + * In the case of the "lightweight accounting-on/off" strategy, the server also + * records NAS reload times + * + */ +GRANT SELECT, INSERT, UPDATE ON nasreload TO radius; + +/* + * Grant permissions on sequences + * + */ +GRANT USAGE, SELECT ON SEQUENCE radcheck_id_seq TO radius; +GRANT USAGE, SELECT ON SEQUENCE radreply_id_seq TO radius; +GRANT USAGE, SELECT ON SEQUENCE radusergroup_id_seq TO radius; +GRANT USAGE, SELECT ON SEQUENCE radgroupcheck_id_seq TO radius; +GRANT USAGE, SELECT ON SEQUENCE radgroupreply_id_seq TO radius; +GRANT USAGE, SELECT ON SEQUENCE radacct_radacctid_seq TO radius; +GRANT USAGE, SELECT ON SEQUENCE radpostauth_id_seq TO radius; +GRANT USAGE, SELECT ON SEQUENCE nas_id_seq TO radius; diff --git a/raddb/mods-config/sql/main/sqlite/process-radacct-close-after-reload.pl b/raddb/mods-config/sql/main/sqlite/process-radacct-close-after-reload.pl new file mode 100755 index 0000000..c43da06 --- /dev/null +++ b/raddb/mods-config/sql/main/sqlite/process-radacct-close-after-reload.pl @@ -0,0 +1,119 @@ +#!/usr/bin/perl -Tw + +# +# main/sqlite/process-radacct-close-after_reload.pl -- Script for +# processing radacct entries to close sessions interrupted by a NAS reload +# +# Requires the DBD::SQLite module: perl-DBD-SQLite (RedHat); libdbd-sqlite3-perl (Debian) +# +# $Id$ +# +# It may be desirable to periodically "close" radacct sessions belonging to a +# reloaded NAS, replicating the "bulk close" Accounting-On/Off behaviour, +# just not in real time. +# +# This script will set radacct.acctstoptime to nasreload.reloadtime, calculate +# the corresponding radacct.acctsessiontime, and set acctterminatecause to +# "NAS reboot" for interrupted sessions. It does so in batches, which avoids a +# single long-lived lock on the table. +# +# It can be invoked as follows: +# +# ./process-radacct-close-after-reload.pl <sqlite_db_file> +# +# Note: This script walks radacct in strides of v_batch_size. It will +# typically skip closed and ongoing sessions at a rate significantly faster +# than 10,000 rows per second and process batched updates faster than 5000 +# orphaned sessions per second. If this isn't fast enough then you should +# really consider using a server-based database for accounting purposes. +# + +use strict; +use DBI; + +# +# Fine for most purposes +# +my $batch_size = 2500; + +if ($#ARGV != 0) { + print "Usage: process-radacct-close-after_reload.pl SQLITE_DB_FILE\n\n"; + exit 1; +} +die "The SQLite database must exist: $ARGV[0]" unless -r $ARGV[0]; + + +my $dbh = DBI->connect("DBI:SQLite:dbname=$ARGV[0]", '', '', { RaiseError => 1 }) or die $DBI::errstr; + +# +# There is no UPDATE ... JOIN/FROM in SQLite, so we have to resort to this +# construction # which does not provide an accurate rows updated count... +# +my $sth_upd = $dbh->prepare(<<'EOF'); + UPDATE radacct + SET + acctstoptime = ( + SELECT COALESCE(acctstoptime, CASE WHEN radacct.acctstarttime < reloadtime THEN reloadtime END) + FROM nasreload WHERE nasipaddress = radacct.nasipaddress + ), + acctsessiontime = ( + SELECT COALESCE(acctsessiontime, + CASE WHEN radacct.acctstoptime IS NULL AND radacct.acctstarttime < reloadtime THEN + CAST((julianday(reloadtime) - julianday(radacct.acctstarttime)) * 86400 AS integer) + END) + FROM nasreload WHERE nasipaddress = radacct.nasipaddress + ), + acctterminatecause = ( + SELECT + CASE WHEN radacct.acctstoptime IS NULL AND radacct.acctstarttime < reloadtime THEN + 'NAS reboot' + ELSE + acctterminatecause + END + FROM nasreload WHERE nasipaddress = radacct.nasipaddress + ) + WHERE + radacctid BETWEEN ? AND ? + AND acctstoptime IS NULL +EOF + +my $sth = $dbh->prepare('SELECT MIN(radacctid), MAX(radacctid) FROM radacct WHERE acctstoptime IS NULL'); +$sth->execute() or die $DBI::errstr; +(my $a, my $m) = $sth->fetchrow_array(); +$sth->finish; + +my $sth_nxt = $dbh->prepare('SELECT radacctid FROM radacct WHERE radacctid > ? ORDER BY radacctid LIMIT ?,1'); + + +my $last = 0; +my $last_report = 0; + +unless ($last) { + + $sth_nxt->execute($a, $batch_size) or die $DBI::errstr; + (my $z) = $sth_nxt->fetchrow_array(); + + unless ($z) { + $z = $m; + $last = 1; + } + + my $rc = $sth_upd->execute($a, $z) or die $DBI::errstr; + + $a = $z + 1; + + # + # Periodically report how far we've got + # + my $now = time(); + if ($last_report != $now || $last) { + print "RadAcctID: $z\n"; + $last_report = $now; + } + +} + +$sth_upd->finish; +$sth_nxt->finish; + +$dbh->disconnect; diff --git a/raddb/mods-config/sql/main/sqlite/process-radacct-new-data-usage-period.sh b/raddb/mods-config/sql/main/sqlite/process-radacct-new-data-usage-period.sh new file mode 100755 index 0000000..edfb129 --- /dev/null +++ b/raddb/mods-config/sql/main/sqlite/process-radacct-new-data-usage-period.sh @@ -0,0 +1,113 @@ +#!/bin/sh +# +# main/sqlite/process-radacct-new-data-usage-period.sh -- Script for +# processing radacct entries to extract daily usage +# +# $Id$ + +# +# See process-radacct-schema.sql for details. +# + +if [ "$#" -ne 1 ]; then + echo "Usage: process-radacct-new-data-usage-period.sh SQLITE_DB_FILE" 2>&1 + exit 1 +fi + +if [ ! -r "$1" ]; then + echo "The SQLite database must exist: $1" 1>&2 + exit 1 +fi + +cat <<EOF | sqlite3 "$1" + + -- + -- SQLite doesn't have a concept of session variables so we fake it. + -- + DROP TABLE IF EXISTS vars; + CREATE TEMPORARY TABLE vars ( + key text, + value text, + PRIMARY KEY (key) + ); + + INSERT INTO vars SELECT 'v_start', COALESCE(DATETIME(MAX(period_end), '+1 seconds'), DATETIME(0, 'unixepoch')) FROM data_usage_by_period; + INSERT INTO vars SELECT 'v_end', CURRENT_TIMESTAMP; + + + -- + -- Make of copy of the sessions that were active during this period to + -- avoid having to execute a potentially long transaction that might hold a + -- global database lock. + -- + DROP TABLE IF EXISTS radacct_sessions; + CREATE TEMPORARY TABLE radacct_sessions ( + username text, + acctstarttime datetime, + acctstoptime datetime, + acctinputoctets bigint, + acctoutputoctets bigint + ); + CREATE INDEX temp.idx_radacct_sessions_username ON radacct_sessions(username); + CREATE INDEX temp.idx_radacct_sessions_acctstoptime ON radacct_sessions(acctstoptime); + + INSERT INTO radacct_sessions + SELECT + username, + acctstarttime, + acctstoptime, + acctinputoctets, + acctoutputoctets + FROM + radacct + WHERE + acctstoptime > (SELECT value FROM vars WHERE key='v_start') OR + acctstoptime IS NULL; + + + -- + -- Add the data usage for the sessions that were active in the current + -- period to the table. Include all sessions that finished since the start + -- of this period as well as those still ongoing. + -- + INSERT INTO data_usage_by_period (username, period_start, period_end, acctinputoctets, acctoutputoctets) + SELECT + username, + (SELECT value FROM vars WHERE key='v_start'), + (SELECT value FROM vars WHERE key='v_end'), + SUM(acctinputoctets) AS acctinputoctets, + SUM(acctoutputoctets) AS acctoutputoctets + FROM + radacct_sessions + GROUP BY + username + ON CONFLICT(username,period_start) DO UPDATE + SET + acctinputoctets = data_usage_by_period.acctinputoctets + EXCLUDED.acctinputoctets, + acctoutputoctets = data_usage_by_period.acctoutputoctets + EXCLUDED.acctoutputoctets, + period_end = (SELECT value FROM vars WHERE key='v_end'); + + -- + -- Create an open-ended "next period" for all ongoing sessions and carry a + -- negative value of their data usage to avoid double-accounting when we + -- process the next period. Their current data usage has already been + -- allocated to the current and possibly previous periods. + -- + INSERT INTO data_usage_by_period (username, period_start, period_end, acctinputoctets, acctoutputoctets) + SELECT + username, + (SELECT DATETIME(value, '+1 seconds') FROM vars WHERE key='v_end'), + NULL, + 0 - SUM(acctinputoctets), + 0 - SUM(acctoutputoctets) + FROM + radacct_sessions + WHERE + acctstoptime IS NULL + GROUP BY + username; + + DROP TABLE vars; + DROP TABLE radacct_sessions; + +EOF diff --git a/raddb/mods-config/sql/main/sqlite/process-radacct-schema.sql b/raddb/mods-config/sql/main/sqlite/process-radacct-schema.sql new file mode 100644 index 0000000..b429d4c --- /dev/null +++ b/raddb/mods-config/sql/main/sqlite/process-radacct-schema.sql @@ -0,0 +1,95 @@ +# -*- text -*- +# +# main/sqlite/process-radacct.sql -- Schema extensions for processing radacct entries +# +# $Id$ + +-- --------------------------------- +-- - Per-user data usage over time - +-- --------------------------------- +-- +-- An extension to the standard schema to hold per-user data usage statistics +-- for arbitrary periods. +-- +-- The data_usage_by_period table is populated by periodically calling the +-- process-radacct-new-data-usage-period.sh script. +-- +-- This table can be queried in various ways to produce reports of aggregate +-- data use over time. For example, if the refresh script is invoked once per +-- day just after midnight, to produce usage data with daily granularity, then +-- a reasonably accurate monthly bandwidth summary for a given user could be +-- obtained by queriing this table with: +-- +-- SELECT +-- STRFTIME('%Y-%m',CURRENT_TIMESTAMP) AS month, +-- SUM(acctinputoctets)*1.0/1000/1000/1000 AS gb_in, +-- SUM(acctoutputoctets)*1.0/1000/1000/1000 AS gb_out +-- FROM +-- data_usage_by_period +-- WHERE +-- username='bob' AND +-- period_end IS NOT NULL +-- GROUP BY +-- month; +-- +-- 2019-07|5.782279231|50.545664824 +-- 2019-08|4.230543344|48.523096424 +-- 2019-09|4.847360599|48.631835488 +-- 2019-10|6.456763254|51.686231937 +-- 2019-11|6.362537735|52.385710572 +-- 2019-12|4.301524442|50.762240277 +-- 2020-01|5.436280545|49.067775286 +-- +CREATE TABLE data_usage_by_period ( + username text, + period_start datetime, + period_end datetime, + acctinputoctets bigint, + acctoutputoctets bigint, + PRIMARY KEY (username, period_start) +); +CREATE INDEX idx_data_usage_by_period_period_start ON data_usage_by_period(period_start); +CREATE INDEX idx_data_usage_by_period_period_end ON data_usage_by_period(period_end); + + +-- ------------------------------------------------------ +-- - "Lightweight" Accounting-On/Off strategy resources - +-- ------------------------------------------------------ +-- +-- The following resources are for use only when the "lightweight" +-- Accounting-On/Off strategy is enabled in queries.conf. +-- +-- Instead of bulk closing the radacct sessions belonging to a reloaded NAS, +-- this strategy leaves them open and records the NAS reload time in the +-- nasreload table. +-- +-- Where applicable, the onus is on the administator to: +-- +-- * Consider the nas reload times when deriving a list of +-- active/inactive sessions, and when determining the duration of sessions +-- interrupted by a NAS reload. (Refer to the view below.) +-- +-- * Close the affected sessions out of band. (Refer to the +-- process-radacct-close-after_reload.pl script.) +-- +-- The radacct_with_reloads view presents the radacct table with two additional +-- columns: acctstoptime_with_reloads and acctsessiontime_with_reloads +-- +-- Where the session isn't closed (acctstoptime IS NULL), yet it started before +-- the last reload of the NAS (radacct.acctstarttime < nasreload.reloadtime), +-- the derived columns are set based on the reload time of the NAS (effectively +-- the point in time that the session was interrupted.) +-- +CREATE VIEW radacct_with_reloads AS +SELECT + a.*, + COALESCE(a.AcctStopTime, + CASE WHEN a.AcctStartTime < n.ReloadTime THEN n.ReloadTime END + ) AS AcctStopTime_With_Reloads, + COALESCE(a.AcctSessionTime, + CASE WHEN a.AcctStopTime IS NULL AND a.AcctStartTime < n.ReloadTime THEN + CAST((julianday(n.ReloadTime) - julianday(a.AcctStartTime)) * 86400 AS integer) + END + ) AS AcctSessionTime_With_Reloads +FROM radacct a +LEFT OUTER JOIN nasreload n USING (nasipaddress); diff --git a/raddb/mods-config/sql/main/sqlite/queries.conf b/raddb/mods-config/sql/main/sqlite/queries.conf new file mode 100644 index 0000000..35016f4 --- /dev/null +++ b/raddb/mods-config/sql/main/sqlite/queries.conf @@ -0,0 +1,635 @@ +# -*- text -*- +# +# main/sqlite/queries.conf -- SQLite configuration for default schema (schema.sql) +# +# Id: e1e83bf94814ed8be6239977b7bacfed21c0cd6a $ + +# Safe characters list for sql queries. Everything else is replaced +# with their mime-encoded equivalents. +# The default list should be ok +#safe_characters = "@abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_: /" + +####################################################################### +# Query config: Username +####################################################################### +# This is the username that will get substituted, escaped, and added +# as attribute 'SQL-User-Name'. '%{SQL-User-Name}' should be used below +# everywhere a username substitution is needed so you you can be sure +# the username passed from the client is escaped properly. +# +# Uncomment the next line, if you want the sql_user_name to mean: +# +# Use Stripped-User-Name, if it's there. +# Else use User-Name, if it's there, +# Else use hard-coded string "DEFAULT" as the user name. +#sql_user_name = "%{%{Stripped-User-Name}:-%{%{User-Name}:-DEFAULT}}" +# +sql_user_name = "%{User-Name}" + +####################################################################### +# Query config: Event-Timestamp +####################################################################### +# event_timestamp_epoch is the basis for the time inserted into +# accounting records. Typically this will be the Event-Timestamp of the +# accounting request, which is usually provided by a NAS. +# +# Uncomment the next line, if you want the timestamp to be based on the +# request reception time recorded by this server, for example if you +# distrust the provided Event-Timestamp. +#event_timestamp_epoch = "%l" + +event_timestamp_epoch = "%{%{integer:Event-Timestamp}:-%l}" + +# event_timestamp is the SQL snippet for converting an epoch timestamp +# to an SQL date. + +event_timestamp = "${event_timestamp_epoch}" + +# NOTE: Recent SQLite versions allow proper arithmetic with dates +# stored as strings including comparison using an index, so we keep +# these variables differentiated in preparation for switching away from +# integer storage. + +####################################################################### +# Query config: Class attribute +####################################################################### +# +# 3.0.22 and later have a "class" column in the accounting table. +# +# However, we do NOT want to break existing configurations by adding +# the Class attribute to the default queries. If we did that, then +# systems using newer versions of the server would fail, because +# there is no "class" column in their accounting tables. +# +# The solution to that is the following "class" subsection. If your +# database has a "class" column for the various tables, then you can +# uncomment the configuration items here. The queries below will +# then automatically insert the Class attribute into radacct, +# radpostauth, etc. +# +class { + # + # Delete the '#' character from each of the configuration + # items in this section. This change puts the Class + # attribute into the various tables. Leave the double-quoted + # string there, as the value for the configuration item. + # + # See also policy.d/accounting, and the "insert_acct_class" + # policy. You will need to list (or uncomment) + # "insert_acct_class" in the "post-auth" section in order to + # create a Class attribute. + # + column_name = # ", class" + packet_xlat = # ", '%{Class}'" + reply_xlat = # ", '%{reply:Class}'" +} + +####################################################################### +# Default profile +####################################################################### +# This is the default profile. It is found in SQL by group membership. +# That means that this profile must be a member of at least one group +# which will contain the corresponding check and reply items. +# This profile will be queried in the authorize section for every user. +# The point is to assign all users a default profile without having to +# manually add each one to a group that will contain the profile. +# The SQL module will also honor the User-Profile attribute. This +# attribute can be set anywhere in the authorize section (ie the users +# file). It is found exactly as the default profile is found. +# If it is set then it will *overwrite* the default profile setting. +# The idea is to select profiles based on checks on the incoming packets, +# not on user group membership. For example: +# -- users file -- +# DEFAULT Service-Type == Outbound-User, User-Profile := "outbound" +# DEFAULT Service-Type == Framed-User, User-Profile := "framed" +# +# By default the default_user_profile is not set +# +#default_user_profile = "DEFAULT" + +####################################################################### +# NAS Query +####################################################################### +# This query retrieves the radius clients +# +# 0. Row ID (currently unused) +# 1. Name (or IP address) +# 2. Shortname +# 3. Type +# 4. Secret +# 5. Server +####################################################################### + +client_query = "\ + SELECT id, nasname, shortname, type, secret, server \ + FROM ${client_table}" + +####################################################################### +# Authorization Queries +####################################################################### +# These queries compare the check items for the user +# in ${authcheck_table} and setup the reply items in +# ${authreply_table}. You can use any query/tables +# you want, but the return data for each row MUST +# be in the following order: +# +# 0. Row ID (currently unused) +# 1. UserName/GroupName +# 2. Item Attr Name +# 3. Item Attr Value +# 4. Item Attr Operation +####################################################################### + +# +# Use these for case sensitive usernames. +# +#authorize_check_query = "\ +# SELECT id, username, attribute, value, op \ +# FROM ${authcheck_table} \ +# WHERE username = BINARY '%{SQL-User-Name}' \ +# ORDER BY id" + +#authorize_reply_query = "\ +# SELECT id, username, attribute, value, op \ +# FROM ${authreply_table} \ +# WHERE username = BINARY '%{SQL-User-Name}' \ +# ORDER BY id" + +# +# The default queries are case insensitive. (for compatibility with older versions of FreeRADIUS) +# +authorize_check_query = "\ + SELECT id, username, attribute, value, op \ + FROM ${authcheck_table} \ + WHERE username = '%{SQL-User-Name}' \ + ORDER BY id" + +authorize_reply_query = "\ + SELECT id, username, attribute, value, op \ + FROM ${authreply_table} \ + WHERE username = '%{SQL-User-Name}' \ + ORDER BY id" + +# +# Use these for case sensitive usernames. +# +#group_membership_query = "\ +# SELECT groupname \ +# FROM ${usergroup_table} \ +# WHERE username = BINARY '%{SQL-User-Name}' \ +# ORDER BY priority" + +group_membership_query = "\ + SELECT groupname \ + FROM ${usergroup_table} \ + WHERE username = '%{SQL-User-Name}' \ + ORDER BY priority" + +authorize_group_check_query = "\ + SELECT id, groupname, attribute, \ + Value, op \ + FROM ${groupcheck_table} \ + WHERE groupname = '%{${group_attribute}}' \ + ORDER BY id" + +authorize_group_reply_query = "\ + SELECT id, groupname, attribute, \ + value, op \ + FROM ${groupreply_table} \ + WHERE groupname = '%{${group_attribute}}' \ + ORDER BY id" + +####################################################################### +# Simultaneous Use Checking Queries +####################################################################### +# simul_count_query - query for the number of current connections +# - If this is not defined, no simultaneous use checking +# - will be performed by this module instance +# simul_verify_query - query to return details of current connections +# for verification +# - Leave blank or commented out to disable verification step +# - Note that the returned field order should not be changed. +####################################################################### + +simul_count_query = "\ + SELECT COUNT(*) \ + FROM ${acct_table1} a \ + LEFT OUTER JOIN nasreload n USING (nasipaddress) \ + WHERE username = '%{SQL-User-Name}' \ + AND acctstoptime IS NULL \ + AND (a.acctstarttime > n.reloadtime OR n.reloadtime IS NULL)" + +simul_verify_query = "\ + SELECT radacctid, acctsessionid, username, nasipaddress, nasportid, framedipaddress, \ + callingstationid, framedprotocol \ + FROM ${acct_table1} a \ + LEFT OUTER JOIN nasreload n USING (nasipaddress) \ + WHERE username = '%{${group_attribute}}' \ + AND acctstoptime IS NULL \ + AND (a.acctstarttime > n.reloadtime OR n.reloadtime IS NULL)" + +####################################################################### +# Accounting and Post-Auth Queries +####################################################################### +# These queries insert/update accounting and authentication records. +# The query to use is determined by the value of 'reference'. +# This value is used as a configuration path and should resolve to one +# or more 'query's. If reference points to multiple queries, and a query +# fails, the next query is executed. +# +# Behaviour is identical to the old 1.x/2.x module, except we can now +# fail between N queries, and query selection can be based on any +# combination of attributes, or custom 'Acct-Status-Type' values. +####################################################################### +accounting { + reference = "%{tolower:type.%{%{Acct-Status-Type}:-%{Request-Processing-Stage}}.query}" + + # Write SQL queries to a logfile. This is potentially useful for bulk inserts + # when used with the rlm_sql_null driver. +# logfile = ${logdir}/accounting.sql + + column_list = "\ + acctsessionid, \ + acctuniqueid, \ + username, \ + realm, \ + nasipaddress, \ + nasportid, \ + nasporttype, \ + acctstarttime, \ + acctupdatetime, \ + acctstoptime, \ + acctsessiontime, \ + acctauthentic, \ + connectinfo_start, \ + connectinfo_stop, \ + acctinputoctets, \ + acctoutputoctets, \ + calledstationid, \ + callingstationid, \ + acctterminatecause, \ + servicetype, \ + framedprotocol, \ + framedipaddress, \ + framedipv6address, \ + framedipv6prefix, \ + framedinterfaceid, \ + delegatedipv6prefix \ + ${..class.column_name}" + + type { + accounting-on { + + # + # "Bulk update" Accounting-On/Off strategy. + # + # Immediately terminate all sessions associated with a + # given NAS. + # + # Note: If a large number of sessions require closing + # then the bulk update may be take a long time to run + # and lock an excessive number of rows. See the + # strategy below for an alternative approach that does + # not touch the radacct session data. + # + query = "\ + UPDATE ${....acct_table1} \ + SET \ + acctstoptime = ${....event_timestamp}, \ + acctsessiontime = \ + (${....event_timestamp_epoch} \ + - acctstarttime), \ + acctterminatecause = '%{Acct-Terminate-Cause}' \ + WHERE acctstoptime IS NULL \ + AND nasipaddress = '%{NAS-IP-Address}' \ + AND acctstarttime <= ${....event_timestamp}" + + # + # "Lightweight" Accounting-On/Off strategy. + # + # Record the reload time of the NAS and let the + # administrator actually close the sessions in radacct + # out-of-band, if desired. + # + # Implementation advice, together with a stored + # procedure for closing sessions and a view showing + # the effective stop time of each session is provided + # in process-radacct.sql. + # + # To enable this strategy, just change the previous + # query to "-query", and this one to "query". The + # previous one will be ignored, and this one will be + # enabled. + # + -query = "\ + INSERT OR REPLACE INTO nasreload (nasipaddress, reloadtime) \ + VALUES ('%{NAS-IP-Address}', ${....event_timestamp})" + + } + + accounting-off { + query = "${..accounting-on.query}" + } + + start { + # + # Insert a new record into the sessions table + # + query = "\ + INSERT INTO ${....acct_table1} \ + (${...column_list}) \ + VALUES \ + ('%{Acct-Session-Id}', \ + '%{Acct-Unique-Session-Id}', \ + '%{SQL-User-Name}', \ + '%{Realm}', \ + '%{NAS-IP-Address}', \ + '%{%{NAS-Port-ID}:-%{NAS-Port}}', \ + '%{NAS-Port-Type}', \ + ${....event_timestamp}, \ + ${....event_timestamp}, \ + NULL, \ + '0', \ + '%{Acct-Authentic}', \ + '%{Connect-Info}', \ + '', \ + '0', \ + '0', \ + '%{Called-Station-Id}', \ + '%{Calling-Station-Id}', \ + '', \ + '%{Service-Type}', \ + '%{Framed-Protocol}', \ + '%{Framed-IP-Address}', \ + '%{Framed-IPv6-Address}', \ + '%{Framed-IPv6-Prefix}', \ + '%{Framed-Interface-Id}', \ + '%{Delegated-IPv6-Prefix}' \ + ${....class.packet_xlat})" + + # + # When using "sql_session_start", you should comment out + # the previous query, and enable this one. + # + # Just change the previous query to "-query", + # and this one to "query". The previous one + # will be ignored, and this one will be + # enabled. + # + -query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctSessionId = '%{Acct-Session-Id}', \ + AcctUniqueId = '%{Acct-Unique-Session-Id}', \ + AcctAuthentic = '%{Acct-Authentic}', \ + ConnectInfo_start = '%{Connect-Info}', \ + ServiceType = '%{Service-Type}', \ + FramedProtocol = '%{Framed-Protocol}', \ + framedipaddress = '%{Framed-IP-Address}', \ + framedipv6address = '%{Framed-IPv6-Address}', \ + framedipv6prefix = '%{Framed-IPv6-Prefix}', \ + framedinterfaceid = '%{Framed-Interface-Id}', \ + delegatedipv6prefix = '%{Delegated-IPv6-Prefix}', \ + AcctStartTime = ${....event_timestamp}, \ + AcctUpdateTime = ${....event_timestamp} \ + WHERE UserName = '%{SQL-User-Name}' \ + AND NASIPAddress = '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}' \ + AND NASPortId = '%{%{NAS-Port-ID}:-%{NAS-Port}}' \ + AND NASPortType = '%{NAS-Port-Type}' \ + AND AcctStopTime IS NULL" + + # + # Key constraints prevented us from inserting a new session, + # use the alternate query to update an existing session. + # + query = "\ + UPDATE ${....acct_table1} SET \ + acctstarttime = ${....event_timestamp}, \ + acctupdatetime = ${....event_timestamp}, \ + connectinfo_start = '%{Connect-Info}' \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-Id}'" + } + + interim-update { + # + # Update an existing session and calculate the interval + # between the last data we received for the session and this + # update. This can be used to find stale sessions. + # + query = "\ + UPDATE ${....acct_table1} \ + SET \ + acctupdatetime = ${....event_timestamp}, \ + acctinterval = 0, \ + framedipaddress = '%{Framed-IP-Address}', \ + framedipv6address = '%{Framed-IPv6-Address}', \ + framedipv6prefix = '%{Framed-IPv6-Prefix}', \ + framedinterfaceid = '%{Framed-Interface-Id}', \ + delegatedipv6prefix = '%{Delegated-IPv6-Prefix}', \ + acctsessiontime = %{%{Acct-Session-Time}:-NULL}, \ + acctinputoctets = %{%{Acct-Input-Gigawords}:-0} \ + << 32 | %{%{Acct-Input-Octets}:-0}, \ + acctoutputoctets = %{%{Acct-Output-Gigawords}:-0} \ + << 32 | %{%{Acct-Output-Octets}:-0} \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-Id}'" + + # + # The update condition matched no existing sessions. Use + # the values provided in the update to create a new session. + # + query = "\ + INSERT INTO ${....acct_table1} \ + (${...column_list}) \ + VALUES \ + ('%{Acct-Session-Id}', \ + '%{Acct-Unique-Session-Id}', \ + '%{SQL-User-Name}', \ + '%{Realm}', \ + '%{NAS-IP-Address}', \ + '%{%{NAS-Port-ID}:-%{NAS-Port}}', \ + '%{NAS-Port-Type}', \ + (${....event_timestamp_epoch} - %{%{Acct-Session-Time}:-0}), \ + ${....event_timestamp}, \ + NULL, \ + %{%{Acct-Session-Time}:-NULL}, \ + '%{Acct-Authentic}', \ + '%{Connect-Info}', \ + '', \ + %{%{Acct-Input-Gigawords}:-0} << 32 | \ + %{%{Acct-Input-Octets}:-0}, \ + %{%{Acct-Output-Gigawords}:-0} << 32 | \ + %{%{Acct-Output-Octets}:-0}, \ + '%{Called-Station-Id}', \ + '%{Calling-Station-Id}', \ + '', \ + '%{Service-Type}', \ + '%{Framed-Protocol}', \ + '%{Framed-IP-Address}', \ + '%{Framed-IPv6-Address}', \ + '%{Framed-IPv6-Prefix}', \ + '%{Framed-Interface-Id}', \ + '%{Delegated-IPv6-Prefix}' \ + ${....class.packet_xlat})" + + # + # When using "sql_session_start", you should comment out + # the previous query, and enable this one. + # + # Just change the previous query to "-query", + # and this one to "query". The previous one + # will be ignored, and this one will be + # enabled. + # + -query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctSessionId = '%{Acct-Session-Id}', \ + AcctUniqueId = '%{Acct-Unique-Session-Id}', \ + AcctAuthentic = '%{Acct-Authentic}', \ + ConnectInfo_start = '%{Connect-Info}', \ + ServiceType = '%{Service-Type}', \ + FramedProtocol = '%{Framed-Protocol}', \ + framedipaddress = '%{Framed-IP-Address}', \ + framedipv6address = '%{Framed-IPv6-Address}', \ + framedipv6prefix = '%{Framed-IPv6-Prefix}', \ + framedinterfaceid = '%{Framed-Interface-Id}', \ + delegatedipv6prefix = '%{Delegated-IPv6-Prefix}', \ + AcctUpdateTime = ${....event_timestamp}, \ + AcctSessionTime = %{%{Acct-Session-Time}:-NULL}, \ + AcctInputOctets = '%{%{Acct-Input-Gigawords}:-0}' \ + << 32 | '%{%{Acct-Input-Octets}:-0}', \ + AcctOutputOctets = '%{%{Acct-Output-Gigawords}:-0}' \ + << 32 | '%{%{Acct-Output-Octets}:-0}' \ + WHERE UserName = '%{SQL-User-Name}' \ + AND NASIPAddress = '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}' \ + AND NASPortId = '%{%{NAS-Port-ID}:-%{NAS-Port}}' \ + AND NASPortType = '%{NAS-Port-Type}' \ + AND AcctStopTime IS NULL" + + } + + stop { + # + # Session has terminated, update the stop time and statistics. + # + query = "\ + UPDATE ${....acct_table2} SET \ + acctstoptime = ${....event_timestamp}, \ + acctsessiontime = %{%{Acct-Session-Time}:-NULL}, \ + acctinputoctets = %{%{Acct-Input-Gigawords}:-0} \ + << 32 | %{%{Acct-Input-Octets}:-0}, \ + acctoutputoctets = %{%{Acct-Output-Gigawords}:-0} \ + << 32 | %{%{Acct-Output-Octets}:-0}, \ + acctterminatecause = '%{Acct-Terminate-Cause}', \ + connectinfo_stop = '%{Connect-Info}' \ + WHERE AcctUniqueId = '%{Acct-Unique-Session-Id}'" + + # + # The update condition matched no existing sessions. Use + # the values provided in the update to create a new session. + # + query = "\ + INSERT INTO ${....acct_table2} \ + (${...column_list}) \ + VALUES \ + ('%{Acct-Session-Id}', \ + '%{Acct-Unique-Session-Id}', \ + '%{SQL-User-Name}', \ + '%{Realm}', \ + '%{NAS-IP-Address}', \ + '%{%{NAS-Port-ID}:-%{NAS-Port}}', \ + '%{NAS-Port-Type}', \ + (${....event_timestamp_epoch} - %{%{Acct-Session-Time}:-0}), \ + ${....event_timestamp}, \ + ${....event_timestamp}, \ + %{%{Acct-Session-Time}:-NULL}, \ + '%{Acct-Authentic}', \ + '', \ + '%{Connect-Info}', \ + %{%{Acct-Input-Gigawords}:-0} << 32 | \ + %{%{Acct-Input-Octets}:-0}, \ + %{%{Acct-Output-Gigawords}:-0} << 32 | \ + %{%{Acct-Output-Octets}:-0}, \ + '%{Called-Station-Id}', \ + '%{Calling-Station-Id}', \ + '%{Acct-Terminate-Cause}', \ + '%{Service-Type}', \ + '%{Framed-Protocol}', \ + '%{Framed-IP-Address}', \ + '%{Framed-IPv6-Address}', \ + '%{Framed-IPv6-Prefix}', \ + '%{Framed-Interface-Id}', \ + '%{Delegated-IPv6-Prefix}' \ + ${....class.packet_xlat})" + + # + # When using "sql_session_start", you should comment out + # the previous query, and enable this one. + # + # Just change the previous query to "-query", + # and this one to "query". The previous one + # will be ignored, and this one will be + # enabled. + # + -query = "\ + UPDATE ${....acct_table1} \ + SET \ + AcctSessionId = '%{Acct-Session-Id}', \ + AcctUniqueId = '%{Acct-Unique-Session-Id}', \ + AcctAuthentic = '%{Acct-Authentic}', \ + ConnectInfo_start = '%{Connect-Info}', \ + ServiceType = '%{Service-Type}', \ + FramedProtocol = '%{Framed-Protocol}', \ + framedipaddress = '%{Framed-IP-Address}', \ + framedipv6address = '%{Framed-IPv6-Address}', \ + framedipv6prefix = '%{Framed-IPv6-Prefix}', \ + framedinterfaceid = '%{Framed-Interface-Id}', \ + delegatedipv6prefix = '%{Delegated-IPv6-Prefix}', \ + AcctStopTime = ${....event_timestamp}, \ + AcctUpdateTime = ${....event_timestamp}, \ + AcctSessionTime = %{%{Acct-Session-Time}:-NULL}, \ + AcctInputOctets = '%{%{Acct-Input-Gigawords}:-0}' \ + << 32 | '%{%{Acct-Input-Octets}:-0}', \ + AcctOutputOctets = '%{%{Acct-Output-Gigawords}:-0}' \ + << 32 | '%{%{Acct-Output-Octets}:-0}', \ + AcctTerminateCause = '%{Acct-Terminate-Cause}', \ + ConnectInfo_stop = '%{Connect-Info}' \ + WHERE UserName = '%{SQL-User-Name}' \ + AND NASIPAddress = '%{%{NAS-IPv6-Address}:-%{NAS-IP-Address}}' \ + AND NASPortId = '%{%{NAS-Port-ID}:-%{NAS-Port}}' \ + AND NASPortType = '%{NAS-Port-Type}' \ + AND AcctStopTime IS NULL" + + } + + + # + # No Acct-Status-Type == ignore the packet + # + accounting { + query = "SELECT true" + } + } +} + +####################################################################### +# Authentication Logging Queries +####################################################################### +# postauth_query - Insert some info after authentication +####################################################################### + +post-auth { + # Write SQL queries to a logfile. This is potentially useful for bulk inserts + # when used with the rlm_sql_null driver. +# logfile = ${logdir}/post-auth.sql + + query = "\ + INSERT INTO ${..postauth_table} \ + (username, pass, reply, authdate ${..class.column_name}) \ + VALUES ( \ + '%{SQL-User-Name}', \ + '%{%{User-Password}:-%{Chap-Password}}', \ + '%{reply:Packet-Type}', \ + '%S.%M' \ + ${..class.reply_xlat})" +} diff --git a/raddb/mods-config/sql/main/sqlite/schema.sql b/raddb/mods-config/sql/main/sqlite/schema.sql new file mode 100644 index 0000000..4625a58 --- /dev/null +++ b/raddb/mods-config/sql/main/sqlite/schema.sql @@ -0,0 +1,164 @@ +----------------------------------------------------------------------------- +-- $Id$ -- +-- -- +-- schema.sql rlm_sql - FreeRADIUS SQLite Module -- +-- -- +-- Database schema for SQLite rlm_sql module -- +-- -- +----------------------------------------------------------------------------- + +-- +-- Table structure for table 'radacct' +-- +CREATE TABLE IF NOT EXISTS radacct ( + radacctid INTEGER PRIMARY KEY AUTOINCREMENT, + acctsessionid varchar(64) NOT NULL default '', + acctuniqueid varchar(32) NOT NULL default '', + username varchar(64) NOT NULL default '', + realm varchar(64) default '', + nasipaddress varchar(15) NOT NULL default '', + nasportid varchar(32) default NULL, + nasporttype varchar(32) default NULL, + acctstarttime datetime NULL default NULL, + acctupdatetime datetime NULL default NULL, + acctstoptime datetime NULL default NULL, + acctinterval int(12) default NULL, + acctsessiontime int(12) default NULL, + acctauthentic varchar(32) default NULL, + connectinfo_start varchar(128) default NULL, + connectinfo_stop varchar(128) default NULL, + acctinputoctets bigint(20) default NULL, + acctoutputoctets bigint(20) default NULL, + calledstationid varchar(50) NOT NULL default '', + callingstationid varchar(50) NOT NULL default '', + acctterminatecause varchar(32) NOT NULL default '', + servicetype varchar(32) default NULL, + framedprotocol varchar(32) default NULL, + framedipaddress varchar(15) NOT NULL default '', + framedipv6address varchar(45) NOT NULL default '', + framedipv6prefix varchar(45) NOT NULL default '', + framedinterfaceid varchar(44) NOT NULL default '', + delegatedipv6prefix varchar(45) NOT NULL default '', + class varchar(64) default NULL +); + +-- +-- You might not need all of these indexes. It should be safe to +-- delete indexes you do not use. For example, if you're not using +-- IPv6, you can delete the indexes on IPv6 attributes. +-- +-- You MUST however leave the indexes needed by the server, which +-- include username, acctstoptime, nasipaddress, acctstarttime, and +-- acctuniqueid. +-- +CREATE UNIQUE INDEX acctuniqueid ON radacct(acctuniqueid); +CREATE INDEX username ON radacct(username); +CREATE INDEX framedipaddress ON radacct (framedipaddress); +CREATE INDEX framedipv6address ON radacct (framedipv6address); +CREATE INDEX framedipv6prefix ON radacct (framedipv6prefix); +CREATE INDEX framedinterfaceid ON radacct (framedinterfaceid); +CREATE INDEX delegatedipv6prefix ON radacct (delegatedipv6prefix); +CREATE INDEX acctsessionid ON radacct(acctsessionid); +CREATE INDEX acctsessiontime ON radacct(acctsessiontime); +CREATE INDEX acctstarttime ON radacct(acctstarttime); +CREATE INDEX acctinterval ON radacct(acctinterval); +CREATE INDEX acctstoptime ON radacct(acctstoptime); +CREATE INDEX nasipaddress ON radacct(nasipaddress); +CREATE INDEX class ON radacct(class); + +-- +-- Table structure for table 'radcheck' +-- +CREATE TABLE IF NOT EXISTS radcheck ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username varchar(64) NOT NULL default '', + attribute varchar(64) NOT NULL default '', + op char(2) NOT NULL DEFAULT '==', + value varchar(253) NOT NULL default '' +); +CREATE INDEX check_username ON radcheck(username); + +-- +-- Table structure for table 'radgroupcheck' +-- +CREATE TABLE IF NOT EXISTS radgroupcheck ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + groupname varchar(64) NOT NULL default '', + attribute varchar(64) NOT NULL default '', + op char(2) NOT NULL DEFAULT '==', + value varchar(253) NOT NULL default '' +); +CREATE INDEX check_groupname ON radgroupcheck(groupname); + +-- +-- Table structure for table 'radgroupreply' +-- +CREATE TABLE IF NOT EXISTS radgroupreply ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + groupname varchar(64) NOT NULL default '', + attribute varchar(64) NOT NULL default '', + op char(2) NOT NULL DEFAULT '=', + value varchar(253) NOT NULL default '' +); +CREATE INDEX reply_groupname ON radgroupreply(groupname); + +-- +-- Table structure for table 'radreply' +-- +CREATE TABLE IF NOT EXISTS radreply ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username varchar(64) NOT NULL default '', + attribute varchar(64) NOT NULL default '', + op char(2) NOT NULL DEFAULT '=', + value varchar(253) NOT NULL default '' +); +CREATE INDEX reply_username ON radreply(username); + +-- +-- Table structure for table 'radusergroup' +-- +CREATE TABLE IF NOT EXISTS radusergroup ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username varchar(64) NOT NULL default '', + groupname varchar(64) NOT NULL default '', + priority int(11) NOT NULL default '1' +); +CREATE INDEX usergroup_username ON radusergroup(username); + +-- +-- Table structure for table 'radpostauth' +-- +CREATE TABLE IF NOT EXISTS radpostauth ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username varchar(64) NOT NULL default '', + pass varchar(64) NOT NULL default '', + reply varchar(32) NOT NULL default '', + authdate timestamp NOT NULL, + class varchar(64) default NULL +); +CREATE INDEX radpostauth_username ON radpostauth(username); +CREATE INDEX radpostauth_class ON radpostauth(class); + +-- +-- Table structure for table 'nas' +-- +CREATE TABLE IF NOT EXISTS nas ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + nasname varchar(128) NOT NULL, + shortname varchar(32), + type varchar(30) DEFAULT 'other', + ports int(5), + secret varchar(60) DEFAULT 'secret' NOT NULL, + server varchar(64), + community varchar(50), + description varchar(200) DEFAULT 'RADIUS Client' +); +CREATE INDEX nasname ON nas(nasname); + +-- +-- Table structure for table 'nasreload' +-- +CREATE TABLE IF NOT EXISTS nasreload ( + nasipaddress varchar(15) PRIMARY KEY, + reloadtime datetime NOT NULL +); diff --git a/raddb/mods-config/sql/moonshot-targeted-ids/mysql/queries.conf b/raddb/mods-config/sql/moonshot-targeted-ids/mysql/queries.conf new file mode 100644 index 0000000..68306db --- /dev/null +++ b/raddb/mods-config/sql/moonshot-targeted-ids/mysql/queries.conf @@ -0,0 +1,15 @@ +# -*- text -*- +# +# moonshot-targeted-ids/mysql/queries.conf -- Queries to update a MySQL Moonshot-Targeted-Ids table. +# +# $Id$ + +post-auth { + # Query to store the Moonshot-*-TargetedId + query = "\ + INSERT IGNORE INTO ${..moonshot_tid_table} \ + (gss_acceptor, namespace, username, targeted_id) \ + VALUES \ + ('%{control:Moonshot-MSTID-GSS-Acceptor}', '%{control:Moonshot-MSTID-Namespace}', \ + '%{tolower:%{User-Name}}', '%{control:Moonshot-MSTID-TargetedId}')" +} diff --git a/raddb/mods-config/sql/moonshot-targeted-ids/mysql/schema.sql b/raddb/mods-config/sql/moonshot-targeted-ids/mysql/schema.sql new file mode 100644 index 0000000..8a33dc1 --- /dev/null +++ b/raddb/mods-config/sql/moonshot-targeted-ids/mysql/schema.sql @@ -0,0 +1,8 @@ +CREATE TABLE `moonshot_targeted_ids` ( + `gss_acceptor` varchar(254) NOT NULL default '', + `namespace` varchar(36) NOT NULL default '', + `username` varchar(64) NOT NULL default '', + `targeted_id` varchar(128) NOT NULL default '', + `creationdate` timestamp NOT NULL default CURRENT_TIMESTAMP, + PRIMARY KEY (`username`,`gss_acceptor`,`namespace`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/raddb/mods-config/sql/moonshot-targeted-ids/postgresql/queries.conf b/raddb/mods-config/sql/moonshot-targeted-ids/postgresql/queries.conf new file mode 100644 index 0000000..f757a87 --- /dev/null +++ b/raddb/mods-config/sql/moonshot-targeted-ids/postgresql/queries.conf @@ -0,0 +1,15 @@ +# -*- text -*- +# +# moonshot-targeted-ids/postgresql/queries.conf -- Queries to update a PostgreSQL Moonshot-*-Targeted-Ids table. +# +# $Id$ + +post-auth { + # Query to store the Moonshot-*-TargetedId + query = "\ + INSERT INTO ${..moonshot_tid_table} \ + (gss_acceptor, namespace, username, targeted_id) \ + VALUES \ + ('%{control:Moonshot-MSTID-GSS-Acceptor}', '%{control:Moonshot-MSTID-Namespace}', \ + '%{tolower:%{User-Name}}', '%{control:Moonshot-MSTID-TargetedId}')" +} diff --git a/raddb/mods-config/sql/moonshot-targeted-ids/postgresql/schema.sql b/raddb/mods-config/sql/moonshot-targeted-ids/postgresql/schema.sql new file mode 100644 index 0000000..649c627 --- /dev/null +++ b/raddb/mods-config/sql/moonshot-targeted-ids/postgresql/schema.sql @@ -0,0 +1,8 @@ +CREATE TABLE moonshot_targeted_ids ( + gss_acceptor varchar(254) NOT NULL DEFAULT '', + namespace varchar(36) NOT NULL DEFAULT '', + username varchar(64) NOT NULL DEFAULT '', + targeted_id varchar(128) NOT NULL DEFAULT '', + creationdate TIMESTAMP with time zone NOT NULL default 'now()', + PRIMARY KEY (username, gss_acceptor, namespace) +); diff --git a/raddb/mods-config/sql/moonshot-targeted-ids/sqlite/queries.conf b/raddb/mods-config/sql/moonshot-targeted-ids/sqlite/queries.conf new file mode 100644 index 0000000..8cdb803 --- /dev/null +++ b/raddb/mods-config/sql/moonshot-targeted-ids/sqlite/queries.conf @@ -0,0 +1,15 @@ +# -*- text -*- +# +# moonshot-targeted-ids/sqlite/queries.conf -- Queries to update a sqlite Moonshot-*-Targeted-Ids table. +# +# $Id$ + +post-auth { + # Query to store the Moonshot-*-TargetedId + query = "\ + INSERT INTO ${..moonshot_tid_table} \ + (gss_acceptor, namespace, username, targeted_id) \ + VALUES \ + ('%{control:Moonshot-MSTID-GSS-Acceptor}', '%{control:Moonshot-MSTID-Namespace}', \ + '%{tolower:%{User-Name}}', '%{control:Moonshot-MSTID-TargetedId}')" +} diff --git a/raddb/mods-config/sql/moonshot-targeted-ids/sqlite/schema.sql b/raddb/mods-config/sql/moonshot-targeted-ids/sqlite/schema.sql new file mode 100644 index 0000000..71195ad --- /dev/null +++ b/raddb/mods-config/sql/moonshot-targeted-ids/sqlite/schema.sql @@ -0,0 +1,8 @@ +CREATE TABLE `moonshot_targeted_ids` ( + `gss_acceptor` varchar(254) NOT NULL default '', + `namespace` varchar(36) NOT NULL default '', + `username` varchar(64) NOT NULL default '', + `targeted_id` varchar(128) NOT NULL default '', + `creationdate` timestamp NOT NULL default CURRENT_TIMESTAMP, + PRIMARY KEY (`username`,`gss_acceptor`,`namespace`) +); diff --git a/raddb/mods-config/unbound/default.conf b/raddb/mods-config/unbound/default.conf new file mode 100644 index 0000000..9aac368 --- /dev/null +++ b/raddb/mods-config/unbound/default.conf @@ -0,0 +1,2 @@ +server: + num-threads: 2 |