OpenWrt Forum Archive

Topic: Time-based firewall rules

The content of this topic has been archived on 23 Apr 2018. There are no obvious gaps in this topic, but there may still be some posts missing at the end.

I want to allow connections from lan devices to the Internet only during scheduled time sessions.  My starting point is the code from this project: https://forum.openwrt.org/viewtopic.php?id=60801

Here is an example of the firewall config addition:

config rule
        option src 'lan'
        option dest 'wan'
        option extra '--kerneltz'
        option proto '0'
        option target 'ACCEPT'
        option src_mac 'xx:xx:xx:xx:xx:xx'
        option enabled '1'
        option start_time '15:35'
        option stop_time '17:00'

The problem is that the rule works fine for granting access once you reach the start_time, but it fails to stop any connections that are already established when you reach the stop_time.  Here are two section of the iptables output which explain why:

Chain delegate_forward (1 references)
target     prot opt source               destination         
forwarding_rule  all  --  anywhere             anywhere             /* user chain for forwarding */
ACCEPT     all  --  anywhere             anywhere             ctstate RELATED,ESTABLISHED
zone_lan_forward  all  --  anywhere             anywhere            
zone_wan_forward  all  --  anywhere             anywhere            
zone_wan_forward  all  --  anywhere             anywhere            
reject     all  --  anywhere             anywhere

The packets from already established connections hit that second rule and never make it to my scheduling rule which is part of the zone_lan_forward section as per below:

Chain zone_lan_forward (1 references)
target     prot opt source               destination         
forwarding_lan_rule  all  --  anywhere             anywhere             /* user chain for forwarding */
zone_wan_dest_REJECT  all  --  anywhere             anywhere             MAC xx:xx:xx:xx:xx:xx TIME from 17:51:00 to 18:00:00 /* @rule[9] */
zone_wan_dest_ACCEPT  all  --  anywhere             anywhere             /* forwarding lan -> wan */
ACCEPT     all  --  anywhere             anywhere             ctstate DNAT /* Accept port forwards */
zone_lan_dest_ACCEPT  all  --  anywhere             anywhere      

My questions are:
Is there any way I can force my new schedule rule to appear BEFORE the rule for established connections in delegate_forward? 
How does this "user chain for forwarding" work? 

It would be much nicer to specify my rule with the timing when traffic is allowed (ACCEPT) instead of when it's blocked (REJECT), but I've given up on that because it doesn't seem possible.

If possible, I would like to implement these rule within the firewall config file, not using raw iptables commands, to enjoy the LuCI CBI functionality.

(Last edited by sleepyhead on 23 Jun 2016, 15:13)

Not an answer to your question, and I don't know if it will work either: would it be possible to switch off the lan for a minute (and thereby breaking all connections)?

tunk wrote:

Not an answer to your question, and I don't know if it will work either: would it be possible to switch off the lan for a minute (and thereby breaking all connections)?

Sure, there's more than one way to skin a cat.  I could also "reset" the connections by flushing the conntrack table.  But it would be a shame to do it that way, since iptables lets you specify timezone-corrected rules.  It's so close to working perfectly without any kludges (daemons or cron jobs or the like...)

I am not clear I am following exactly where you have your rules configured, but if they are in the firewall config file, then you can change the order in the Luci GUI or edit the file manually.  In Luci there are up\dn arrows on the right side of the window.

RangerZ wrote:

I am not clear I am following exactly where you have your rules configured, but if they are in the firewall config file, then you can change the order in the Luci GUI or edit the file manually.  In Luci there are up\dn arrows on the right side of the window.

Thanks, but unfortunately, the rule which allows traffic for established connections is something that gets created automatically (and is not part of the firewall config file).  No matter where I place my time-scheduling rule in the config file, that automatic rule always comes first in the iptables chain.

Here is one approach that almost works.  Instead of trying to place my time-scheduling rules before the one for established connections, I add iptables commands in /etc/firewall.user to move that established connection rule to later in the chain:

/etc/firewall.user

# Delete rule for forwarding established connection traffic
old_rule_num=$(iptables -v -L delegate_forward --line-numbers | grep ESTABLISHED | cut -c1)
iptables -D delegate_forward $old_rule_num

