applications/luci-splash:
authorJo-Philipp Wich <jow@openwrt.org>
Mon, 6 Jul 2009 21:14:59 +0000 (21:14 +0000)
committerJo-Philipp Wich <jow@openwrt.org>
Mon, 6 Jul 2009 21:14:59 +0000 (21:14 +0000)
- rewrote init script, cli
- introduce download traffic counters
- adept user interface

applications/luci-splash/luasrc/controller/splash/splash.lua
applications/luci-splash/luasrc/view/admin_status/splash.htm
applications/luci-splash/root/etc/init.d/luci_splash
applications/luci-splash/root/usr/sbin/luci-splash

index 88aafcdf5256f208209259b05a400658d213a2d2..13f9f4121ae5bac95af4486dd0284ca3d7fab16e 100644 (file)
@@ -24,7 +24,7 @@ function action_activate()
        local ip = luci.http.getenv("REMOTE_ADDR") or "127.0.0.1"
        local mac = luci.sys.net.ip4mac(ip:match("^[\[::ffff:]*(%d+.%d+%.%d+%.%d+)\]*$"))
        if mac and luci.http.formvalue("accept") then
-               os.execute("luci-splash add "..mac.." >/dev/null 2>&1")
+               os.execute("luci-splash lease "..mac.." >/dev/null 2>&1")
                luci.http.redirect(luci.model.uci.cursor():get("freifunk", "community", "homepage"))
        else
                luci.http.redirect(luci.dispatcher.build_url())
@@ -35,63 +35,44 @@ function action_status_admin()
        local uci = luci.model.uci.cursor_state()
        local macs = luci.http.formvaluetable("save")
 
-       local function delete_mac(what, mac)
-               uci:delete_all("luci_splash", what,
-                       function(s)
-                               return ( s.mac and s.mac:lower() == mac )
-                       end)
-       end
-
-       local function leases(mac)
-               local leases = { }
-
-               uci:foreach("luci_splash", "lease", function(s)
-                       if s.start and s.mac and s.mac:lower() ~= mac then
-                               leases[#leases+1] = {
-                                       start = s.start,
-                                       mac   = s.mac
-                               }
-                       end
-               end)
-
-               uci:revert("luci_splash")
-
-               return leases
-       end
-
-       local function commit(leases, no_commit)
-               if not no_commit then
-                       uci:save("luci_splash")
-                       uci:commit("luci_splash")
-               end
-
-               for _, l in ipairs(leases) do
-                       uci:section("luci_splash", "lease", nil, l)
-               end
-
-               uci:save("luci_splash")
-               os.execute("/etc/init.d/luci_splash restart")
-       end
+       local changes = { 
+               whitelist = { },
+               blacklist = { },
+               lease     = { },
+               remove    = { }
+       }
 
        for key, _ in pairs(macs) do
                local policy = luci.http.formvalue("policy.%s" % key)
                local mac    = luci.http.protocol.urldecode(key)
-               local lslist = leases(policy ~= "kick" and mac)
-
-               delete_mac("blacklist", mac)
-               delete_mac("whitelist", mac)
 
                if policy == "whitelist" or policy == "blacklist" then
