Score:1

Freeradius with dhcp server: Calls to perl module returns error

us flag

This is a continuation of my previous question in sending static routes from Freeradius DHCP server implementation in combination with Strongswan VPN server.

When debugging Freeradius using tcpdump and Wireshark I found out that I can send classless static routes from Freeradius DHCP server by through adding DHCP-Classless-Static-Route and DHCP-Site-specific-25 (aka Microsoft static route) options to my DHCP-Discover and DHCP-Request sections of the dhcp server configuration file.

However: It appears that the static routes are not accepted by Microsoft VPN client if I set the default gateway to be 0.0.0.0 as suggested by Strongswan documentation.

At least I cannot find the advetised routes on my Windows client when using route print -4.

Also I cannot add the routes manually on Windows client when I am using 0.0.0.0 as standard gateway over VPN interface.

However:

Lets say I want to access the subnet 192.168.200.0/24 over VPN and my VPN server assign the address 192.168.201.2/24 to my Windows client. Then it is actually possible to create a static route on windows client side by declaring that the subnet 192.168.200.0/24 is accessible via 192.168.201.2 using the windows command:

route add 192.168.200.0 mask 255.255.255.0 192.168.201.2

I know it looks a bit weird, but I can ping any host on the 192.168.200.0 subnet, so as long as it works I am happy. :-)

But: I would be more happy if I could do the same thing by advertising the routes from my VPN server instead of doing it manually on all VPN clients. :-)

That means I have to do a bit of dynamic programming to the DHCP configuration in Freeradius. I my case it means I have to make a reference to a perl module in DHCP-Discover and DHCP-request that grabs the assigned client vpn ip address, convert it into octets and combine it with the static routes that is also given as octets.

An example:

The subnet 192.168.200.0/24 will be encoded as 0x18c0a8c8 as subnet mask is encoded first.

The client 192.168.201.2/24 will be encoded as 0xc0a8c902 as it is just converting each number in the ip address to hex.

The final encoding for the route will be: 0x18c0a8c8c0a8c902 as it is just a concatination of the two strings.

I then have to use update reply with the following code:

  update reply {
    &DHCP-Classless-Static-Route = 0x18c0a8c8c0a8c902
    &DHCP-Site-specific-25 = 0x18c0a8c8c0a8c902
  }

If there are any more routes then all the routes will be concatenated into one long string.

The tricky part:

Assume you have the default configuration of Freeradius DHCP server as found in freeradius/3.0/sites-available/dhcp file.

The general structure of the file for DHCP-Discover and DHCP-Request is as follows:

dhcp DHCP-Request {
  update reply {
    &DHCP-Message-Type = DHCP-Ack
  }

  update reply {
    # General DHCP options, such as default GW, DNS, IP-address lease time etc.
  }

  update control {
    &Pool-Name := "vpn_pool"
  }

  dhcp_sqlippool

  ok
}

Then as far as I have gathered I need to call my perl module after dhcp_sqlippool has been called and before returning ok, because dhcp_sqlippool is the module that assigns the ipaddress to the VPN client.

That means my version would be something like:

dhcp DHCP-Request {
  update reply {
    &DHCP-Message-Type = DHCP-Ack
  }

  update reply {
    # General DHCP options, such as default GW, DNS, IP-address lease time etc.
  }

  update control {
    &Pool-Name := "vpn_pool"
  }

  dhcp_sqlippool

  perl

  # If perl module returned no error
  if(ok) {
    update reply {
      # Perl-Route contains a hex encoded string with all routes.
      &DHCP-Classless-Static-Route = Perl-Route
      &DHCP-Site-specific-25 = Perl-Route      
    }
  }

  # Not sure if this one is needed?
  update reply {
    &DHCP-End-Of-Options = 255
  }

  ok
}

In order to make it work, I have to enable perl under the freeradius/3.0/mods-enabled folder and modify the filename in freeradius/3.0/mods-enabled/perl to point it to my perl module. Like for instance:

filename = ${modconfdir}/${.:instance}/dhcp/Options.pm

But how do I do reference the call to perl the right way?

I thought I had to enable the line func_post_auth = post_auth in freeradius/3.0/mods-enabled/perl and create a sub post_auth section in my perl module to handle calls from Freeradius, but as far as I can see in my log I get the following error in Freeradius:

(8) perl: perl_embed:: module = /etc/freeradius/3.0/mods-config/perl/dhcp/Options.pm , 
func = post_auth exit status= Undefined subroutine &main::post_auth called.
...
(8)     [perl] = fail
(8)   } # dhcp DHCP-Discover = fail

So what is it that I am not seeing?

Score:0
us flag

I banged my head against the wall a few times, but at least I got the perl module to work, though I am not completely where I want to be, since static routing via DHCP does not pass from Freeradius DHCP server to VPN client via Strongswan, but debugging UDP packages from Freeradius DHCP server implies the issue lies elsewhere.

Anyhow here is what I did:

  1. Enable perl module in freeradius/3.0/mods-enabled and set at least the following lines:
