FreeRadius Wifi PEAP/MSCHAPv2

Install FreeRadius on FreeBSD for authenticate wifi using 802.1x assigne VLAN

FreeRadius Wifi PEAP/MSCHAPv2

FreeRadius server set up on FreeBSD
Join domain with Samba, Authentication use mschapv2
Assigned VLAN by AD group via mod_perl

Request Certificate

openssl.conf

[ req ]
prompt = no
encrypt_key = no
default_bits = 2048
req_extensions = v3_req
distinguished_name = req_distinguished_name

[ req_distinguished_name ]
commonName               = wifi-bkk.example.com
emailAddress             = ne@example.com
countryName              = TH
stateOrProvinceName      = Bangkok
localityName             = BKK
0.organizationName       = Example
organizationalUnitName   = NE

[ v3_req ]
basicConstraints = CA:FALSE
keyUsage = digitalSignature, keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
IP.1  = 192.168.214.250
DNS.1 = wifi-bkk.example.com

[ xpclient_ext ]
extendedKeyUsage = 1.3.6.1.5.5.7.3.2

[ xpserver_ext ]
extendedKeyUsage = 1.3.6.1.5.5.7.3.1

Generate server certificate

openssl req -new -nodes -keyout key.pem -out req.pem -days 1825 -config openssl.cnf

Generate dh file

openssl dhparam -check -text -5 -out dh 4096
dd if=/dev/urandom of=random count=2

Secure cert file

