#  -*- text -*-
######################################################################
#
#  This virtual server simplifies the process of sending CoA-Request or
#  Disconnect-Request packets to a NAS.
#
#  This virtual server will receive CoA-Request or Disconnect-Request
#  packets that contain *minimal* identifying information.  e.g. Just
#  a User-Name, or maybe just an Acct-Session-Id attribute.  It will
#  look up that information in a database in order to find the rest of
#  the session data.  e.g. NAS-IP-Address, NAS-Identifier, NAS-Port,
#  etc.  That information will be added to the packet, which will then
#  be sent to the NAS.
#
#  This process is useful because NASes require the CoA packets to
#  contain "session identification" attributes in order to to do CoA
#  or Disconnect.  If the attributes aren't in the packet, then the
#  NAS will NAK the request.  This NAK happens even if you ask to
#  disconnect "User-Name = bob", and there is only one session with a
#  "bob" active.
#
#  Using this virtual server makes the CoA or Disconnect process
#  easier.  Just tell FreeRADIUS to disconnect "User-Name = bob", and
#  FreeRADIUS will take care of adding the "session identification"
#  attributes.
#
#  The process is as follows:
#
#    - A CoA/Disconnect-Request is received by FreeRADIUS.
#    - The radacct table is searched for active sessions that match each of
#      the provided identifier attributes: User-Name, Acct-Session-Id. The
#      search returns the owning NAS and Acct-Unique-Id for the matching
#      session/s.
#    - We originate CoA/Disconenct-Requests containing the original content,
#      filtered as required, with session identification added, to the
#      corresponding NAS for each session.
#
#  This simplifies scripting directly against a set of NAS devices since a
#  script need only send a single CoA/Disconnect to FreeRADIUS which will
#  then:
#
#    - Lookup all active sessions belonging to a user, in the case that only a
#      User-Name attribute is provided in the request
#    - Populate all necessary session identifiers
#    - Handle routing of the request to the correct NAS, in the case of a
#      multi-NAS setup
#
#  For example, to disconnect a specific session:
#
#    $ echo 'Acct-Session-Id = "769df3 312343"' | \
#      radclient -t 15 127.0.0.1 disconnect testing123
#
#  To perform a CoA update of all active sessions belonging to a user:
#
#    $ cat <<EOF | radclient -t 15 127.0.0.1 coa testing123
#      User-Name = bob
#      Vendor-Specific.Cisco.AVPair = "subscriber:sub-qos-policy-out=q_out_uncapped"
#      EOF
#
#  In addition to configuring and activating this site, instances of the radius
#  client module must be created for each NAS, for example:
#
#      radius radius-originate-coa-192.0.2.1 {
#	      type = CoA-Request
#	      type = Disconnect-Request
#	      transport = udp
#	      udp {
#		      ipaddr = 192.0.2.1
#		      port = 1700
#		      secret = "testing123"
#	      }
#
#	      # We are "originating" these packets since including Proxy-State
#	      # may confuse the NAS
#	      originate = yes
#
#	      # Uncomment this to fire and forget, rather than wait and retry
#	      # replicate = yes
#      }
#
#  Refer to `mods-enabled/radius` as a detailed reference for configuring
#  client access to each NAS.