# Insert rule for forwarding established connection traffic, just before the final rule (reject)
new_rule_num=$(iptables -v -L delegate_forward --line-numbers | grep reject | cut -c1)
iptables -I delegate_forward $new_rule_num -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

This has the following effect: it changes the iptables rule order from:

Chain delegate_forward (1 references)
num  target     prot opt source               destination         
1    forwarding_rule  all  --  anywhere             anywhere             /* user chain for forwarding */
2    ACCEPT     all  --  anywhere             anywhere             ctstate RELATED,ESTABLISHED
3    zone_lan_forward  all  --  anywhere             anywhere            
4    zone_wan_forward  all  --  anywhere             anywhere            
5    zone_wan_forward  all  --  anywhere             anywhere            
6    reject     all  --  anywhere             anywhere

to:

Chain delegate_forward (1 references)
num  target     prot opt source               destination         
1    forwarding_rule  all  --  anywhere             anywhere             /* user chain for forwarding */
2    zone_lan_forward  all  --  anywhere             anywhere            
3    zone_wan_forward  all  --  anywhere             anywhere            
4    zone_wan_forward  all  --  anywhere             anywhere            
5    ACCEPT     all  --  anywhere             anywhere             ctstate RELATED,ESTABLISHED
6    reject     all  --  anywhere             anywhere 

The only problem is that this firewall.user script only gets executed during a firewall restart, not during a reload.  And LuCI's Save&Apply button causes a reload (which undoes any changes to the rule order). 

I am tempted to patch LuCI so that the Apply function causes a restart instead of a reload...

@LuCI .. this can be accomplished with a few lines of code ...

m.on_after_commit = function()
  sys.exec("/etc/fw_restart.sh &")
end

are you using k-szuster's luci-access-control ?? if so this is how you would implement such a maneuver ..


--[[
LuCI - Lua Configuration Interface - Internet access control

Copyright 2015 Krzysztof Szuster.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    [url]http://www.apache.org/licenses/LICENSE-2.0[/url]

$Id$
]]--
local sys = require "luci.sys"
local CONFIG_FILE_RULES = "firewall"  
local CONFIG_FILE_AC    = "access_control"
local ma, mr, s, o

ma = Map(CONFIG_FILE_AC, translate("Internet Access Control"),
    translate("Access Control allows you to manage internet access for specific local hosts.<br/>\
       Each rule defines which user has blocked access to the internet. The rules may be active permanently or in certain time of day.<br/>\
       The rules may also be restricted to specific days of the week."))
if CONFIG_FILE_AC==CONFIG_FILE_RULES then
    mr = ma
else
    mr = Map(CONFIG_FILE_RULES)
end

ma.on_after_commit = function()
  sys.exec("/etc/fw_restart.sh &")
end
---------------------------------------------------------------------------------------------
--  General switch

s = ma:section(NamedSection, "general", "access_control", "General switch")
    o_global_enable = s:option(Flag, "enabled", translate("Enabled"))
        o_global_enable.rmempty = false
        
---------------------------------------------------------------------------------------------
-- Rule table

s = mr:section(TypedSection, "rule", translate("Client Rules"))
    s.addremove = true
    s.anonymous = true
--    s.sortable  = true
    s.template = "cbi/tblsection"
    -- hidden, constant options
    s.defaults.enabled = "0"
    s.defaults.src     = "*" --"lan", "guest" or enything on local side
    s.defaults.dest    = "wan"
    s.defaults.target  = "REJECT"
    s.defaults.proto    = "0"
    s.defaults.extra = "--kerneltz"
    
    -- only AC-related rules
    s.filter = function(self, section)
          return self.map:get (section, "ac_enabled") ~= nil
    end
        
    o = s:option(Flag, "ac_enabled", translate("Enabled"))
        o.default = '1'
        o.rmempty  = false
    
        -- ammend "enabled" option and set weekdays  
        function o.write(self, section, value)
            wd_write (self, section, value)
            local key = o_global_enable:cbid (o_global_enable.section.section)
            --  "cbid.access_control.general.enabled"
            local global_enable = o_global_enable.map:formvalue (key)
            if global_enable == "1" then
                self.map:set(section, "enabled", value)
            else
                self.map:set(section, "enabled", "0")
            end    
