Using Radiator to re-write VLAN assignment (RADIUS tunnel) attributes

by Matthew Gast

Related link: http://www.open.com.au/radiator/index.html




VLANs are often used to ensure that traffic is segregated between user groups. Assigning users into VLANs based on their identity is an attractive way of enforcing user privilege--in fact, it was the main topic of the Interop Labs LAN Access Security class this year. VLAN assignment is accomplished by configuring a RADIUS server to pass back the VLAN identifier to a wireless access point or a switch. The precise method of passing VLAN assignment information was published last September as RFC 3580. Section 3.31 tells you clearly how to assign a VLAN to a user: in the final RADIUS Access-Accept message, put the VLAN ID in the Tunnel-Private-Group-ID attribute. Tunnel-Private-Group-ID was defined in RFC 2868 as a string, so RFC 3580 says that the numeric VLAN ID should be encoded a string and passed in the attribute.




Not all 802.1X authenticators follow RFC 3580, however. Some do VLAN assignment only through the use of vendor-specific RADIUS attributes. Others expect the Tunnel-Private-Group-ID attribute to contain an integer data type, not a string. Still others expect a string that corresponds to something else entirely. By far the most common non-compliant case, however, are the devices which expect to receive an integer data type instead of the string-encoded integer mandated by the standard.



The Demo at the Interop Labs


For our demonstration booth on the show floor at Interop, we had several RADIUS servers, all of which were configured with a set of users and attributes. We try to assess interoperability, so we had defined VLAN identifiers as strings, as required by RFC 3580. When I started working on our RADIUS proxy demonstration, however, I quickly determined that the wireless access point assigned to the demonstration was non-compliant. It required the VLAN identifier encoded as binary integer, not the string representation of that number. Fortunately, I was working with the Radiator RADIUS server, a powerful and flexible RADIUS server written entirely in Perl. Among many other capabilities, it has "hooks" to run arbitrary user-defined procedures at various check points in the authentication process. By writing my own hooks, I was able to selectively rewrite the VLAN identifiers for RADIUS clients that expected the identifier in the wrong format.




I had to use two different hooks, depending on whether the user was authenticated locally by Radiator or proxied to another RADIUS server. In the local authentication case, I used a PostAuthHook, which is run after the authentication transaction completes, but is not run when the request is proxied to another server. For the proxy authentications handled by external servers, I used a ReplyHook, which is run when the reply from the external RADIUS server is received. (Due to the arguments in the functions, I needed two separate hook procedures.)




Configuring the Hooks


The first thing I did was to define a "Identifier" string used by Radiator and passed along with authentication requests. I used the string "IntegerVLANTag" in radius.cfg, the main configuration file, to note which clients required me to rewrite the VLAN identifier from the standard string into a binary integer. Here's an example:




# HP420 AP for proxy demo
<Client 45.200.1.39>
Secret imnottelling
Identifier IntegerVLANTag
</Client>



To call the hooks, I needed to configure them with the appropriate authentication handlers. One of the back-end servers I was using was a FreeRADIUS server. The handler is configured to route authentication request for a username of the form user@freeradius to the FreeRADIUS server. There is a simple configuration section for the external server to use, and the reply hook is defined there. In the following configuration, the external file vlan-ascii-to-binary-reply contains a Perl procedure that will be run against the reply packet received from 45.200.1.11.




<Handler Realm=/freeradius/>
EAPAnonymous %0

<AuthBy RADIUS>
AuthPort 1812
AcctPort 1813
Host 45.200.1.11
Secret imnottelling

ReplyHook file:"%D/hooks/vlan-ascii-to-binary-reply"
</AuthBy>

</Handler>



For authentication requests which are handled locally, the configuration is a bit more complex because the authentication details are done locally. In this example, we are stripping the realm, so that user names of the form user@radiator become simply user. When authentication completes, the procedure in the file vlan-ascii-to-binary-postauth will be run to rewrite the VLAN identifier.




<Handler>
# Strip realm - @radiator goes away
RewriteUsername s/^([^@]+).*/$1/

<AuthBy FILE>
Filename %D/radiator-realm-users.txt

EAPType PEAP
EAPTLS_CertificateFile %D/certificates/cert-srv.pem
EAPTLS_CertificateType PEM

EAPTLS_PrivateKeyFile %D/certificates/cert-srv.pem
EAPTLS_PrivateKeyPassword imnottelling

EAPTLS_MaxFragmentSize 1000
AutoMPPEKeys
EAPTLS_PEAPVersion 0