chown -R freeradius:freeradius cert
chmod -R 0700 cert
chmod 0400 cert/*
openssl x509 -text -noout -in /root/cert/cert.crt

Install software

cd /usr/ports/net/freeradius3
make install clean
cd /usr/ports/net/samba413
make install clean

Config freeradius

Enable mod_perl
ln -s /usr/local/etc/raddb/mods-available/perl /usr/local/etc/raddb/mods-enabled/perl
vi /usr/local/etc/raddb/mods-available/perl

perl {
  filename = ${modconfdir}/perl/radius.pl
}

vi /usr/local/etc/raddb/mods-available/eap

eap {
     default_eap_type = peap

     timer_expire = 60
     ignore_unknown_eap_types = no
     cisco_accounting_username_bug = no
     max_sessions = ${max_requests}

     tls-config tls-common {
         private_key_file = /root/cert/key.pem
         certificate_file = /root/cert/cert.crt
         ca_file = /root/cert/cacert.crt

         dh_file = /root/cert/dh
         # random_file = /root/cert/random

         check_crl = no
         #ca_path = ${cadir}

         cipher_list = "DEFAULT"
         ecdh_curve = "prime256v1"

         cache {
             enable = yes
             lifetime = 24 # hours
             max_entries = 255
         }
     }

     peap {
         tls = tls-common
         default_eap_type = mschapv2

         copy_request_to_tunnel = no
         use_tunneled_reply = no

         virtual_server = "inner-tunnel"
     }

     mschapv2 {
         # send_error = no
     }
}

vi /usr/local/etc/raddb/mods-available/mschap

mschap {
    with_ntdomain_hack = yes
    ntlm_auth = "/usr/local/bin/ntlm_auth --use-cached-creds --request-nt-key --username=%{mschap:User-Name:-%{Stripped-User-Name}:-%{%{User-Name}:-None}} --challenge=%{%{mschap:Challenge}:-00} --nt-response=%{%{mschap:NT-Response}:-00}"
}

vi /usr/local/etc/raddb/clients.conf

client localhost {
     ipaddr = 127.0.0.1
     proto = *
     secret = test
     require_message_authenticator = no
     nas_type         = other        # localhost isn't usually a NAS...
}
# IPv6 Client
client localhost_ipv6 {
     ipv6addr        = ::1
     secret          = testing123
} 
#AP.BKK2
client AP-BKK2 {
     ipaddr = 192.168.214.250
     proto = *
     secret = xxxxxxxx
     require_message_authenticator = no
     nas_type         = other        # localhost isn't usually a NAS...
}

vi /usr/local/etc/raddb/sites-available/default

authorize {
     chap
     mschap
     eap {
         ok = return
     }
     files
}
authenticate {
     mschap
     eap
}
accounting {
     perl
}
post-auth {
     perl
}

Config Samba

vi /usr/local/etc/smb4.conf

[global]
    netbios name = BKK2-RADIUS
    workgroup = EXAMPLE
    server string = BKK2 RADIUS
    security = ads
    invalid users = root
    socket options = TCP_NODELAY
    idmap uid = 16777216-33554431
    idmap gid = 16777216-33554431
    winbind use default domain = yes
    winbind max domain connections = 5
    winbind max clients = 1000
    # allow enumeration of winbind users and groups
    winbind enum users = yes
    winbind enum groups = yes
    password server = *
    realm = example.com

Join domain

vi /etc/hosts

192.168.213.240         bkk2-radius.example.com bkk2-radius
net ads join -U Administrator
net ads testjoin
ntlm_auth --username=user --domain=domain

winbindpriv permission

chown root:freeradius /var/db/samba4/winbindd_priviliged/pipe
chown root:freeradius /var/db/samba4/winbindd_priviliged

vi /etc/nsswitch.conf

group: files winbind
passwd: files winbind

vi /etc/rc.conf

samba_server_enable="YES" 
radiusd_enable="YES"

Conifg perl radius

For assigne VLAN and accounting on Fortigate
sub post_auth for assigne VLAN and add group
sub accounting for accounting, query MAC/IP tuple from Fortigate(Main router)
Enable/disable function on use
vi /usr/local/etc/raddb/mods-config/perl/radius.pl

#!/usr/local/bin/perl

use utf8;
use strict;
use warnings;
use Data::Dumper;

# Filter AD group name
my $ad_filter = 'AD\-(?:Executives|Managers|Engineering|Web|Support|Billing)';
# Use for Query ARP/IP
my $ssh_arg   = '-p 55 192.168.254.66';

my $radius_host   = '192.168.254.66';
my $radius_secret = 'fortigate';
my $radius_called = 'BKK2';

# find vlan for user
sub find_vlan {
	my %class = map { $_ => 1 } @_;

	return 215 if ( $class{'AD-Engineering'} );
	return 216 if ( $class{'AD-Billing'} );
	return 215 if ( $class{'AD-Web'} );
	return 215 if ( $class{'AD-Managers'} );
	return 215 if ( $class{'AD-Executives'} );
	return 214 if ( $class{'AD-Support'} );
	return 217; # Other/Unknow = Guest VLAN
}

sub send_rad_ip {
	my $host       = shift;
	my $secret     = shift;
	my $user       = shift;
	my $ip         = shift;
	my $action     = shift;
	my @acct_class = @_;

	open( RAD, "| /usr/local/bin/radclient -q $host acct $secret" )
	  || return 1;

	print RAD "Acct-Status-Type=$action\n";
	print RAD "Acct-Authentic=RADIUS\n";
	print RAD "Acct-Session-Id=$ip-$user\n";
	print RAD "Called-Station-Id=00-50-56-BD-04-71:$radius_called\n";
	print RAD "User-Name=$user\n";
	for (@acct_class) {
		print RAD "Class=$_\n";
	}
	print RAD "Service-Type=Framed\n";
	print RAD "Framed-IP-Address=$ip\n" if ($ip);

	close RAD;
	return 0;
}

my $regex_ip = '(?:\d{1,3}\.){3}\d{1,3}';
my $regex_mac =
  '(?:(?:[0-9A-Fa-f]{2}[:\-]?){2}[\.:\-]){2}(?:[0-9A-Fa-f]{2}[:\-]?){2}';

# Normalized mac address
sub extract_mac {
	shift =~ /($regex_mac)/;
	return 0 unless ($1);
	my $mac = uc $1;
	$mac =~ s/[^0-9A-F]//g;
	$mac = lc($mac);
	$mac =~ s/..\K(?=.)/\:/sg;
	return $mac;
}

# Normalized username
sub strip_username {
	my $user = shift;
	my $index;
	if (   ( $index = index( $user, '/' ) ) != -1
		|| ( $index = index( $user, '\\' ) ) != -1 )
	{
		return substr( $user, $index + 1 );
	}
	if ( ( $index = index( $user, '@' ) ) != -1 ) {
		return substr( $user, 0, $index );
	}
	return $user;
}

# Get IP from Fortigate
sub find_arp {
	my $mac = shift;
	my $out = qx{/usr/bin/ssh $ssh_arg "get system arp"};
	while ( $out =~ /($regex_ip)\s+\d+\s+($regex_mac)/g ) {
		my $qmac = extract_mac($2);
		return $1 if ( $mac eq $qmac );
	}
	return "";
}

# Get IP from Fortigate
sub find_dhcp {
	my $mac = shift;
	my $out = qx{/usr/bin/ssh $ssh_arg "execute dhcp lease-list"};
	while ( $out =~ /($regex_ip)\s+($regex_mac)/g ) {
		my $qmac = extract_mac($2);
		return $1 if ( $mac eq $qmac );
	}
	return "";
}

# function to find AD group from SAMBA
sub find_group($;$) {
	my ( $user, $prefix ) = @_;
	my ($sid) = split( /\s/, qx{/usr/local/bin/wbinfo --name-to-sid $user} )
	  or return ();
	$prefix = "" unless ($prefix);
	my @groups    = ();
	my @domgroups = qx{/usr/local/bin/wbinfo --user-domgroups $sid};
	for (@domgroups) {
		chomp;
		push @groups, $1
		  if qx{/usr/local/bin/wbinfo --sid-to-fullname $_} =~
		  /.*\\($prefix.*)\s\d+/;
	}
	return @groups;
}

#startwith
sub begins_with {
	#return !rindex( $_[0], $_[1], 0 );
	return substr( $_[0], 0, length( $_[1] ) ) eq $_[1];
}

# RADIUS function
our ( %RAD_REQUEST, %RAD_REPLY, %RAD_CHECK );

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
};

use constant {
	L_DBG   => 1,
	L_AUTH  => 2,
	L_INFO  => 3,
	L_ERR   => 4,
	L_PROXY => 5,
	L_ACCT  => 6
};

# Function to handle accounting
sub accounting {
	# For debugging purposes only
	#&log_request_attributes;

	if ( $RAD_REQUEST{'Calling-Station-Id'} ) {
		my $user = strip_username( $RAD_REQUEST{'User-Name'} );
		my $mac  = extract_mac $RAD_REQUEST{'Calling-Station-Id'};
		&radiusd::radlog( L_ACCT,
			"Accounting $RAD_REQUEST{'Acct-Status-Type'} for $user" );

		if ( $RAD_REQUEST{'Acct-Status-Type'} eq 'Interim-Update' ) {
			my $ip = $RAD_REQUEST{'Framed-IP-Address'} || find_arp($mac);
			unless ($ip) {
				return RLM_MODULE_NOOP;
			}

			my @group = find_group( $user, $ad_filter );

			if ( $RAD_REQUEST{'NAS-Port-Type'} eq 'Ethernet' ) {
				push( @group, 'Ethernet' );
			}
			elsif ( $RAD_REQUEST{'Fortinet-Client-IP-Address'} ) {

				# Fortinet-Vdom-Name = _internal
				# Fortinet-Client-IP-Address = 223.24.156.237
				push( @group, 'VPN' );
			}
			elsif ( begins_with( $RAD_REQUEST{'NAS-Port-Type'}, 'Wireless' ) ) {
				push( @group, 'Wireless' );
			}

			&radiusd::radlog( L_ACCT,
				"Interim for $user, IP: $ip, MAC: $mac, Class: "
				  . join( ', ', @group ) );
			send_rad_ip( $radius_host, $radius_secret, $user, $ip,
				'Interim-Update', @group );
		}
		elsif ( $RAD_REQUEST{'Acct-Status-Type'} eq 'Start' ) {
			my $ip    = $RAD_REQUEST{'Framed-IP-Address'};
			my @group = find_group( $user, $ad_filter );

			if ( $RAD_REQUEST{'NAS-Port-Type'} eq 'Ethernet' ) {
				push( @group, 'Ethernet' );
			}
			elsif ( $RAD_REQUEST{'Fortinet-Client-IP-Address'} ) {

				# Fortinet-Vdom-Name = _internal
				# Fortinet-Client-IP-Address = 223.24.156.237
				push( @group, 'VPN' );
			}
			elsif ( begins_with( $RAD_REQUEST{'NAS-Port-Type'}, 'Wireless' ) ) {
				push( @group, 'Wireless' );
			}
			if ($mac) {
				unless ($ip) {
					$ip = find_dhcp($mac);
				}
				unless ($ip) {
					sleep(3);
					$ip = find_arp($mac);
				}
			}

			unless ($ip) {
				&radiusd::radlog( L_ACCT,
					"Login for $user, IP: $ip, MAC: $mac, NOIP" );
				return RLM_MODULE_NOOP;
			}

			&radiusd::radlog( L_ACCT,
				"Login for $user, IP: $ip, MAC: $mac, Class: "
				  . join( ', ', @group ) );
			send_rad_ip( $radius_host, $radius_secret, $user, $ip, 'Start',
				@group );
		}
		elsif ( $RAD_REQUEST{'Acct-Status-Type'} eq 'Stop' ) {
			my $ip = $RAD_REQUEST{'Framed-IP-Address'} || find_arp($mac);

			&radiusd::radlog( L_ACCT, "Logout for $user, IP: $ip, MAC: $mac" );
			send_rad_ip( $radius_host, $radius_secret, $user, $ip, 'Stop' );
		}
	}
	return RLM_MODULE_OK;
}

sub authorize {
	my $user = $RAD_REQUEST{'User-Name'};
	my $mac  = $RAD_REQUEST{'Calling-Station-Id'};
	&radiusd::radlog( L_AUTH, "AUTH received for $user, MAC: $mac" );
	return RLM_MODULE_NOOP;
}

# Function to handle authenticate
sub authenticate {
	return RLM_MODULE_NOOP;
}

# Function to handle preacct
sub preacct {
	return RLM_MODULE_NOOP;
}

# Function to handle checksimul
sub checksimul {
	return RLM_MODULE_NOOP;
}

# Function to handle pre_proxy
sub pre_proxy {
	return RLM_MODULE_NOOP;
}

# Function to handle post_proxy
sub post_proxy {
	return RLM_MODULE_NOOP;
}

# Function to handle post_auth
sub post_auth {
	# For debugging purposes only
	#       &log_request_attributes;
	return RLM_MODULE_REJECT if ( $RAD_REQUEST{'User-Name'} =~ /^host\// );
	return RLM_MODULE_OK unless ( $RAD_REQUEST{'User-Name'} );

	my $user  = strip_username $RAD_REQUEST{'User-Name'};
	my @group = find_group( $user, $ad_filter );

	if ( $RAD_REQUEST{'NAS-Port-Type'} eq 'Ethernet' ) {
		push( @group, 'Ethernet' );
	}
	elsif ( $RAD_REQUEST{'Fortinet-Client-IP-Address'} ) {

		# Fortinet-Vdom-Name = _internal
		# Fortinet-Client-IP-Address = 223.24.156.237
		push( @group, 'VPN' );
	}
	elsif ( begins_with( $RAD_REQUEST{'NAS-Port-Type'}, 'Wireless' ) ) {
		push( @group, 'Wireless' );
	}

	$RAD_REPLY{'Class'} = \@group;
	my $vlan = find_vlan(@group);
	$RAD_REPLY{'Tunnel-Type'}             = 13;
	$RAD_REPLY{'Tunnel-Medium-Type'}      = 6;
	$RAD_REPLY{'Tunnel-Private-Group-Id'} = "$vlan";

	my $mac = extract_mac $RAD_REQUEST{'Calling-Station-Id'};
	&radiusd::radlog( L_AUTH,
		"AUTH for $user, MAC: $mac, VLAN: $vlan, Class: "
		  . join( ', ', @group ) );
	if ( $RAD_REQUEST{'NAS-Port-Type'} eq 'Ethernet' ) {

		# from switch, send to acct
		&radiusd::radlog( L_INFO,
			"PORT-TYPE for user $user is ethernet, send rad acct" );
		my $ip =
			 $RAD_REQUEST{'Framed-IP-Address'}
		  || find_dhcp($mac)
		  || find_arp($mac);

		if ($ip) {
			&radiusd::radlog( L_INFO,
				"Interim for $user, IP: $ip, MAC: $mac, Class: "
				  . join( ', ', @group ) );

			send_rad_ip( $radius_host, $radius_secret, $user, $ip,
				'Interim-Update', @group );
		}
	}
	return RLM_MODULE_UPDATED;
}

# Function to handle detach
sub detach {
	&radiusd::radlog( L_DBG, "rlm_perl::Detaching. Reloading. Done." );
}

sub accounting_start {
}

sub accounting_stop {
}

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{$_}" );
	}
}

#END

Extra

Local user
vi /usr/local/etc/raddb/users

username1 Cleartext-Password := "user-password1", MS-CHAP-Use-NTLM-Auth := 0
username2 Cleartext-Password := "user-password2", MS-CHAP-Use-NTLM-Auth := 0
    Class = "AD-Support"
username3 Cleartext-Password := "user-password3", MS-CHAP-Use-NTLM-Auth := 0