--            self.map:set(section, "src",  "*")
--            self.map:set(section, "dest", "wan")
--            self.map:set(section, "target", "REJECT")
--            self.map:set(section, "proto", "0")
--            self.map:set(section, "extra", "--kerneltz")
            return Flag.write(self, section, value)
        end
      
    o = s:option(Value, "name", "Description")
--        o.rmempty = false  -- force validate
--        -- better validate, then: o.datatype = "minlength(1)"
--        o.validate = function(self, val, sid)
--            if type(val) ~= "string" or #val == 0 then
--                return nil, translate("Name must be specified!")
--            end
--            return val
--        end
        
     o = s:option(Value, "src_mac", "MAC address") 
        o.rmempty = false
        o.datatype = "macaddr"
        luci.sys.net.mac_hints(function(mac, name)
            o:value(mac, "%s (%s)" %{ mac, name })
        end)

    function validate_time(self, value, section)
        local hh, mm
        hh,mm = string.match (value, "^(%d?%d):(%d%d)$")
        hh = tonumber (hh)
        mm = tonumber (mm)
        if hh and mm and hh <= 23 and mm <= 59 then
            return value
        else
            return nil, "Time value must be HH:MM or empty"
        end
    end
    o = s:option(Value, "start_time", "Start time")
        o.rmempty = true  -- do not validae blank
        o.validate = validate_time 
        o.size = 5
    o = s:option(Value, "stop_time", "End time") 
        o.rmempty = true  -- do not validae blank
        o.validate = validate_time
        o.size = 5

    local Days = {'mon','tue','wed','thu','fri','sat','sun'}
    local Days1 = translate('MTWTFSS')
    
    function make_day (nday)
        local day = Days[nday]
        local label = Days1:sub (nday,nday)
        local o = s:option(Flag, day, label)
        o.default = '1'
        o.rmempty = false  --  always call write
        
        -- read from weekdays actually
        function o.cfgvalue(self, s)
            local days = self.map:get (s, "weekdays")
            if days==nil then
                return '1'
            end
            return string.find (days, day) and '1' or '0'
        end
     
        --  prevent saveing option in config file   
        function o.write(self, section, value)
            self.map:set(section, self.option, '')
        end
    end
  
    for i=1,7 do   
        make_day (i)
    end   
    
    function wd_write(self, section, value)
        value=''
        local cnt=0
        for _,day in ipairs (Days) do
            local key = "cbid."..self.map.config.."."..section.."."..day
--io.stderr:write (tostring(key)..'='..tostring(mr:formvalue(key))..'\n')
            if mr:formvalue(key) then
                value = value..' '..day
                cnt = cnt+1
            end
        end
        if cnt==7  then  --all days means no filterung 
            value = ''
        end
        self.map:set(section, "weekdays", value)
    end


if CONFIG_FILE_AC==CONFIG_FILE_RULES then
  return ma
else
  return ma, mr
end

/etc/fw_restart.sh

#!/bin/sh

/etc/init.d/firewall restart

This will fire the above "fw_restart.sh"  script every time you "save and apply " in turn the firewall is restarted. Although this is kind of a hack, it would work to suite your needs

(Last edited by hostle19 on 26 Jun 2016, 14:12)

Thank you hostle19, I didn't know that this on_after_commit hook existed.

Unfortunately, that would not solve my immediate problem because briefly after the commit, the LuCI web page makes an AJAX call to the systemctl URI to reload the firewall - this reload will restore the internal rules to the way they were and undo the iptables commands in firewall.user.

What seems to be working for me now is to modify the firewall init script so that a reload actually causes a restart:
/etc/config/firewall

reload_service() {
#       fw3 reload
        fw3 restart
}

And crucially, the firewall reload that occurs when wan interface comes up must be changed to a restart:
/etc/hotplug.d/iface/20-firewall

#fw3 -q reload
fw3 -q restart