server coa {

	namespace = radius

	#  Listen on a local CoA port.
	#
	#  This uses the normal set of clients, with the same secret as for
	#  authentication and accounting.
	#
	listen {
		type = CoA-Request
		type = Disconnect-Request
		transport = udp
		udp {
			ipaddr = 127.0.0.1
			port = 3799
		}
	}

	dictionary {
		   uint32 sent-counter
		   string user-session
	}

	#
	#  Receive a CoA request
	#
	recv CoA-Request {
		#
		#  Lookup all active sessions matching User-Name and/or
		#  Acct-Session-Id and send updates for each session
		#
		#  The query returns a single result in the format:
		#
		#  NasIpAddress1#AcctSessionId1|NasIPAddress2#AcctSessionId2|...
		#
		#  i.e. each session is separated by '|', and attributes
		#  within a session are separated by '#'
		#
		#  You will likely have to update the SELECT to add in
		#  any other "session identification" attributes
		#  needed by the NAS.  These may include NAS-Port,
		#  NAS-Identifier, etc.  Only the NAS vendor knows
		#  what these attributes are unfortunately, so we
		#  cannot give more detailed advice here.
		#

		#
		#  Example MySQL lookup
		#
#		&control.user-session := %sql("SELECT IFNULL(GROUP_CONCAT(CONCAT(nasipaddress,'#',acctsessionid) separator '|'),'') FROM (SELECT * FROM radacct WHERE ('%{User-Name}'='' OR UserName='%{User-Name}') AND ('%{Acct-Session-Id}'='' OR acctsessionid = '%{Acct-Session-Id}') AND AcctStopTime IS NULL) a")

		#
		#  Example PostgreSQL lookup
		#
#		&control.user-session := %sql("SELECT STRING_AGG(CONCAT(nasipaddress,'#',acctsessionid),'|') FROM (SELECT * FROM radacct WHERE ('%{User-Name}'='' OR UserName='%{User-Name}') AND ('%{Acct-Session-Id}'='' OR acctsessionid = '%{Acct-Session-Id}') AND AcctStopTime IS NULL) a")

		#
		#  Keep a count of what we send.
		#
		&control.sent-counter := 0

		#
		#  Split the string and split into pieces.
		#
		if ("%explode(&control.user-session, '|')") {

			foreach &control.user-session {
				#
				#  Send an update for each session we find.
				#
				if ("%{Foreach-Variable-0}" =~ /([^#]*)#(.*)/) {
					#  NAS-IP-Address
					&control.NAS-IP-Address := "%{1}"

					#  Acct-Session-Id
					&control.Acct-Session-Id := "%{2}"

					subrequest CoA-Request {
						#
						#  The subrequest begins empty, so initially copy all attributes
						#  from the incoming request.
						#
						&request := &parent.request

						#
						#  Add/override the session identification attributes looked up
						#
						&request.Acct-Session-Id := &parent.control.Acct-Session-Id

						#
						#  Some NASs want these, others don't
						#
						&request.Event-Timestamp := "%l"
						&request.Message-Authenticator := 0x00

						#
						#  Remove attributes which will confuse the NAS
						#
						#  The NAS will "helpfully" NAK the packet
						#  if it contains attributes which are NOT
						#  "session identification" attributes.
						#

						#
						#  SQL-User-Name is a side-effect of the xlat
						#
						&request -= &SQL-User-Name[*]

						#
						#  Those attributes should be listed here
						#
						&request -= &Acct-Delay-Time[*]
						&request -= &Proxy-State[*]

						#
						#  Uncomment if the NAS does not expect User-Name
						#
						#&request -= &User-Name[*]

						#
						#  Call the radius client module instance for the NAS-IP-Address
						#
						switch &parent.control.NAS-IP-Address {
							#
							#  Repeat this block for each NAS
							#
							case "192.0.2.1" {

								#
								#  Increment count of sent updates
								#
								&parent.control.sent-counter += 1

								radius-originate-coa-192.0.2.1

							}

							#
							#  Likely a missing "case" block if we can't map NAS-IP-Address to a module
							#
							default {
								&parent.control += {
									&Reply-Message = "Missing map for NAS: %{parent.control.NAS-IP-Address}"
								}
							}

						}  # subrequest

					}

				}
			}  # foreach session
		}


		#
		#  Report what we did
		#
		if (&control.sent-counter) {
			&reply += {
				&Reply-Message = "Sent updates for %{control.sent-counter} active sessions"
			}

			ok
		} else {
			&reply += {
				&Reply-Message = "No active sessions found"
			}

			reject
		}

	}


	#
	#  Receive a Disconnect request
	#
	recv Disconnect-Request {
		#
		#  Populate this section as above, but use "subrequest Disconnect-Request"
		#
	}

}

