Force manually expired voucher to renew dhcp lease

  • Hello,

    I am looking for a way to kick a user who is using a voucher, but if i manually expire the voucher, the user can still continue browsing until dhcp releases the lease. Is there any way to make this possible without setting the dhcp lease to a low number of seconds?

  • Hi,

    Well. at first I was saying : why are you trying to force to disconnect a user when its voucher is expired ?
    Better said: you are forcing the expiration of a voucher by using Status: Captive portal: Expire Vouchers:"my cpzone". The user should be disconnected right away …. (edit: or is it "within 5 minutes ? - see below)

    I tried it myself.
    I created a voucher roll, with one voucher, "Ndw8S328MHv" - usage time : 5 minutes.
    Apr 10 13:11:30 logportalauth[11708]: Voucher: All 1 vouchers from Roll 1 marked unused

    I used that voucher:
    Apr 10 13:12:58 logportalauth[65029]: Zone: cpzone1 - Voucher login good for 5 min.: Ndw8S328MHv, 0c:77:1a:2b:55:35,

    One minute later, I used forced "Ndw8S328MHv" to an expiration:
    Apr 10 13:13:47 logportalauth[1047]: Zone: cpzone1 - Ndw8S328MHv (1/1) forced to expire
    Apr 10 13:13:47 logportalauth[1047]: Zone: cpzone1 - FORCLY TERMINATING VOUCHER Ndw8S328MHv SESSION: , ,

    I tested the voucher by using Status: Captive portal: Test Vouchers:"my cpzone" - and obtain a logic result:
    Apr 10 13:14:00 logportalauth[58247]: Zone: cpzone1 - Ndw8S328MHv (1/1) already used and expired

    But, my device ( 0c:77:1a:2b:55:35 - an iPhone) was still able to surf - I really had the impression that my iPhone was still connected to the net.

    Finally, the cron function "captiveportal_prune_old" which runs every 5 minutes, decided that it was time to "TIMOUT" my voucher session "Ndw8S328MHv".
    Apr 10 13:18:01 logportalauth[89098]: Zone: cpzone1 - TIMEOUT: Ndw8S328MHv, 0c:77:1a:2b:13:35,

    After this moment, and only after this moment, my iPhone was disconnected - a Portal popup showd up right away when I continued to 'surf'.

    Wat is missing ?
    I should have ran:

    ipfw -x 2 table 1 list


    ipfw -x 2 table 2 list

    to check if my IP (= was really thrown out at the moment i forced the voucher to be expired. I guess it wasn't.

    (The context of my captive portal is "2")

    I should have looked at Status: Captive portal to see if my phone was still listed as "connected".

    I know that captiveportal_disconnect($cpentry,null,13); is being called (from /etc/inc/ - line 282) when I force a voucher to expire, the "FORCLY ;) TERMINATING VOUCHER" message in the logs is a proof, but I had this impression that something went wrong: the voucher is expired, but the user isn't disconnected (yet).
    Real disconnection is taken place when the 5 minutes cron task "captiveportal_prune_old()" has been run.

    Or, I'm I missing something  :o

    Anyway, Monday morning I'll have a more close look at this.

    Btw: removing a DHCP lease won't do it.

    Can some oe confirm: create a voucher roll, with one voucher, let say 60 minutes - use the voucher - force the voucher to be expired. Test the connection on device: is it still listed in the firewall ? (see commands above) Is Status: Captive portal still mentioning the device as 'connected' ? Is the device still connected (visit some random sites with Google) ?

  • LAYER 8 Netgate

    Expiring a voucher doesn't kick a user off.  The pruning process sees the expired voucher and walks the table terminating sessions.  You should have been able to find the user in Status > Captive Portal or in the Services > Captive Portal, MAC (depending on whether you're using automatic MAC pass-through editions, etc) and manually kill the actual session (remove the table entry) there.

  • Thanks for the quick precision.


    Expiring a voucher doesn't kick a user off.

    Have a look at this:

    I just found this - at the bottom of the page:

    User is online after voucher expires (?)
        The session timeout must be enabled in order to allow the voucher session to expire and deactivate.

    My "idle timeout" is 60 minutes and "hard timeout" is 360 minutes.

    But looking at the code, captiveportal_prune_old(),  "hard timeout" ($timeout from $cpcfg['timeout']) and "idle timeout" ($idletimeout from $cpcfg['idletimeout']) aren't really used when handling vouchers - its the vouchers time length that is being taken in account.

    Voucher time is being handled here
    (Start session time + voucher duration time) should not be greater then (actual time)
    If so, timeout is being executed here:
    and the user is shut off using the all mighty captiveportal_disconnect($cpentry, $radiusservers,$term_cause,$stop_time); function.

    But, here: - where vouchers are forced to be expired, and then the same
    captiveportal_disconnect($cpentry,null,13); is being called from there (/etc/inc/

    ….... the Portal User should have been thrown off the portal at that moment. What else is calling captiveportal_disconnect(...) good for ?

    I'm ready to believe that

    Expiring a voucher does kick a user off.


    The code states : == "/* Check if this voucher has any active sessions */"
    I guess this test is being taken at that moment to …. disconnect the user.
    Right ?!

    As said above, I know the function captiveportal_disconnect($cpentry,null,13); is being called, because the portal log outputs just afterwards the typo "FORCLY" log line.

    Is this a a question of states in the firewall that remain existent ?

    edit: why, … why do I not have pfSEnse @ home ....  >:(

  • LAYER 8 Netgate

    Mine might behave differently because I use auto-added MAC pass-throughs pretty much exclusively when I use vouchers.  The CP code takes a lot of different paths based on options.

    Do some experimentation, do like you said and look at the ipfw context tables, and see how it behaves for your set of options.

  • Ok, found it.


    $cpentry = captiveportal_read_db("WHERE username = '{$voucher}'");

    returns an array within array.
    So, using it like this

    					captiveportal_logportalauth($cpentry[4],$cpentry[3],$cpentry[2],"FORCLY TERMINATING VOUCHER {$voucher} SESSION");

    will not disconnect a client.
    A very visible hint was always present:
    Look at the end of the logged line (as a result of captiveportal_logportalauth(…) showed above:

    logportalauth[35291]: Zone: cpzone1 - FORCLY TERMINATING VOUCHER 3eSUmPGMfwx SESSION: , ,

    All function parameters ($cpentry[4],$cpentry[3],$cpentry[2]) are empty or non-defined.



    So, use the first array from the array of arrays retrieved like this:

    $cpentry = $cpentry[0];

    There might be a better solution, but it seems the most logic way to me.

    Now, when forcing a voucher to expire, and a portal client is actually using that voucher at that moment, he WILL get disconnected right away.

    I received a log line like this:

    logportalauth[26366]: Zone: cpzone1 - FORCLY TERMINATING VOUCHER cqM6WyFhW8G SESSION: cqM6WyFhW8G, 0c:77:1a:xx:yy:35,

    Firewall tables 1 & 2 confirme that user "0c:77:1a:xx:yy:35," was removed.

    Feedback is welcome, I'm not an active 'voucher' user.

  • I found out that when you manually kick a user on the status_captiveportal.php?zone= page, that the kick is instant.

  • @Just_Michel:

    I found out that when you manually kick a user on the status_captiveportal.php?zone= page, that the kick is instant.

    That is one case were captiveportal_disconnect(….) is called the correct way, so the user will be disconnected.

    The voucher expire function in /etc/inc/ wasn't calling captiveportal_disconnect(….) correctly.

    Apply the pach (just one line) and check yourself.

  • I guess its time for a pull request.

    …. and I closed it.


    $cpentry = captiveportal_read_db("WHERE username = '{$voucher}'");
    **    if (!empty($cpentry) && !empty($cpentry[0]) ) {
          $cpentry = $cpentry[0];**

    and … because it works now, I've got something new: (see image) which originates from /etc/inc/ - lines 890 - 898.

    	if (is_ipaddr($dbent[2])) {
    		/* Delete client's ip entry from tables 1 and 2\. */
    		$clientsn = (is_ipaddrv6($dbent[2])) ? 128 : 32;
    		pfSense_ipfw_Tableaction($cpzoneid, IP_FW_TABLE_XDEL, 1, $dbent[2], $clientsn, $dbent[3]);
    		pfSense_ipfw_Tableaction($cpzoneid, IP_FW_TABLE_XDEL, 2, $dbent[2], $clientsn, $dbent[3]);
    		/* XXX: Redundant?! Ensure all pf(4) states are killed. */
    		$_gb = @pfSense_kill_states($dbent[2]);
    		$_gb = @pfSense_kill_srcstates($dbent[2]);

    $dbent[2] (client IP like and $dbent[3] (client MAC like  0c:77:1a:xx:13:35) are correctly defined.

    pfSense_ipfw_Tableaction() isn't happy about what ??
    (must be him, the output of @pfSense_kill_states() is already suppressed).

    New bug : the global $cpzoneid in captiveportal_disconnect() when called from /etc/inc/ ISN'T defined  !!
    …. back to the drawing table  >:(

    edit: got it.
    $cpzoneid (a global variable) isn't set.
    This makes bark the code quoted above (the 2 functions pfSense_ipfw_Tableaction()) - logic, because the used $cpzoneid was '0'.

    new test:

    				if (!empty($cpentry) && !empty($cpentry[0]) ) {
    					$cpentry = $cpentry[0];
    					// surface global variable $cpzoneid needed by captiveportal_disconnect()
    					$cpzoneid = $config['captiveportal'][$cpzone]['zoneid'];

    I hit another test cycle.