I chose to use a cron job to change the position of the iptables rule. Everything works like a charm. This is very helpful in limiting youtube time for my son on weekdays. My gateway uses OpenWrt Chaos Calmer 15.05 / LuCI (git-15.248.30277-3836b45) and i have installed luci-app-access-control_0.3.1_all.ipk This package should be made part of standard Openwrt package list

Before cron job run
--------------------------

root@OpenWrt-gateway:~# iptables -nL  delegate_forward --line-numbers
Chain delegate_forward (1 references)
num  target     prot opt source               destination
1    forwarding_rule  all  --  0.0.0.0/0            0.0.0.0/0            /* user chain for forwarding */
2    ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
3    zone_wan_dest_REJECT  all  --  0.0.0.0/0            0.0.0.0/0            MAC xx:xx:xx:xx:xx:xx TIME from 19:30:00 to 18:30:00 on Mon,Tue,Wed,Thu,Fri /* Sony TV */
4    zone_lan_forward  all  --  0.0.0.0/0            0.0.0.0/0
5    zone_wan_forward  all  --  0.0.0.0/0            0.0.0.0/0
6    zone_guest_forward  all  --  0.0.0.0/0            0.0.0.0/0
7    reject     all  --  0.0.0.0/0            0.0.0.0/0

After cron job run
------------------------

root@OpenWrt-gateway:~# iptables -nL  delegate_forward --line-numbers
Chain delegate_forward (1 references)
num  target     prot opt source               destination
1    forwarding_rule  all  --  0.0.0.0/0            0.0.0.0/0            /* user chain for forwarding */
2    zone_wan_dest_REJECT  all  --  0.0.0.0/0            0.0.0.0/0            MAC xx:xx:xx:xx:xx:xx TIME from 19:30:00 to 18:30:00 on Mon,Tue,Wed,Thu,Fri /* Sony TV */
3    zone_lan_forward  all  --  0.0.0.0/0            0.0.0.0/0
4    zone_wan_forward  all  --  0.0.0.0/0            0.0.0.0/0
5    zone_guest_forward  all  --  0.0.0.0/0            0.0.0.0/0
6    ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            ctstate RELATED,ESTABLISHED
7    reject     all  --  0.0.0.0/0            0.0.0.0/0

Script run by cron job (Slight modification to script posted by sleepyhead,
the script adds the new rule first and then deletes the unneeded rule. This
prevents TCP connection issues)
----------------------------------------------------------------------------------------------

#!/bin/sh
# Insert rule for forwarding established connection traffic, just before the final rule (reject)
new_rule_num=$(iptables -v -L delegate_forward --line-numbers | grep reject | cut -c1)
iptables -I delegate_forward $new_rule_num -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# Delete first rule for forwarding established connection traffic
old_rule_num=$(iptables -v -L delegate_forward --line-numbers | grep ESTABLISHED | cut -c1 | sed -n 1p)
iptables -D delegate_forward $old_rule_num

Cron job to run script every 1 mins
------------------------------------------------

root@OpenWrt-gateway:~# crontab -l
*/1* * * * /etc/cronfw.sh

(Last edited by Srini on 3 Oct 2016, 02:52)

I can't even get k.szuster1's .ipk to install at all!! Any ideas on this?

How are you trying to install it? I just used wget and then opkg

opkg install ./luci-app-access-control_0.3.1_all.ipk

(Last edited by Srini on 29 Aug 2016, 02:48)

Srini wrote:

How are you trying to install it? I just used wget and then opkg

opkg install ./luci-app-access-control_0.3.1_all.ipk

I used WinSCP to upload the ipk file, and then installed using the command above!

My doubt is respective to the method sleepyhead did in the post #8 but i want to confirm he kept the iptable commands, because in my terminal did not appear the rules inverted.

@Srini, thanks buddy!

I didn't work for me, my son was telling me that he still could keep using the already open connections.
Just now, I suddenly realized that cronfw.sh needs the right permissions!
After "chmod 755 cronfw.sh" the script will execute correctly, now let's see what my son will say during Monday-night... smile

I can see that you have found out the root cause of script not executing but I would like to share my idea which I have been using for quite some time now. Maybe it helps someone in future.

