# -*- 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.
#    - The original CoA/Disconnect-Request content is written to a detail file
#      with custom attributes representing the NAS and Acct-Session-Id.
#    - A detail reader follows the file and originates CoA/Disconenct-Requests
#      containing the original content, relayed to the corresponding NAS for
#      each session using the custom attributes.
#
#  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
#    - 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 127.0.0.1 disconnect testing123
#
#  To perform a CoA update of all active sessions belonging to a user:
#
#    $ cat <<EOF | radclient 127.0.0.1 coa testing123
#      User-Name = bob
#      Cisco-AVPair = "subscriber:sub-qos-policy-out=q_out_uncapped"
#      EOF
#
#  In addition to configuring and activating this site, a detail
#  writer module must be configured in mods-enabled:
#
#    detail detail_coa {
#        filename = ${radacctdir}/detail_coa
#        escape_filenames = no
#        permissions = 0600
#        header = "%t"
#        locking = yes
#    }
#


#  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
	ipaddr = 127.0.0.1
	port = 3799
	virtual_server = coa
}

#
#  Receive CoA/Disconnect, lookup sessions, write them to a detail file
#
server coa {
	#
	#  When a packet is received, it is processed through the
	#  recv-coa section.  This applies to *both* CoA-Request and
	#  Disconnect-Request packets.
	#
	recv-coa {
		#
		#  Lookup all active sessions matching User-Name and/or
		#  Acct-Session-Id and write each session (which includes to
		#  owning NAS and session ID) to a detail file.
		#
		#  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.
		#
		update control {

			#
			#  Example MySQL lookup
			#
#			Tmp-String-0 := "%{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
			#
#			Tmp-String-0 := "%{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}"

		}

		#
		#  Split the string and split into pieces.
		#
		if (&control:Tmp-String-0 != "" && "%{explode:&control:Tmp-String-0 |}") {
			foreach &control:Tmp-String-0 {
				if ("%{Foreach-Variable-0}" =~ /([^#]*)#(.*)/) {
					update request {
						CoA-Packet-Type := "%{Packet-Type}"

						#
						#  Or use CoA-Packet-DST-IPv6-Address
						#  for IPv6 address.
						#
						CoA-Packet-DST-IP-Address := "%{1}"

						CoA-Acct-Session-Id := "%{2}"

						#
						#  Add any other attributes here.
						#

						# Set the CoA/Disconnect port
						CoA-Packet-DST-Port := 1700

						# SQL-User-Name was left over
						# from the xlat
						SQL-User-Name !* ANY

						# Uncomment if the NAS does not
						# expect User-Name
						#User-Name !* ANY

					}

					#
					#  If we're sending a CoA packet, send it out.
					#
					if ((CoA-Packet-DST-IP-Address || CoA-Packet-DST-IPv6-Address) && \
					    CoA-Acct-Session-Id != "") {
						detail_coa.accounting
					}
				}
			}
		} else {
			# No sessions found
			reject
		}

	}
}

#
#  Detail file reader that processes the queue of CoA/Disconnect requests
#
server coa-buffered-reader {
	listen {
		#
		#  See sites-available/buffered-sql for more details on
		#  all the options available for the detail reader.
		#
		type = detail
		filename = "${radacctdir}/detail_coa"
		load_factor = 90
		track = yes
	}

	#
	#  For historical reasons packets from the detail file reader are
	#  processed through the "accounting" section.
	#
	accounting {
		switch &CoA-Packet-Type {
			case "Disconnect-Request" {
				update {
					#  Include given attributes
					disconnect: += request:[*]

					disconnect:Packet-DST-Port := \
						&CoA-Packet-DST-Port

					disconnect:Acct-Session-Id := \
						&CoA-Acct-Session-Id

					#  Some NASs want these, others don't
					disconnect:Event-Timestamp := "%l"
					disconnect: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.
					#
					#  Those attributes should be listed here.
					#
					disconnect:Acct-Delay-Time !* ANY
					disconnect:Proxy-State !* ANY
				}

				if (CoA-Packet-DST-IP-Address) {
					update {
						disconnect:Packet-DST-IP-Address := \
							&CoA-Packet-DST-IP-Address
					}
				} else {
					update {
						disconnect:Packet-DST-IPv6-Address := \
							&CoA-Packet-DST-IPv6-Address
					}
				}
			}

			case "CoA-Request" {
				update {
					#  Include given attributes
					coa: += request:[*]

					coa:Packet-DST-Port := \
						&CoA-Packet-DST-Port

					coa:Acct-Session-Id := \
						&CoA-Acct-Session-Id

					#  Some NASs want these, others don't
					coa:Event-Timestamp := "%l"
					coa: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.
					#
					#  Those attributes should be listed here.
					#
					coa:Acct-Delay-Time !* ANY
					coa:Proxy-State !* ANY
				}

				if (CoA-Packet-DST-IP-Address) {
					update {
						coa:Packet-DST-IP-Address := \
							&CoA-Packet-DST-IP-Address
					}
				} else {
					update {
						coa:Packet-DST-IPv6-Address := \
							&CoA-Packet-DST-IPv6-Address
					}
				}

			}
		}

		#
		#  ACK the CoA / Disconnect packet.
		#
		ok
	}
}


#  The CoA packet is in the "proxy-request" attribute list.
#  The CoA reply (if any) is in the "proxy-reply" attribute list.
#
server originate-coa-relay {
	#
	#  Handle the responses here.
	#
	post-proxy {
		switch &proxy-reply:Packet-Type {
			case CoA-ACK {
				ok
			}

			case CoA-NAK {
				# the NAS didn't like the CoA request
				ok
			}

			case Disconnect-ACK {
				ok
			}

			case Disconnect-NAK {
				# the NAS didn't like the Disconnect request
				ok
			}

			#  Invalid packet type.  This shouldn't happen.
			case {
				fail
			}
		}

		#
		#  These methods are run when there is NO response
		#  to the request.
		#
		Post-Proxy-Type Fail-CoA {
			ok
		}

		Post-Proxy-Type Fail-Disconnect {
			ok
		}
	}
}


#
#  Homeserver CoA / Disconnect endpoints
#
#  See proxy.conf for more details on configuring a home_server and
#  home_server_pool.
#
home_server coa-nas1 {
	type = coa

	# Update these to match your NAS
	ipaddr = 192.0.2.1
	port = 1700
	secret = testing1234

	coa {
		irt = 2
		mrt = 16
		mrc = 5
		mrd = 30
	}
}
home_server_pool coa-nas1 {
	type = fail-over
	home_server = coa-nas1
	virtual_server = originate-coa-relay
}