</AuthBy>
PostAuthHook file:"%D/hooks/vlan-ascii-to-binary-postauth"

</Handler>


Show Me the Code: The Hooks Themselves


The PostAuthHook is run for locally-processed authentications. First, the hook pulls in the arguments passed to it: the request ($p), the reply ($rp), and the result of the authentication ($result). If the reply packet is an Access-Accept and the RADIUS client is defined to want binary integer VLAN identifiers, then the attribute rewriting takes place. I had to work around one additional wrinkle, which is that RADIUS servers may supply groups of tunnel attributes with a tag. My hook needed to preserve the tag on attributes, but rewrite the value from a string into a binary number.




# -*- mode: Perl -*-
# vlan-ascii-to-binary-postauth
#
# PostAuthHook to rewrite RFC 3580-compliant VLAN ID
# in Tunnel-Private-Group ID to integer for non-compliant
# 802.1X authenticators
#
# Author: Matthew Gast (msg@trpz.com)
# Interop Labs Las Vegas 2004
#

sub
{
my $p = ${$_[0]};
my $rp = ${$_[1]};
my $result = ${$_[2]};

my $ASCIIvlan;
my $binaryvlan;

my $identifier;
my $tag;

$identifier = $p->{Client}->{Identifier};
if (($result == $main::ACCEPT) && ($identifier == "IntegerVLANTag"))
{
&main::log($main::LOG_DEBUG, "ASCII-to-VLAN tag rewriter called");
$ASCIIvlan = $rp->get_attr('Tunnel-Private-Group-ID');
# check for attribute tag
if ($ASCIIvlan =~ /^(\d+):(.*)/)
{
# tagged attribute
&main::log($main::LOG_DEBUG, "Found tagged ASCII VLAN attribute of $ASCIIvlan");
$binaryvlan = pack 'N', $ASCIIvlan | $1 << 24;
}
else
{
# untagged attribute
&main::log($main::LOG_DEBUG, "Found untagged ASCII VLAN attribute of $ASCIIvlan");
$binaryvlan = pack ('N',unpack('a*',$ASCIIvlan));
}

# Replace attribute
&main::log($main::LOG_DEBUG, "Replacing ASCII vlan tag with $binaryvlan");
$rp->change_attr('Tunnel-Private-Group-ID', $binaryvlan);
}
return;
}



The ReplyHook uses the same code for the guts, but it needs to pick up arguments that are passed in a slightly different order. Also, the ReplyHook is run for every response packet from the remote RADIUS server, so it may be run several times before it acts on an Access-Accept message. I didn't find a way around this when I was trying to get the demonstration running before the show floor opened.




# -*- mode: Perl -*-
# vlan-ascii-to-binary-reply
#
# ReplyHook to rewrite RFC 3580-compliant VLAN ID
# in Tunnel-Private-Group ID to integer for non-compliant
# 802.1X authenticators
#
# Author: Matthew Gast (msg@trpz.com)
# Interop Labs Las Vegas 2004
#

sub
{
my $rp = ${$_[1]};
my $p = ${$_[2]};

my $ASCIIvlan;
my $binaryvlan;

my $identifier;
my $tag;

$identifier = $p->{Client}->{Identifier};
&main::log($main::LOG_DEBUG, "ReplyHook VLAN rewriter called.");
if ($identifier == "IntegerVLANTag")
{
&main::log($main::LOG_DEBUG, "ASCII-to-VLAN tag rewriter called");
$ASCIIvlan = $rp->get_attr('Tunnel-Private-Group-ID');
# check for attribute tag
if ($ASCIIvlan =~ /^(\d+):(.*)/)
{
# tagged attribute
&main::log($main::LOG_DEBUG, "Found tagged ASCII VLAN attribute of $ASCIIvlan");
$binaryvlan = pack 'N', $2 | $1 << 24;
}
else
{
# untagged attribute
&main::log($main::LOG_DEBUG, "Found untagged ASCII VLAN attribute of $ASCIIvlan");
$binaryvlan = pack ('N',unpack('a*',$ASCIIvlan));
}

# Replace attribute
&main::log($main::LOG_DEBUG, "Replacing ASCII vlan tag with $binaryvlan");
$rp->change_attr('Tunnel-Private-Group-ID', $binaryvlan);
}
return;
}



How often do you have to deal with devices that don't comply with RFC 3580? Do you work around it on the RADIUS server, or in some other way?