I have made a cronjob which monitors the dhcp.leases file and check for mac addresses which are not known to the router ( I have made file called private which has mac addresses of my own pcs and phones ). Other than these mac addresses all are treated as guest wifi connections for the router.

If a guest mac address logs in to the router script picks up the mac address and generates a cron job on the fly to block the internet after 1 hour. How it does is by Inserting a rule in FORWARD chain for rejecting all packets from the source mac address which is the guest mac.

After 2 hours I unblock the internet of guest mac by deleting the rule which is also handled by the script.

I've been thinking, wouldn't this work as well, or better?

config rule
        option src 'wan'
        option dest 'lan'
        option extra '--kerneltz'
        option proto '0'
        option target 'DROP'
        option dest_mac 'xx:xx:xx:xx:xx:xx' (does the parameter "dest_mac" exist?)
        option enabled '1'
        option start_time '15:35'
        option stop_time '17:00'

I'm still having problems with the present setup and the cronfw.sh-"fix": existing connections (like from a gaming-application) are not blocked.

I will try to do some experiments during the weekend, if I can find the time...

(Last edited by bouwew on 21 Nov 2016, 13:16)

sleepyhead wrote:

The only problem is that this firewall.user script only gets executed during a firewall restart, not during a reload.  And LuCI's Save&Apply button causes a reload (which undoes any changes to the rule order).

Its not a problem. It can be easily corrected. No need to hack firewall init scripts.

config include
 option path '/etc/firewall.user'
 option reload '1'

Or automated version :

# enable execute /etc/firewall.user on every firewall reload
set_firewall_user_reload() {
        i=0
        while true
        do
         path=$(uci -q get firewall.@include[$i].path)
         [ -n "$path" ] || break
         [ "$path" == "/etc/firewall.user" ] && {
           reload=$(uci -q get firewall.@include[$i].reload)
           [ "$reload" = "1" ] || {
             echo Setting 'reload' call option to /etc/firewall.user
             uci set firewall.@include[$i].reload=1
             uci commit firewall
           }
         }
         i=$((i+1))
        done
}

(Last edited by bolvan on 21 Nov 2016, 21:20)

bolvan wrote:

Or automated version :

# enable execute /etc/firewall.user on every firewall reload
set_firewall_user_reload() {
        i=0
        while true
        do
         path=$(uci -q get firewall.@include[$i].path)
         [ -n "$path" ] || break
         [ "$path" == "/etc/firewall.user" ] && {
           reload=$(uci -q get firewall.@include[$i].reload)
           [ "$reload" = "1" ] || {
             echo Setting 'reload' call option to /etc/firewall.user
             uci set firewall.@include[$i].reload=1
             uci commit firewall
           }
         }
         i=$((i+1))
        done
}

Sorry for this noob question but where this automated version should be written? I mean which file?

sleepyhead wrote:
# Delete rule for forwarding established connection traffic
old_rule_num=$(iptables -v -L delegate_forward --line-numbers | grep ESTABLISHED | cut -c1)
(...)
new_rule_num=$(iptables -v -L delegate_forward --line-numbers | grep reject | cut -c1)
iptables -I delegate_forward $new_rule_num -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

This works fine as long as your firewall rules have single digit numbers.
I had to change the two "cut" commands to "cut -d ' ' -f 2" to get the first number in the line, not only the first character!

So now it should read:

# Delete rule for forwarding established connection traffic
old_rule_num=$(iptables -v -L delegate_forward --line-numbers | grep ESTABLISHED | cut -d ' ' -f 2)
iptables -D delegate_forward $old_rule_num
# Insert rule for forwarding established connection traffic, just before the final rule (reject)
new_rule_num=$(iptables -v -L delegate_forward --line-numbers | grep reject | cut -d ' ' -f 2)
iptables -I delegate_forward $new_rule_num -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

In combination with the suggestion below by bolvan, this solves the problem quite neatly!

Cheers!

Hello,

So, if we want to use restrictions access, we must :
- install Internet Access Control
- create our rules
- create a cronfw.sh (for example) containing :