perl {
  # Perl code location: ("freeradius/3.0/mods-config/dhcp/Options.pm")
  filename = ${modconfdir}/${.:instance}/dhcp/Options.pm

  # DHCP module is called during freeradius post_auth
  func_post_auth = post_auth
}
  1. Modify freeradius/3.0/sites-enabled/dhcp The relevant places are DHCP-Discover and DHCP-Request:
dhcp DHCP-Discover {

        update reply {
               DHCP-Message-Type = DHCP-Offer
        }

        #  The contents here are invented.  Change them!
        update reply {
                &DHCP-Domain-Name-Server = 192.168.200.1
                &DHCP-Subnet-Mask = 255.255.255.0
                &DHCP-IP-Address-Lease-Time = 86400
                &DHCP-DHCP-Server-Identifier = 192.168.200.4
        }

        #  Or, allocate IPs from the DHCP pool in SQL. You may need to
        #  set the pool name here if you haven't set it elsewhere.
        update control {
                &Pool-Name := "vpn_pool"
        }

        dhcp_sqlippool

        # Call static route generation.
        perl

        ok
}

dhcp DHCP-Request {

        # Response packet type. See DHCP-Discover section above.
        update reply {
               &DHCP-Message-Type = DHCP-Ack
        }

        #  The contents here are invented.  Change them!
        update reply {
                &DHCP-Domain-Name-Server = 192.168.200.1
                &DHCP-Subnet-Mask = 255.255.255.0
                &DHCP-IP-Address-Lease-Time = 86400
                &DHCP-DHCP-Server-Identifier = 192.168.200.4
        }

        #  Or, allocate IPs from the DHCP pool in SQL. You may need to
        #  set the pool name here if you haven't set it elsewhere.
        update control {
                &Pool-Name := "vpn_pool"
        }
 
        dhcp_sqlippool

        # Call static route generation.
        perl

        ok
}
  1. Create the perl code located at freeradius/3.0/mods-config/perl/dhcp/Options.pm:
use strict;
use warnings;
use Data::Dumper;
use Net::IP;

# Bring the global hashes into the package scope
our (%RAD_REQUEST, %RAD_REPLY, %RAD_CHECK);

#
# 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/radiusd.h
use constant    L_DBG=>   1;
use constant    L_AUTH=>  2;
use constant    L_INFO=>  3;
use constant    L_ERR=>   4;
use constant    L_PROXY=> 5;
use constant    L_ACCT=>  6;

# Function to handle post_auth

sub post_auth {

    # Get VPN Client IP from Freeradius DHCP server.
    my $client_ip = new Net::IP ( $RAD_REQUEST{'DHCP-Requested-IP-Address'} ) or die (Net::IP::Error());

    # An example of 2 routing rules sent ('192.168.20.0/24' and '192.168.200.0/24') 
    my @routes = (new Net::IP('192.168.20/24'), new Net::IP('192.168.200/24'));

    # Measure how many elements there is in the routes array.
    my $size = @routes;

    # Convert client ip into hex code.
    my $client_octets = get_ip_octets ($client_ip->ip(),$client_ip->prefixlen());

    # Freeradius want the encoded string start with '0x'
    # followed by the encoded octets as hex.
    my $octet_str = "0x";

    for(my $i = 0; $i < $size; $i++)
    {
        # Convert subnet into octets, skipping ending zeroes.
        my $route_octets = get_ip_octets ($routes[$i]->ip(),$routes[$i]->prefixlen());

        # Convert network prefix into octets
        my $hex_prefix = sprintf("%02x", $routes[$i]->prefixlen());

        # Route is encoded by network octets followed by subnet octets
        $route_octets = $hex_prefix . $route_octets;

        # The entire route string is the route octets followed by gateway octets ('the client vpn ip').
        my $route_str = $route_octets . $client_octets;

        $octet_str = $octet_str . $route_str;
    }

    # Classless static routing (dhcp option 121)
    $RAD_REPLY{'DHCP-Classless-Static-Route'} = $octet_str;

    # Microsoft classless static routing (dhcp option 249)
    $RAD_REPLY{'DHCP-Site-specific-25'} = $octet_str;

    return RLM_MODULE_OK;

}

sub get_ip_octets {
    # First parameter: Source ip address
    my $sip = $_[0];

    # Second parameter: Bitlength of network (aka CIDR notation).
    my $cidr = $_[1];

    my @decimals = split('\.', $sip);
    my $index = int($cidr / 8) ;

    my $result = '';
    for(my $i = 0; $i < $index; $i++)
    {
        # Convert each number in ip address to hex and format with leading
        # zero in case converted number is less than 16.
        $result = $result . sprintf("%02x", $decimals[$i]);
    }

    return $result;
}

The perl code can be tweaked from here, so option 121 or option 249 is sent depending on client operating system.

I also leave the possibility to make the code more generic, so static routes can be defined directly in Freeradius config file to the reader.

mangohost

Post an answer

Most people don’t grasp that asking a lot of questions unlocks learning and improves interpersonal bonding. In Alison’s studies, for example, though people could accurately recall how many questions had been asked in their conversations, they didn’t intuit the link between questions and liking. Across four studies, in which participants were engaged in conversations themselves or read transcripts of others’ conversations, people tended not to realize that question asking would influence—or had influenced—the level of amity between the conversationalists.