-                       uci:section("luci_splash", policy, nil, { mac = mac })
-               elseif policy == "normal" then                  
-                       lslist[#lslist+1] = { mac = mac, start = os.time() }
-               elseif policy == "kick" then
-                       for _, l in ipairs(lslist) do
-                               if l.mac:lower() == mac then l.kicked="1" end
-                       end
+                       changes[policy][#changes[policy]+1] = mac
+               elseif policy == "normal" then
+                       changes["lease"][#changes["lease"]+1] = mac
+               elseif policy == "kicked" then
+                       changes["remove"][#changes["remove"]+1] = mac
                end
+       end
+
+       if #changes.whitelist > 0 then
+               os.execute("luci-splash whitelist %s >/dev/null"
+                       % table.concat(changes.whitelist))
+       end
+
+       if #changes.blacklist > 0 then
+               os.execute("luci-splash blacklist %s >/dev/null"
+                       % table.concat(changes.blacklist))
+       end
+
+       if #changes.lease > 0 then
+               os.execute("luci-splash lease %s >/dev/null"
+                       % table.concat(changes.lease))
+       end
 
-               commit(lslist)
+       if #changes.remove > 0 then
+               os.execute("luci-splash remove %s >/dev/null"
+                       % table.concat(changes.remove))
        end
 
        luci.template.render("admin_status/splash", { is_admin = true })
index 4690e3348683cc76527cb26236f99d8ad30b53a4..99395200d3b6d5343da1a36b7fb42cf45174295a 100644 (file)
@@ -35,10 +35,10 @@ uci:foreach("luci_splash", "lease",
                                start   = tonumber(s.start),
                                limit   = ( tonumber(s.start) + leasetime ),
                                mac     = s.mac:upper(),
+                               ipaddr  = s.ipaddr,
                                policy  = "normal",
                                packets = 0,
                                bytes   = 0,
-                               kicked  = s.kicked and true or false
                        }
                end
        end)
@@ -58,11 +58,25 @@ for _, r in ipairs(ipt:find({table="nat", chain="luci_splash_leases"})) do
        end
 end
 
-for _, r in ipairs(ipt:find({table="filter", chain="luci_splash_filter", options={"MAC"}})) do
-       local c = clients[r.options[2]:lower()]
-       if c and c.packets == 0 then
-               c.bytes   = tonumber(r.bytes)
-               c.packets = tonumber(r.packets)
+for mac, client in pairs(clients) do
+       client.bytes_in    = 0
+       client.bytes_out   = 0
+       client.packets_in  = 0
+       client.packets_out = 0
+
+       if client.ipaddr then
+               local rin  = ipt:find({table="mangle", chain="luci_splash_mark_in", destination=client.ipaddr})
+               local rout = ipt:find({table="mangle", chain="luci_splash_mark_out", options={"MAC", client.mac:upper()}})
+
+               if rin and #rin > 0 then
+                       client.bytes_in   = rin[1].bytes
+                       client.packets_in = rin[1].packets
+               end
+
+               if rout and #rout > 0 then
+                       client.bytes_out   = rout[1].bytes
+                       client.packets_out = rout[1].packets
+               end
        end
 end
 
@@ -123,7 +137,7 @@ end
                                        <th class="cbi-section-table-cell"><%:ff_splash_ip IP Address%></th>
                                        <th class="cbi-section-table-cell"><%:ff_splash_mac MAC Address%></th>
                                        <th class="cbi-section-table-cell"><%:ff_splash_timeleft Time remaining%></th>
-                                       <th class="cbi-section-table-cell"><%:ff_splash_traffic Outgoing traffic%></th>
+                                       <th class="cbi-section-table-cell"><%:ff_splash_traffic Traffic in/out%></th>
                                        <th class="cbi-section-table-cell"><%:ff_splash_policy Policy%></th>
                                </tr>
 
@@ -149,7 +163,7 @@ end
                                                        (c.limit >= os.time()) and wat.date_format(c.limit-os.time()) or
                                                                (c.policy ~= "normal") and "-" or "<em>" .. translate("ff_splash_expired", "expired") .. "</em>"
                                                %></td>
-                                               <td class="cbi-section-table-cell"><%=wat.byte_format(c.bytes)%></td>
+                                               <td class="cbi-section-table-cell"><%=wat.byte_format(c.bytes_in)%> / <%=wat.byte_format(c.bytes_out)%></td>
                                                <td class="cbi-section-table-cell">
                                                        <% if is_admin then %>
                                                        <select name="policy.<%=c.mac:lower()%>" style="width:200px">
@@ -157,7 +171,7 @@ end
                                                                <option value="normal"<%=c.policy=="normal" and not c.kicked and ' selected="selected"'%>><%:ff_splash_splashed splashed%></option>
                                                                <option value="blacklist"<%=c.policy=="blacklist" and ' selected="selected"'%>><%:ff_splash_blacklisted blacklisted%></option>
                                                                <% if c.policy == "normal" then -%>
-                                                               <option value="kick"<%=c.kicked and ' selected="selected"'%>><%:ff_splash_tempblock temporarily blocked%> (<%=wat.date_format(c.limit-os.time())%>)</option>
+                                                                       <option value="kicked"><%:ff_splash_tempblock temporarily blocked%></option>
                                                                <%- end %>
                                                        </select>
                                                        <input type="submit" class="cbi-button cbi-button-save" name="save.<%=c.mac:lower()%>" value="<%:save Save%>" />
index dd688d77c353a14d1249b42cd50a9cd492f51ded..08c3939b140a996a7fb8bebee953c3632d871589 100755 (executable)
@@ -1,15 +1,29 @@
 #!/bin/sh /etc/rc.common
+
 START=70
 EXTRA_COMMANDS=clear_leases
-SPLASH_INTERFACES=""
 LIMIT_DOWN=0
 LIMIT_DOWN_BURST=0
 LIMIT_UP=0
 
+IPT_REPLAY=/var/run/luci_splash.iptlog
+LOCK=/var/run/luci_splash.lock
+
+include /lib/network
+scan_interfaces
+config_load luci_splash
+
+set -x
+
 silent() {
        "$@" 2>/dev/null
 }
 
+ipt_log() {
+       iptables -I "$@"
+       echo iptables -D "$@" >> $IPT_REPLAY
+}
+
 iface_add() {
        local cfg="$1"
        
@@ -37,95 +51,45 @@ iface_add() {
        
        eval "$(ipcalc.sh $ipaddr $netmask)"
 
-       iptables -t nat -A prerouting_${zone} -j luci_splash_prerouting
-       iptables -t nat -A luci_splash_prerouting -j luci_splash_portal
-
-       iptables -t filter -I luci_splash_filter -s ! "$NETWORK/$PREFIX" -j RETURN
-       iptables -t nat    -I luci_splash_leases -s ! "$NETWORK/$PREFIX" -j RETURN
+       ### Add interface specific chain entry rules
+       ipt_log "zone_${zone}_prerouting" -i "${ifname%:*}" -s "$NETWORK/$PREFIX" -j luci_splash_prerouting -t nat
+       ipt_log "zone_${zone}_forward"    -i "${ifname%:*}" -s "$NETWORK/$PREFIX" -j luci_splash_forwarding -t filter
 
-       iptables -t filter -I luci_splash_filter -s "$NETWORK/$PREFIX" -d "$ipaddr/${netmask:-32}" -j RETURN
-       iptables -t nat    -I luci_splash_leases -s "$NETWORK/$PREFIX" -d "$ipaddr/${netmask:-32}" -j RETURN
+       ### Allow traffic to the same subnet
+       iptables -t nat    -I luci_splash_prerouting -d "$ipaddr/${netmask:-32}" -j RETURN
+       iptables -t filter -I luci_splash_forwarding -d "$ipaddr/${netmask:-32}" -j RETURN
 
+       ### Allow traffic to the mesh subnet
        [ "$parentproto" = "static" -a -n "$parentipaddr" ] && {
-               iptables -t filter -I luci_splash_filter -s "$NETWORK/$PREFIX" -d "$parentipaddr/${parentnetmask:-32}" -j RETURN
-               iptables -t nat    -I luci_splash_leases -s "$NETWORK/$PREFIX" -d "$parentipaddr/${parentnetmask:-32}" -j RETURN
+               iptables -t nat    -I luci_splash_prerouting -d "$parentipaddr/${parentnetmask:-32}" -j RETURN
+               iptables -t filter -I luci_splash_forwarding -d "$parentipaddr/${parentnetmask:-32}" -j RETURN
        }
 
-       iptables -t filter -A luci_splash_filter -s "$NETWORK/$PREFIX" -p udp --dport 53 -j RETURN
-       iptables -t filter -A luci_splash_filter -s "$NETWORK/$PREFIX" -p tcp --dport 22 -j RETURN    # XXX: ssh really needed?
-       iptables -t filter -A luci_splash_filter -s "$NETWORK/$PREFIX" -p tcp --dport 80 -j RETURN
-       iptables -t filter -A luci_splash_filter -s "$NETWORK/$PREFIX" -j REJECT --reject-with icmp-admin-prohibited
-
        qos_iface_add "$ifname"
-
-       append SPLASH_INTERFACES "$ifname"
 }
 
 iface_del() {
        config_get zone "$1" zone                                                                
        [ -n "$zone" ] || return 0
 
-       while iptables -t nat -D prerouting_${zone} -j luci_splash_prerouting 2>&-; do :; done
-
        config_get net "$1" network
        [ -n "$net" ] || return 0
 
        config_get ifname "$net" ifname
        [ -n "$ifname" ] || return 0
 
-       qos_iface_del "$ifname"
-}
-
-blacklist_add() {
-       local cfg="$1"
-       
-       config_get mac "$cfg" mac
-       [ -n "$mac" ] && {
-               iptables -t filter -I luci_splash_filter -m mac --mac-source "$mac" -j DROP
-               iptables -t nat    -I luci_splash_leases -m mac --mac-source "$mac" -j DROP
+       # Clear interface specific rules
+       [ -s $IPT_REPLAY ] && {
+               grep -- "-i ${ifname%:*}" $IPT_REPLAY | while read ln; do silent $ln; done
+               sed -ie "/-i ${ifname%:*}/d" $IPT_REPLAY
        }
-}
 
-whitelist_add() {
-       local cfg="$1"
-       
-       config_get mac "$cfg" mac
-       [ -n "$mac" ] && {
-               iptables -t filter -I luci_splash_filter -m mac --mac-source "$mac" -j RETURN
-               iptables -t nat    -I luci_splash_leases -m mac --mac-source "$mac" -j RETURN
-       }
+       qos_iface_del "$ifname"
 }
 
-lease_add() {
-       local cfg="$1"
-       
-       config_get mac "$cfg" mac
-       config_get ban "$cfg" kicked
-
-       ban=${ban:+DROP}
-
-       [ -n "$mac" ] && {
-               local oIFS="$IFS"; IFS=":"
-               set -- $mac
-               IFS="$oIFS"; unset oIFS
-       
-               local mac_pre="$1$2"
-               local mac_post="$3$4$5$6"
-               local handle="$6"
-
-               iptables -t filter -I luci_splash_filter -m mac --mac-source "$mac" -j RETURN
-               iptables -t nat    -I luci_splash_leases -m mac --mac-source "$mac" -j "${ban:-RETURN}"
-
-               [ "$LIMIT_UP" -gt 0 -a "$LIMIT_DOWN" -gt 0 ] && {
-                       iptables -t mangle -I luci_splash_mark -m mac --mac-source "$mac" -j MARK --set-mark 79
-
-                       for i in $SPLASH_INTERFACES; do
-                               tc filter add dev $i parent 77:0 protocol ip prio 2 handle ::$handle u32 \
-                                       match u16 0x0800 0xFFFF at -2 match u32 0x$mac_post 0xFFFFFFFF at -12 \
-                                       match u16 0x$mac_pre 0xFFFF at -14 flowid 77:10
-                       done
-               }
-       }
+mac_add() {
+       config_get mac "$1" mac
+       append MACS "$mac"
 }
 
 subnet_add() {
@@ -135,8 +99,8 @@ subnet_add() {
        config_get netmask "$cfg" netmask
        
        [ -n "$ipaddr" ] && {
-               iptables -t filter -I luci_splash_filter -d "$ipaddr/${netmask:-32}" -j RETURN
-               iptables -t nat    -I luci_splash_portal -d "$ipaddr/${netmask:-32}" -j RETURN
+               iptables -t nat    -I luci_splash_prerouting -d "$ipaddr/${netmask:-32}" -j RETURN
+               iptables -t filter -I luci_splash_forwarding -d "$ipaddr/${netmask:-32}" -j RETURN
        }
 }
 
@@ -145,7 +109,8 @@ qos_iface_add() {
 
        # 77 -> download root qdisc
        # 78 -> upload root qdisc
-       # 79 -> fwmark
+       # 79 -> fwmark: client->inet
+       # 80 -> fwmark: inet->client
 
        silent tc qdisc del dev "$iface" root handle 77:
 
@@ -157,16 +122,20 @@ qos_iface_add() {
 
                # set download limit and burst
                tc class add dev "$iface" parent 77:1 classid 77:10 htb \
-                       rate ${LIMIT_DOWN}kb ceil ${LIMIT_DOWN_BURST}kb prio 2
+                       rate ${LIMIT_DOWN}kbit ceil ${LIMIT_DOWN_BURST}kbit prio 2
 
                tc qdisc add dev "$iface" parent 77:10 handle 78: sfq perturb 10
 
                # adding ingress can result in "File exists" if qos-scripts are active
                silent tc qdisc add dev "$iface" ingress
 
+               # set client download speed
+               tc filter add dev "$iface" parent 77: protocol ip prio 2 \
+                       handle 80 fw flowid 77:10
+
                # set client upload speed
                tc filter add dev "$iface" parent ffff: protocol ip prio 1 \
-                       handle 79 fw police rate ${LIMIT_UP}kb mtu 6k burst 6k drop
+                       handle 79 fw police rate ${LIMIT_UP}kbit mtu 6k burst 6k drop
        fi      
 }
 
@@ -180,7 +149,7 @@ qos_iface_del() {
 
 boot() {
        ### Setup splash-relay
-       uci get lucid.splashr || {
+       uci get lucid.splashr 2>/dev/null || {
 uci batch <<EOF
        set lucid.splashr=daemon
        set lucid.splashr.slave=httpd
@@ -202,101 +171,113 @@ EOF
 }
 
 start() {
-       ### Read chains from config
-       include /lib/network
-       scan_interfaces
-       config_load luci_splash
-       
+       lock -w $LOCK && lock $LOCK
+
        ### Find QoS limits
        config_get LIMIT_UP general limit_up
        config_get LIMIT_DOWN general limit_down
        config_get LIMIT_DOWN_BURST general limit_down_burst
 
-       LIMIT_UP="${LIMIT_UP:-0}"
-       LIMIT_DOWN="${LIMIT_DOWN:-0}"
-       LIMIT_DOWN_BURST="${LIMIT_DOWN_BURST:-$(($LIMIT_DOWN * 2))}"
+       LIMIT_UP="$((8*${LIMIT_UP:-0}))"
+       LIMIT_DOWN="$((8*${LIMIT_DOWN:-0}))"
+       LIMIT_DOWN_BURST="${LIMIT_DOWN_BURST:+$((8*$LIMIT_DOWN_BURST))}"
+       LIMIT_DOWN_BURST="${LIMIT_DOWN_BURST:-$(($LIMIT_DOWN / 5 * 6))}"
 
        ### Load required modules
        [ "$LIMIT_UP" -gt 0 -a "$LIMIT_DOWN" -gt 0 ] && {
                silent insmod cls_fw
                silent insmod cls_u32
                silent insmod sch_htb
+               silent insmod sch_sfq
                silent insmod sch_ingress
        }
 
        ### Create subchains
-       iptables -t filter -N luci_splash_filter
-       iptables -t nat    -N luci_splash_portal
-       iptables -t nat    -N luci_splash_leases
        iptables -t nat    -N luci_splash_prerouting
+       iptables -t nat    -N luci_splash_leases
+       iptables -t filter -N luci_splash_forwarding
+       iptables -t filter -N luci_splash_filter
 
-       [ "$LIMIT_UP" -gt 0 -a "$LIMIT_DOWN" -gt 0 ] && \
-               iptables -t mangle -N luci_splash_mark
+       ### Clear iptables replay log
+       [ -s $IPT_REPLAY ] && . $IPT_REPLAY
+       echo -n > $IPT_REPLAY
 
        ### Build the main and portal rule
        config_foreach iface_add iface
        config_foreach subnet_add subnet
-       config_foreach blacklist_add blacklist
-       config_foreach whitelist_add whitelist
-       config_foreach lease_add lease
-       
-       ### Build the portal rule
-       iptables -t filter -I INPUT      -j luci_splash_filter
-       iptables -t filter -I FORWARD    -j luci_splash_filter
 
-       [ "$LIMIT_UP" -gt 0 -a "$LIMIT_DOWN" -gt 0 ] && \
-               iptables -t mangle -I PREROUTING -j luci_splash_mark
+       ### Add interface independant prerouting rules
+       iptables -t nat -A luci_splash_prerouting -j luci_splash_leases
+       iptables -t nat -A luci_splash_leases -p udp --dport 53 -j REDIRECT --to-ports 53
+       iptables -t nat -A luci_splash_leases -p tcp --dport 80 -j REDIRECT --to-ports 8082
 
-       ### Allow icmp, dns and traceroute
-       iptables -t nat -A luci_splash_portal -p udp --dport 33434:33523 -j RETURN
-       iptables -t nat -A luci_splash_portal -p icmp -j RETURN
-       iptables -t nat -A luci_splash_portal -p udp --dport 53 -j RETURN
+       ### Add interface independant forwarding rules
+       iptables -t filter -A luci_splash_forwarding -j luci_splash_filter
+       iptables -t filter -A luci_splash_filter -p tcp -j REJECT --reject-with tcp-reset
+       iptables -t filter -A luci_splash_filter -j REJECT --reject-with icmp-net-prohibited
+
+       ### Add QoS chain
+       [ "$LIMIT_UP" -gt 0 -a "$LIMIT_DOWN" -gt 0 ] && {
+               iptables -t mangle -N luci_splash_mark_out
+               iptables -t mangle -N luci_splash_mark_in
+               iptables -t mangle -I PREROUTING  -j luci_splash_mark_out
+               iptables -t mangle -I POSTROUTING -j luci_splash_mark_in
+       }
+
+       ### Find active mac addresses
+       MACS=""
+       config_foreach mac_add lease
+       config_foreach mac_add blacklist
+       config_foreach mac_add whitelist
 
-       ### Redirect the rest into the lease chain
-       iptables -t nat -A luci_splash_portal -j luci_splash_leases
-       
-       ### Build the leases rule
-       iptables -t nat -A luci_splash_leases -p tcp --dport 80 -j REDIRECT --to-ports 8082
-       
        ### Add crontab entry
        test -f /etc/crontabs/root || touch /etc/crontabs/root
        grep -q luci-splash /etc/crontabs/root || {
                echo '*/5 * * * *       /usr/sbin/luci-splash sync' >> /etc/crontabs/root
        }
+
+       lock -u $LOCK
+
+       ### Populate iptables
+       [ -n "$MACS" ] && luci-splash add-rules $MACS
 }
 
 stop() {
+       lock -w $LOCK && lock $LOCK
+
        ### Clear interface rules
-       include /lib/network
-       scan_interfaces
-       config_load luci_splash
        config_foreach iface_del iface
 
-       silent iptables -t filter -D INPUT      -j luci_splash_filter
-       silent iptables -t filter -D FORWARD    -j luci_splash_filter
-       silent iptables -t mangle -D PREROUTING -j luci_splash_mark
+       silent iptables -t mangle -D PREROUTING  -j luci_splash_mark_out
+       silent iptables -t mangle -D POSTROUTING -j luci_splash_mark_in
         
        ### Clear subchains
-       silent iptables -t nat    -F luci_splash_leases
-       silent iptables -t nat    -F luci_splash_portal
        silent iptables -t nat    -F luci_splash_prerouting
+       silent iptables -t nat    -F luci_splash_leases
+       silent iptables -t filter -F luci_splash_forwarding
        silent iptables -t filter -F luci_splash_filter
-       silent iptables -t mangle -F luci_splash_mark
+       silent iptables -t mangle -F luci_splash_mark_out
+       silent iptables -t mangle -F luci_splash_mark_in
        
        ### Delete subchains
-       silent iptables -t nat    -X luci_splash_leases
-       silent iptables -t nat    -X luci_splash_portal
        silent iptables -t nat    -X luci_splash_prerouting
+       silent iptables -t nat    -X luci_splash_leases
+       silent iptables -t filter -X luci_splash_forwarding
        silent iptables -t filter -X luci_splash_filter
-       silent iptables -t mangle -X luci_splash_mark
+       silent iptables -t mangle -X luci_splash_mark_out
+       silent iptables -t mangle -X luci_splash_mark_in
        
        sed -ie '/\/usr\/sbin\/luci-splash sync/d' /var/spool/cron/crontabs/root
+
+       lock -u $LOCK
 }
 
-       
 clear_leases() {
-       stop
-       while uci -P /var/state del luci_splash.@lease[0] 2>&-;do :; done
-       start
+       ### Find active mac addresses
+       MACS=""
+       config_foreach mac_add lease
+
+       ### Clear leases
+       [ -n "$MACS" ] && luci-splash remove $MACS
 }
 
index b55e960720ce0b0972e94d402e3b5bf4d7144f72..017cfebfcaee5c5d702c0bf38fd12a6c1b804ca8 100755 (executable)
 
 require("luci.util")
 require("luci.model.uci")
+require("luci.sys")
 require("luci.sys.iptparser")
 
 -- Init state session
 local uci = luci.model.uci.cursor_state()
 local ipt = luci.sys.iptparser.IptParser()
+local net = luci.sys.net
 
-local splash_interfaces = { }
 local limit_up = 0
 local limit_down = 0
 
+function lock()
+       os.execute("lock -w /var/run/luci_splash.lock && lock /var/run/luci_splash.lock")
+end
+
+function unlock()
+       os.execute("lock -u /var/run/luci_splash.lock")
+end
+
 function main(argv)
-       local cmd = argv[1]
-       local arg = argv[2]
+       local cmd = table.remove(argv, 1)
+       local arg = argv[1]
 
        limit_up = tonumber(uci:get("luci_splash", "general", "limit_up")) or 0
        limit_down = tonumber(uci:get("luci_splash", "general", "limit_down")) or 0
-       
-       uci:foreach("luci_splash", "iface", function(s)
-               if s.network then
-                       splash_interfaces[#splash_interfaces+1] = uci:get("network", s.network, "ifname")
-               end
-       end)
-
-       if cmd == "status" and arg then
-               if islisted("whitelist", arg) then
-                       print("whitelisted")
-               elseif islisted("blacklist", arg) then
-                       print("blacklisted")
-               else            
-                       local lease = haslease(arg)
-                       if lease and lease.kicked then
-                               print("kicked")
-                       elseif lease then
-                               print("lease")
+
+       if ( cmd == "lease" or cmd == "add-rules" or cmd == "remove" or
+            cmd == "whitelist" or cmd == "blacklist" ) and #argv > 0
+       then
+               lock()
+
+               local arp_cache      = net.arptable()
+               local leased_macs    = get_known_macs("lease")
+               local blacklist_macs = get_known_macs("blacklist")
+               local whitelist_macs = get_known_macs("whitelist")
+
+               for i, adr in ipairs(argv) do
+                       local mac = nil
+                       if adr:find(":") then
+                               mac = adr:lower()
                        else
-                               print("unknown")
+                               for _, e in ipairs(arp_cache) do
+                                       if e["IP address"] == adr then
+                                               mac = e["HW address"]
+                                               break
+                                       end
+                               end
+                       end
+
+                       if mac and cmd == "add-rules" then
+                               if leased_macs[mac] then
+                                       add_lease(mac, arp_cache, true)
+                               elseif blacklist_macs[mac] then
+                                       add_blacklist_rule(mac)
+                               elseif whitelist_macs[mac] then
+                                       add_whitelist_rule(mac)
+                               end
+                       elseif mac and ( cmd == "whitelist" or cmd == "blacklist" or cmd == "lease" ) then
+                               if cmd ~= "lease" and leased_macs[mac] then
+                                       print("Removing %s from leases" % mac)
+                                       remove_lease(mac)
+                                       leased_macs[mac] = nil
+                               end
+
+                               if cmd ~= "whitelist" and whitelist_macs[mac] then
+                                       print("Removing %s from whitelist" % mac)
+                                       remove_whitelist(mac)
+                                       whitelist_macs[mac] = nil                                       
+                               end
+
+                               if cmd ~= "blacklist" and blacklist_macs[mac] then
+                                       print("Removing %s from blacklist" % mac)
+                                       remove_blacklist(mac)
+                                       blacklist_macs[mac] = nil
+                               end
+
+                               if cmd == "lease" and not leased_macs[mac] then
+                                       print("Adding %s to leases" % mac)
+                                       add_lease(mac)
+                                       leased_macs[mac] = true
+                               elseif cmd == "whitelist" and not whitelist_macs[mac] then
+                                       print("Adding %s to whitelist" % mac)
+                                       add_whitelist(mac)
+                                       whitelist_macs[mac] = true
+                               elseif cmd == "blacklist" and not blacklist_macs[mac] then
+                                       print("Adding %s to blacklist" % mac)
+                                       add_blacklist(mac)
+                                       blacklist_macs[mac] = true
+                               else
+                                       print("The mac %s is already %sed" %{ mac, cmd })
+                               end
+                       elseif mac and cmd == "remove" then
+                               if leased_macs[mac] then
+                                       print("Removing %s from leases" % mac)
+                                       remove_lease(mac)
+                                       leased_macs[mac] = nil
+                               elseif whitelist_macs[mac] then
+                                       print("Removing %s from whitelist" % mac)
+                                       remove_whitelist(mac)
+                                       whitelist_macs[mac] = nil                                       
+                               elseif blacklist_macs[mac] then
+                                       print("Removing %s from blacklist" % mac)
+                                       remove_blacklist(mac)
+                                       blacklist_macs[mac] = nil
+                               else
+                                       print("The mac %s is not known" % mac)
+                               end
+                       else
+                               print("Can not find mac for ip %s" % argv[i])
                        end
                end
-               os.exit(0)
-       elseif cmd == "add" and arg then
-               if not haslease(arg) then
-                       add_lease(arg)
-               else
-                       print("already leased!")
-                       os.exit(2)
-               end
-               os.exit(0)
-       elseif cmd == "remove" and arg then
-               remove_lease(arg)
-               os.exit(0)              
+
+               unlock()
+               os.exit(0)      
        elseif cmd == "sync" then
                sync()
                os.exit(0)
+       elseif cmd == "list" then
+               list()
+               os.exit(0)
        else
-               print("Usage: " .. argv[0] .. " <status|add|remove|sync> [MAC]")
+               print("Usage:")
+               print("\n  luci-splash list\n    List connected, black- and whitelisted clients")
+               print("\n  luci-splash sync\n    Synchronize firewall rules and clear expired leases")
+               print("\n  luci-splash lease <MAC-or-IP>\n    Create a lease for the given address")
+               print("\n  luci-splash blacklist <MAC-or-IP>\n    Add given address to blacklist")
+               print("\n  luci-splash whitelist <MAC-or-IP>\n    Add given address to whitelist")
+               print("\n  luci-splash remove <MAC-or-IP>\n    Remove given address from the lease-, black- or whitelist")
+               print("")
+
                os.exit(1)      
        end
 end
 
--- Add a lease to state and invoke add_rule
-function add_lease(mac)
-       uci:section("luci_splash", "lease", nil, {
-               mac = mac,
-               start = os.time()
-       })
-       add_rule(mac)
-       
-       uci:save("luci_splash")
-end
+-- Get a list of known mac addresses
+function get_known_macs(list)
+       local leased_macs = { }
 
+       if not list or list == "lease" then
+               uci:foreach("luci_splash", "lease",
+                       function(s) leased_macs[s.mac:lower()] = true end)
+       end
 
--- Remove a lease from state and invoke remove_rule
-function remove_lease(mac)
-       mac = mac:lower()
-       remove_rule(mac)
+       if not list or list == "whitelist" then
+               uci:foreach("luci_splash", "whitelist",
+                       function(s) leased_macs[s.mac:lower()] = true end)
+       end
 
-       uci:delete_all("luci_splash", "lease",
-               function(s) return ( s.mac:lower() == mac ) end)
-               
-       uci:save("luci_splash")
+       if not list or list == "blacklist" then
+               uci:foreach("luci_splash", "blacklist",
+                       function(s) leased_macs[s.mac:lower()] = true end)
+       end
+
+       return leased_macs
 end
 
 
--- Add an iptables rule
-function add_rule(mac)
-       local a, b, c, d, e, f = mac:match("(%w+):(%w+):(%w+):(%w+):(%w+):(%w+)")
-       local mac_pre  = "%s%s" %{ a, b }
-       local mac_post = "%s%s%s%s" %{ c, d, e, f }
-       local handle   = f
+-- Get a list of known ip addresses
+function get_known_ips(macs, arp)
+       local leased_ips = { }
+       if not macs then macs = get_known_macs() end
+       for _, e in ipairs(arp or net.arptable()) do
+               if macs[e["HW address"]] then leased_ips[e["IP address"]] = true end
+       end
+       return leased_ips
+end
 
-       if limit_up > 0 and limit_down > 0 then
-               os.execute("iptables -t mangle -I luci_splash_mark -m mac --mac-source %q -j MARK --set-mark 79" % mac)
 
-               for _, i in ipairs(splash_interfaces) do
-                       os.execute("tc filter add dev %q parent 77:0 protocol ip prio 2 " % i .. 
-                               "handle ::%q u32 " % handle ..
-                               "match u16 0x0800 0xFFFF at -2 match u32 0x%q 0xFFFFFFFF at -12 " % mac_post ..
-                               "match u16 0x%q 0xFFFF at -14 flowid 77:10" % mac_pre)
+-- Helper to delete iptables rules
+function ipt_delete_all(args, comp, off)
+       off = off or { }
+       for i, r in ipairs(ipt:find(args)) do
+               if comp == nil or comp(r) then
+                       off[r.table] = off[r.table] or { }
+                       off[r.table][r.chain] = off[r.table][r.chain] or 0
+
+                       os.execute("iptables -t %q -D %q %d 2>/dev/null"
+                               %{ r.table, r.chain, r.index - off[r.table][r.chain] })
+
+                       off[r.table][r.chain] = off[r.table][r.chain] + 1
                end
        end
-
-       os.execute("iptables -t filter -I luci_splash_filter -m mac --mac-source %q -j RETURN" % mac)
-       return os.execute("iptables -t nat -I luci_splash_leases -m mac --mac-source %q -j RETURN" % mac)
 end
 
 
--- Remove an iptables rule
-function remove_rule(mac)
-       local handle = mac:match("%w+:%w+:%w+:%w+:%w+:(%w+)")
+-- Add a lease to state and invoke add_rule
+function add_lease(mac, arp, no_uci)
+       mac = mac:lower()
 
-       local function ipt_delete_foreach(args)
-               for _, r in ipairs(ipt:find(args)) do
-                       os.execute("iptables -t %q -D %q -m mac --mac-source %q %s 2>/dev/null"
-                               %{ r.table, r.chain, mac,
-                                       r.target == "MARK" and "-j MARK --set-mark 79" or
-                                               r.target and "-j %q" % r.target or "" })
+       -- Get current ip address
+       local ipaddr
+       for _, entry in ipairs(arp or net.arptable()) do
+               if entry["HW address"] == mac then
+                       ipaddr = entry["IP address"]
+                       break
                end
        end
 
-       ipt_delete_foreach({table="filter", chain="luci_splash_filter", options={"MAC", mac:upper()}})
-       ipt_delete_foreach({table="mangle", chain="luci_splash_mark", options={"MAC", mac:upper()}})
-       ipt_delete_foreach({table="nat", chain="luci_splash_leases", options={"MAC", mac:upper()}})
-
-       for _, i in ipairs(splash_interfaces) do
-               os.execute("tc filter del dev %q parent 77:0 protocol ip prio 2 " % i .. 
-                       "handle 800::%q u32 2>/dev/null" % handle)
+       -- Add lease if there is an ip addr
+       if ipaddr then
+               if not no_uci then
+                       uci:section("luci_splash", "lease", nil, {
+                               mac    = mac,
+                               ipaddr = ipaddr,
+                               start  = os.time()
+                       })
+                       uci:save("luci_splash")
+               end
+               add_lease_rule(mac, ipaddr)
+       else
+               print("Found no active IP for %s, lease not added" % mac)
        end
-
-       ipt:resync()
 end
 
 
--- Check whether a MAC-Address is listed in the lease state list
-function haslease(mac)
+-- Remove a lease from state and invoke remove_rule
+function remove_lease(mac)
        mac = mac:lower()
-       local lease = nil
 
-       uci:foreach("luci_splash", "lease",
-               function (section)
-                       if section.mac:lower() == mac then
-                               lease = section
+       uci:delete_all("luci_splash", "lease",
+               function(s)
+                       if s.mac:lower() == mac then
+                               remove_lease_rule(mac, s.ipaddr)
+                               return true
                        end
+                       return false
                end)
+               
+       uci:save("luci_splash")
+end
 
-       return lease
+
+-- Add a whitelist entry
+function add_whitelist(mac)
+       uci:section("luci_splash", "whitelist", nil, { mac = mac })
+       uci:save("luci_splash")
+       uci:commit("luci_splash")
+       add_whitelist_rule(mac)
 end
 
 
--- Check whether a MAC-Address is in given list
-function islisted(what, mac)
+-- Add a blacklist entry
+function add_blacklist(mac)
+       uci:section("luci_splash", "blacklist", nil, { mac = mac })
+       uci:save("luci_splash")
+       uci:commit("luci_splash")
+       add_blacklist_rule(mac)
+end
+
+
+-- Remove a whitelist entry
+function remove_whitelist(mac)
        mac = mac:lower()
+       uci:delete_all("luci_splash", "whitelist",
+               function(s) return not s.mac or s.mac:lower() == mac end)
+       uci:save("luci_splash")
+       uci:commit("luci_splash")
+       remove_lease_rule(mac)
+end
 
-       uci:foreach("luci_splash", what,
-               function (section)
-                       if section.mac:lower() == mac then
-                               stat = true
-                               return
-                       end
-               end)
 
-       return false
+-- Remove a blacklist entry
+function remove_blacklist(mac)
+       mac = mac:lower()
+       uci:delete_all("luci_splash", "blacklist",
+               function(s) return not s.mac or s.mac:lower() == mac end)
+       uci:save("luci_splash")
+       uci:commit("luci_splash")
+       remove_lease_rule(mac)
 end
 
 
--- Returns a list of MAC-Addresses for which a rule is existing
-function listrules()
-       local macs = { }
-       for i, r in ipairs(ipt:find({table="nat", chain="luci_splash_leases", options={"MAC"}})) do
-               macs[r.options[2]:lower()] = true
+-- Add an iptables rule
+function add_lease_rule(mac, ipaddr)
+       if limit_up > 0 and limit_down > 0 then
+               os.execute("iptables -t mangle -I luci_splash_mark_out -m mac --mac-source %q -j MARK --set-mark 79" % mac)
+               os.execute("iptables -t mangle -I luci_splash_mark_in -d %q -j MARK --set-mark 80" % ipaddr)
        end
-       return luci.util.keys(macs)
+
+       os.execute("iptables -t filter -I luci_splash_filter -m mac --mac-source %q -j RETURN" % mac)
+       os.execute("iptables -t nat    -I luci_splash_leases -m mac --mac-source %q -j RETURN" % mac)
+end
+
+
+-- Remove lease, black- or whitelist rules
+function remove_lease_rule(mac, ipaddr)
+       ipt:resync()
+
+       if ipaddr then
+               ipt_delete_all({table="mangle", chain="luci_splash_mark_in",  destination=ipaddr})
+               ipt_delete_all({table="mangle", chain="luci_splash_mark_out", options={"MAC", mac:upper()}})
+       end
+
+       ipt_delete_all({table="filter", chain="luci_splash_filter",   options={"MAC", mac:upper()}})
+       ipt_delete_all({table="nat",    chain="luci_splash_leases",   options={"MAC", mac:upper()}})
+end
+
+
+-- Add whitelist rules
+function add_whitelist_rule(mac)
+       os.execute("iptables -t filter -I luci_splash_filter -m mac --mac-source %q -j RETURN" % mac)
+       os.execute("iptables -t nat    -I luci_splash_leases -m mac --mac-source %q -j RETURN" % mac)
+end
+
+
+-- Add blacklist rules
+function add_blacklist_rule(mac)
+       os.execute("iptables -t filter -I luci_splash_filter -m mac --mac-source %q -j DROP" % mac)
+       os.execute("iptables -t nat    -I luci_splash_leases -m mac --mac-source %q -j DROP" % mac)
 end
 
 
 -- Synchronise leases, remove abandoned rules
 function sync()
-       local written = {}
+       lock()
+
        local time = os.time()
-       
+
        -- Current leases in state files
        local leases = uci:get_all("luci_splash")
        
@@ -191,36 +330,105 @@ function sync()
        uci:load("luci_splash")
        uci:revert("luci_splash")
        
-       
        -- For all leases
        for k, v in pairs(leases) do
                if v[".type"] == "lease" then
                        if os.difftime(time, tonumber(v.start)) > leasetime then
                                -- Remove expired
-                               remove_rule(v.mac)
+                               remove_lease_rule(v.mac, v.ipaddr)
                        else
                                -- Rewrite state
                                uci:section("luci_splash", "lease", nil, {              
                                        mac    = v.mac,
-                                       start  = v.start,
-                                       kicked = v.kicked
+                                       ipaddr = v.ipaddr,
+                                       start  = v.start
                                })
-                               written[v.mac:lower()] = 1
                        end
-               elseif v[".type"] == "whitelist" or v[".type"] == "blacklist" then
-                       written[v.mac:lower()] = 1
                end
        end
-       
-       
-       -- Delete rules without state
-       for i, r in ipairs(listrules()) do
-               if #r > 0 and not written[r:lower()] then
-                       remove_rule(r)
+
+       uci:save("luci_splash")
+
+       -- Get current IPs and MAC addresses
+       local macs = get_known_macs()
+       local ips  = get_known_ips(macs)
+
+       ipt:resync()
+
+       ipt_delete_all({table="filter", chain="luci_splash_filter", options={"MAC"}},
+               function(r) return not macs[r.options[2]:lower()] end)
+
+       ipt_delete_all({table="nat", chain="luci_splash_leases", options={"MAC"}},
+               function(r) return not macs[r.options[2]:lower()] end)
+
+       ipt_delete_all({table="mangle", chain="luci_splash_mark_out", options={"MAC", "MARK", "set"}},
+               function(r) return not macs[r.options[2]:lower()] end)
+
+       ipt_delete_all({table="mangle", chain="luci_splash_mark_in", options={"MARK", "set"}},
+               function(r) return not ips[r.destination] end)
+
+       unlock()
+end
+
+-- Show client info
+function list()
+       -- Get current arp cache
+       local arpcache = { }
+       for _, entry in ipairs(net.arptable()) do
+               arpcache[entry["HW address"]] = { entry["Device"], entry["IP address"] }
+       end
+
+       -- Find traffic usage
+       local function traffic(lease)
+               local traffic_in  = 0
+               local traffic_out = 0
+
+               local rin  = ipt:find({table="mangle", chain="luci_splash_mark_in", destination=lease.ipaddr})
+               local rout = ipt:find({table="mangle", chain="luci_splash_mark_out", options={"MAC", lease.mac:upper()}})
+
+               if rin  and #rin  > 0 then traffic_in  = math.floor( rin[1].bytes / 1024) end
+               if rout and #rout > 0 then traffic_out = math.floor(rout[1].bytes / 1024) end
+
+               return traffic_in, traffic_out
+       end
+
+       -- Print listings
+       local leases = uci:get_all("luci_splash")
+
+       print(string.format(
+               "%-17s  %-15s  %-9s  %-4s  %-7s  %20s",
+               "MAC", "IP", "State", "Dur.", "Intf.", "Traffic down/up"
+       ))
+
+       -- Leases
+       for _, s in pairs(leases) do
+               if s[".type"] == "lease" and s.mac then
+                       local ti, to = traffic(s)
+                       local mac = s.mac:lower()
+                       local arp = arpcache[mac]
+                       print(string.format(
+                               "%-17s  %-15s  %-9s  %3dm  %-7s  %7dKB  %7dKB",
+                               mac, s.ipaddr, "leased",
+                               math.floor(( os.time() - tonumber(s.start) ) / 60),
+                               arp and arp[1] or "?", ti, to
+                       ))
+               end
+       end
+
+       -- Whitelist, Blacklist
+       for _, s in luci.util.spairs(leases,
+               function(a,b) return leases[a][".type"] > leases[b][".type"] end
+       ) do
+               if (s[".type"] == "whitelist" or s[".type"] == "blacklist") and s.mac then
+                       local mac = s.mac:lower()
+                       local arp = arpcache[mac]
+                       print(string.format(
+                               "%-17s  %-15s  %-9s  %4s  %-7s  %9s  %9s",
+                               mac, arp and arp[2] or "?", s[".type"], "- ",
+                               arp and arp[1] or "?", "-", "-"
+                       ))
                end
        end
-       
-       uci:save("luci_splash")
 end
 
 main(arg)