#!/bin/sh
# Insert rule for forwarding established connection traffic, just before the final rule (reject)
new_rule_num=$(iptables -v -L delegate_forward --line-numbers | grep reject | cut -c1)
iptables -I delegate_forward $new_rule_num -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT

# Delete first rule for forwarding established connection traffic
old_rule_num=$(iptables -v -L delegate_forward --line-numbers | grep ESTABLISHED | cut -c1 | sed -n 1p)
iptables -D delegate_forward $old_rule_num

and add this job to the crontab :

*/1* * * * /etc/cronfw.sh

A good resume for a noob ?

It's been a while since the original post but since the question seems still to be of interest I though I would tell how I had set up time-based rules for my LAN.  Basically, I found it easier to learn about "iptables" than to write various scripts, and LuCI has a nice place to put the iptables commands.

I am using ChaosCalmer 15.05.1 on an ASUS RT-N16.  In my terminal I enter "ssh root@192.168.1.1" and enter the password to log in to OpenWrt.   If I enter "iptables -L -v" I can see the chains of rules.  I am interested in the forwarding rules.  The starting point is chain "FORWARD", which calls chain "delegate_forward", which calls chain "forwarding_rule".  There is a note in chain "delegate_forward" saying that chain "forwarding_rule" is to be the "user chain for forwarding".  On my system I have not done anything else with the firewall, so the rules in "forwarding_rule" will be the very first real tests that  are looked at when a packet comes through. The tests for established connections will come later.

Now I use the browser to go to LuCI:Network:Firewall:Custom Rules.  Here is the place to enter iptables commands to make rules for chain "forwarding_rule".  LuCI says these rules get executed every time the firewall is started or restarted.   The first command is to flush all the rules in chain "forwarding_rule" so that it starts empty.  Next are commands to exempt packets to or from specific ip adresses so that I can continue to use the LAN after my children go to bed.  (These packets return to chain "delegate_forward" and then go on through the rest of the rules.)  Last is the rule to check the time and reject if the kids need to be sleeping.

Here are my iptables commands:

# Flush the rules in forwarding_rule, which is the user chain
iptables -F forwarding_rule

# Insert at beginning of chain any rules for packets not to reject
iptables -I forwarding_rule 1 -j RETURN -d 0.0.0.0/0 -s 192.168.1.xx
iptables -I forwarding_rule 1 -j RETURN -d 192.168.1.xx -s 0.0.0.0/0
iptables -I forwarding_rule 1 -j RETURN -d 192.168.1.yyy -s 0.0.0.0/0
iptables -I forwarding_rule 1 -j RETURN -d 0.0.0.0/0 -s 192.168.1.yyy
iptables -I forwarding_rule 1 -j RETURN -d 192.168.1.zzz -s 0.0.0.0/0
iptables -I forwarding_rule 1 -j RETURN -d 0.0.0.0/0 -s 192.168.1.zzz

# Append last rule, which is to reject from 8:30p to 7:00a CST
# which is 02:30 to 13:00 UTC
# or same time CDT which is 01:30 to 12:00 UTC
iptables -A forwarding_rule -j REJECT -m time --timestart 01:30 --timestop 12:00

After entering these lines in the window, click on the "Submit" button.  Then go to "General settings" and click on "Save & Apply".

So far, this has worked well for my family and has been easy to edit when I have needed to add another exempt ip address or change the time.  I haven't needed to do it, but it should be easy to write an iptables command to exempt or reject packets to/from a specific ip address during specific times, such as for two children with different bed times.  I am not sure what will happen if one of the children sets up his/her computer to use the ip address that is normally mine!  It might be worth experimenting with MAC addresses (but someone probably could tell us whether MAC addresses would be reliable in this situation.)

Based on your suggestion, I've tested the following and it seems to be working well:

iptables -A forwarding_rule -j REJECT -m mac --mac-source XX:XX:XX:XX:XX:XX -m time --timestart 18:00 --timestop 02:00

That seems to be the only line needed in the Custom Rules (/etc/firewall.user). Simple and effective.
Note that the time for me was UTC and not my local time.

The discussion might have continued from here.