Cut Internet temporarily in a device in your LAN

Cut Internet temporarily in a device in your LAN (or Applying for the Worst Daddy Award 2017)

Background

(take it with a grain of salt)

Internet is great. Children are great, too. The combination of both can be sometimes problematic. Tablets are very attractive devices for children, which maybe are not mature enough to make a proper use. Well, some adults are not good examples either.
If you want to limit the Internet use of these little criatures that are wandering around your house but you are not brave enough to tell them to give their tablets to you, keep on reading. At the end of the day theyā€™ll keep their digital devices with them but without Internet. They will blame your Internet Service Provider and then start playing with other toys or children orā€¦ you.

Objective

The main aim is to setup an automation to disconnect temporarily a device in our LAN using, obviously, Home Assistant on a Raspberry Pi with Hassbian (it could also work in other scenarios but Iā€™m not 100% sure).

(My usual) Disclaimer: Iā€™m a computer hobbyist, self-learner, and with proficiency in copy-paste tool. What I describe here has worked for me but YMMV). As you may have guessed, my mother language is not English so sorry for any vocabulary inaccuracy, and so on.
(Additional) Disclaimer: In this post I assume that you are fairly comfortable with the command line usage, know how to ssh to your RPi and are used to concepts such as IP, MAC address, sudo, and stuff like that. Typical things if you belong to the HA community.

Steps

Step 1.- Getting the IP address
Itā€™s pretty obvious that we must know something from the device where we want to cut the Internet connection. If you have set a static IP for this device in your LAN you can skip this step. Iā€™m assuming now that you know the MAC address and not the IP. For this we will use nmap, which should be already available in Hassbian distribution. So, ssh to your RPi and write:

pi@hassbian:~ $ nmap -sP ROUTER_ADRS/24 >/dev/null && arp -an | grep CHILD_MAC | awk '{print $2}' | sed 's/[()]//g'

where ROUTER_ADRS is your router IP (for example: 192.168.0.1) and CHILD_MAC is the MAC address of the device (for example: AA:BB:CC:DD:EE:FF). The previous command will show the deviceā€™s IP address in the screen.

Step 2.- Installing arpspoof
We will use the arpspoof command later. We have to install it first. Iā€™ll save you a Google search; it belongs to the dsniff package, so:

pi@hassbian:~ $ sudo apt-get install dsniff

Step 3.- Using arpspoof
You need to run arpspoof with the sudo command, so:

pi@hassbian:~ $ sudo arpspoof -i INTERFACE -t CHILD_ADRS ROUTER_ADRS

where INTERFACE is the network interface used by the RPi to connect to the router (typically eth0 if it is wire-connected, wlan0 if it is wifi) and CHILD_ADRS is the deviceā€™s IP address obtained in Step 1.
Once you press Enter youā€™ll see some messages in the screen and you wonā€™t be able to connect to the Internet with the device (try it!) until you stop arpspoofing by pressing Ctrl+C. Iā€™m not going to spend any time commenting what is happening behind the scenes. Wikipedia and Google are your friends.
Since arpspoof (i) needs root privileges, (ii) will be used later in a bash script, and (iii) we donā€™t want to be asked for the password, we need to make some modifications to the system to allow the homeassistant user run arpspoof. This is accomplished by adding these two lines to the /etc/sudoers.d/020_homeassistant_hassbian-scripts file:

homeassistant ALL=(ALL) NOPASSWD: /usr/sbin/arpspoof
homeassistant ALL=(ALL) NOPASSWD: /usr/bin/killall arpspoof

Disclaimer: I donā€™t know 100% the consequences in terms of security issues when doing this. Youā€™ve been warned.

Step 4.- Setting up everything in HA
Ok, so what we are trying to accomplish is this card in the frontend:

which is grouped and saved, in my case, in .homeassistant/includes/groups/internet_control.yaml :

internet:
  name: 'Internet'
  control: hidden
  entities:
    - automation.disconnect_ipad2
    - input_slider.internet_cut_time
    - input_slider.internet_cut_hour
    - input_slider.internet_cut_minutes
    - sensor.internetcuttime

It consists of three input_sliders. The first one (internet_cut_time) defines the duration in minutes that is going to take the Internet disconnection. The second and third sliders define the hour of the day when the disconnection is going to hapen: (internet_cut_hour) defines the hour and (internet_cut_minutes) defines the minutes. In my setup all this is defined in this file .homeassistant/includes/input_sliders/internet_cuttime.yaml :

internet_cut_time:
  name: 'Disconnection time (min)'
  initial: 120
  min: 2
  max: 240
  step: 1
internet_cut_hour:
  name: Hour
  icon: mdi:timer
  initial: 1
  min: 0
  max: 23
  step: 1
internet_cut_minutes:
  name: Minutes
  icon: mdi:timer
  initial: 05
  min: 0
  max: 55
  step: 5

There is also a sensor created to easily visualize the moment when the disconnection will happen. It gathers the information from the two previous sliders. In my setup this sensor is defined in the file .homeassistant/includes/sensors/internet_cut.yaml :

- platform: template
  sensors:
    internetcuttime:
      friendly_name: Switch off at
      entity_id:
        - input_slider.internet_cut_hour
        - input_slider.internet_cut_minutes
      value_template: '{{ "%0.02d:%0.02d" | format(states("input_slider.internet_cut_hour") | int, states("input_slider.internet_cut_minutes") | int) }}'

The first element of the card is the automation. In my case, Iā€™ve migrated from the split automation configuration to the one-file automation configuration. So, I have this entry in my automations.yaml file:

- id: id031
  alias: 'Disconnect iPad2'
  trigger:
    platform: time
    minutes: '/5'
    seconds: '0'
  condition:
    condition: template
    value_template: '{{ ((now().strftime("%s") | int ) | timestamp_custom("%H:%M")) == states.sensor.internetcuttime.state  }}'
  action:
    - service: shell_command.cut_internet_temp

As you can see, the automation calls a shell_command that I have placed in the file .homeassistant/includes/shell_commands/cut_internet.yaml :

cut_internet_temp: '/home/homeassistant/.homeassistant/includes/shell_scripts/cut_internet.sh {{ states.input_slider.internet_cut_time.state }}'

which calls a bash script with the Internet disconnection duration as argument. I have the bash scripts stored in .homeassistant/includes/shell_scripts folder and the file cut_internet.sh gathers the instructions we already saw in Steps 1 and 3. This is the file contents (you have to update the routerā€™s IP, the MAC address of the device you want to disconnect and the network interface the RPi uses):

#!/bin/bash
# The argument is saved in CUT_TIME variable
CUT_TIME=$1
# Update your router's IP
ROUTER_ADRS=WWW.XXX.YYY.ZZZ
# Update the device's MAC address
CHILD_MAC=AA:BB:CC:DD:EE:FF
# Update the network interface
INTERFACE=eth0
# Finding the device's IP address based on the MAC
CHILD_ADRS=$(nmap -sP ${ROUTER_ADRS}/24 >/dev/null && arp -an | grep ${CHILD_MAC} | awk '{print $2}' | sed 's/[()]//g')
# Arpspoofing the device
nohup sudo arpspoof -i ${INTERFACE} -t ${CHILD_ADRS} ${ROUTER_ADRS} > /dev/null 2>&1 &
# Pausing the script for CUT_TIME minutes (arpspoof is active during this time)
sleep ${CUT_TIME}m
# Killing the arpspoof command (the disconnection ends)
sudo killall arpspoof
# I have to check what happens if there are more than one instance of this script running in parallel
# and the first one that ends kills the arpspoof commands of the rest (due to the killall)

Finally, all the related components appear in my configuration.yaml file as:

group: !include_dir_merge_named includes/groups
input_slider: !include_dir_merge_named includes/input_sliders
sensor: !include_dir_merge_list includes/sensors
shell_command: !include_dir_merge_named includes/shell_commands
automation: !include automations.yaml

Final words

If I havenā€™t been clear enough, please ask. Iā€™ll try to do my best to help.
It is very likely that this can be accomplished in a simpler/safer way (remember the disclaimers). Iā€™m ready to follow your hints (if any) and keep on learning.
The current automation only cuts Internet based on the hour of the day. My next step is to include an automation taking into account how much time the device has been connected during the day. Iā€™ll continue to update this post as I make progress. You are invited to share your improvements and automations too.
Thanks for reading!

31 Likes

I donā€™t need something like this, but well done.
I may let my father use this, my little sister uses her ipad too much.

2 Likes

This arpspoofing method is EXACTLY how Circle worksā€¦ Well done rolling your own!

2 Likes

If only I had HA and this a few years ago. Not sure I could still add it in with 4 wifi AP on 2 subnets now. Bravo though, great project!

1 Like

This looks amazing, Iā€™ve not finished setting it up. But itā€™s this sort of community generate stuff that really makes a product come to life (thank you)

Ok. Some questions :slight_smile: - With the device identifiable and its internet access controllable, Is there a way that I can flip a switch to immediately disconnect a device, rather than wait for the timer to end ?

Also how would you set up the file if you want to track more that 1 device, I have 2 kids each with access to an iPad ?

Thank you!

@CCOSTAN Wow, that CIRCLE device seems to be a good finished solution. But the priceā€¦

Thank you! 4 wifi AP on 2 subnets! This community always surprising me!

Thanks to you, HA devs and to this community!

Right now there is not such switch. To immediately disconnect a device with the current set up you will have to (imagine that itā€™s 20:41):

  • Update the ā€œHourā€ and ā€œMinutesā€ input_sliders to 20 and 45
  • Update the first input_slider ā€œDisconnection time (min)ā€ to the minutes you want to disconnect the device (right now between 2 and 240 min)
  • Activate the automation switch
  • Wait a little bit (4 min) for the disconnection to happen

We have several solutions here depending on the situation:

Situation 1: you want to apply the same starting time and disconnection duration to both devices

In this case we could modify the bash script adding a second device. Iā€™ll have to test it in a near future (also two children wandering around). Maybe something like this could work:

#!/bin/bash
# The argument is saved in CUT_TIME variable
CUT_TIME=$1
# Update your router's IP
ROUTER_ADRS=WWW.XXX.YYY.ZZZ
# Update the device's MAC address
CHILD1_MAC=AA:BB:CC:DD:EE:FF
CHILD2_MAC=GG:HH:II:JJ:KK:LL
# Update the network interface
INTERFACE=eth0
# Finding the device's IP address based on the MAC
CHILD1_ADRS=$(nmap -sP ${ROUTER_ADRS}/24 >/dev/null && arp -an | grep ${CHILD1_MAC} | awk '{print $2}' | sed 's/[()]//g')
CHILD2_ADRS=$(nmap -sP ${ROUTER_ADRS}/24 >/dev/null && arp -an | grep ${CHILD2_MAC} | awk '{print $2}' | sed 's/[()]//g')
# Arpspoofing the device
nohup sudo arpspoof -i ${INTERFACE} -t ${CHILD1_ADRS} ${ROUTER_ADRS} > /dev/null 2>&1 &
nohup sudo arpspoof -i ${INTERFACE} -t ${CHILD2_ADRS} ${ROUTER_ADRS} > /dev/null 2>&1 &
# Pausing the script for CUT_TIME minutes (arpspoof is active during this time)
sleep ${CUT_TIME}m
# Killing the arpspoof command (the disconnection ends)
sudo killall arpspoof
# I have to check what happens if there are more than one instance of this script running in parallel
# and the first one that ends kills the arpspoof commands of the rest (due to the killall)

Situation 2: you want to apply different starting time and disconnection duration to both devices

In this case I think the best way to proceed is to duplicate (and rename) all the elements explained before. However, Iā€™m not sure what will happen when one of the killall commands is executed: maybe it kills the arpspoof of both automations.

In a near future Iā€™ll update the post because I also have two kids.
I will also try to implement the ā€œDisconnect nowā€ switch you mentioned before.

Just and one firewall, config the URLF and itĀ“s done.
HA its nice but do not try to invent the wheel

1 Like

Could you elaborate a bit more?

I could see two scripts per device or maybe better per child. One to start the spoof and one to stop it.

with one toggle:
SPOOF_CHILD ON/OFF
when it goes to ON start the script
when it goes to OFF kill the script

Then we have lots of options:

  • The automation that you have would still work.
  • Manual kill switch :slight_smile:
  • Start now for x minutes
  • WiFi free Dinner Time
1 Like

That is a very smart strategy because, as you say, the options for automation increase. However, we have to think how to face the new situation because I know how to start the spoof (nohup sudo arpspoof [ā€¦]) but I only know how to stop ALL of them (sudo killall arpspoof). Maybe it is possible to retain the PID of the arpspoof in order to kill it properly (sudo kill -9 PID). Iā€™m going to think about it because, definitely, more automation flexibility is achived as you point out. Thanks!

Just guessing here:
create a little script that has just this line (accepting all the parameters) per child

SPOOF_CHILD1.sh

arpspoof -i ${INTERFACE} -t ${CHILD1_ADRS} ${ROUTER_ADRS}

then call it like

nohup sudo SPOOF_CHILD1.sh <parameters> > /dev/null 2>&1 &

and kill it like this:

sudo killall SPOOF_CHILD1.sh

this is all untested. I need to find a way to install arpspoof on my unraid box

@nodecentral , @datamonkey , thanks for your suggestions.

@datamonkey , while your approach may also work I already started to make modifications yesterday and have reached the solution explained below. So here are the changes to the first post:

Update to 2 devices and manual disconnection
Ok, so what I try to accomplish is summarized in the card shown in the next figure. The red box joins the elements needed to make a programmed control of the disconnection while the blue one corresponds to the manual (and immediate) disconnection/connection.

Letā€™s start explaining this card, which is saved, in my case, in .homeassistant/includes/groups/internet_control.yaml:

internet:
  name: 'Internet'
  control: hidden
  entities:
    - automation.disconnect_ipad_1_prog
    - automation.disconnect_ipad_2_prog
    - input_slider.internet_cut_time
    - input_slider.internet_cut_hour
    - input_slider.internet_cut_minutes
    - sensor.internetcuttime
    - switch.internet_ipad1
    - switch.internet_ipad2

It consists of the same three input_sliders explained earlier in this post and have not been modified. The sensor created to easily visualize the moment when the disconnection will happen remains the same, too.

The first two elements of the card is the automation for the programmed disconnection. Keep in mind that it is an automation with configurable starting and duration times on the fly; so no need to restart HA when re-programming the disconnection. I have these two entries in my automations.yaml file:

- id: id031
  alias: 'Disconnect iPad 1 (prog)'
  trigger:
    platform: time
    minutes: '/5'
    seconds: '0'
  condition:
    condition: template
    value_template: '{{ ((now().strftime("%s") | int ) | timestamp_custom("%H:%M")) == states.sensor.internetcuttime.state  }}'
  action:
    - service: shell_command.cut_internet_ipad1

- id: id032
  alias: 'Disconnect iPad 2 (prog)'
  trigger:
    platform: time
    minutes: '/5'
    seconds: '0'
  condition:
    condition: template
    value_template: '{{ ((now().strftime("%s") | int ) | timestamp_custom("%H:%M")) == states.sensor.internetcuttime.state  }}'
  action:
    - service: shell_command.cut_internet_ipad2

Programmed Internet disconnection can be applied to one or both devices. With the current setup only one program per day is allowed and, if both devices are activated, they will run with the same timing settings.
As in my previous post, the automation calls a shell_command that is in the file .homeassistant/includes/shell_commands/cut_internet.yaml and has been modified to include two devices:

cut_internet_ipad1: '/home/homeassistant/.homeassistant/includes/shell_scripts/control_internet.sh DE:VI:CE:ONE:MAC {{ states.input_slider.internet_cut_time.state }}'

cut_internet_ipad2: '/home/homeassistant/.homeassistant/includes/shell_scripts/control_internet.sh DE:VI:CE:TWO:MAC {{ states.input_slider.internet_cut_time.state }}'

which calls a complete renewed bash script with two arguments: MAC and programmed disconnection time. I have the bash scripts stored in .homeassistant/includes/shell_scripts folder and the file control_internet.sh is now:

#!/bin/bash
# Examples:
# Activate spoofing given a MAC address
# ./control_internet AA:BB:CC:DD:EE:FF on
# Deactivate spoofing given a MAC address
# ./control_internet AA:BB:CC:DD:EE:FF off
# Activate spoofing for 10 minutes given a MAC address
# ./control_internet AA:BB:CC:DD:EE:FF 10
CHILD_MAC=$1
INTERFACE=eth0
ROUTER_ADRS=192.168.0.1
if [ "$2" = "on" ]
then
# Activating manual spoof
    CHILD_ADRS=$(nmap -sP ${ROUTER_ADRS}/24 >/dev/null && arp -an | grep ${CHILD_MAC} | awk '{print $2}' | sed 's/[()]//g')
    sudo arpspoof -i ${INTERFACE} -t ${CHILD_ADRS} ${ROUTER_ADRS} > /dev/null 2>&1 & echo $! > /tmp/manualspoof-${CHILD_MAC}.pid
elif [ "$2" = "off" ]
then
# Deactivating manual spoof
    sudo pkill -P `cat /tmp/manualspoof-${CHILD_MAC}.pid`
    rm /tmp/manualspoof-${CHILD_MAC}.pid
else
# Activate temporarily spoof
    CUT_TIME=$2
    CHILD_ADRS=$(nmap -sP ${ROUTER_ADRS}/24 >/dev/null && arp -an | grep ${CHILD_MAC} | awk '{print $2}' | sed 's/[()]//g')
    sudo arpspoof -i ${INTERFACE} -t ${CHILD_ADRS} ${ROUTER_ADRS} > /dev/null 2>&1 & echo $! > /tmp/progspoof-${CHILD_MAC}.pid
    sleep ${CUT_TIME}m
    sudo pkill -P `cat /tmp/progspoof-${CHILD_MAC}.pid`
    rm /tmp/progspoof-${CHILD_MAC}.pid
fi

When the second argument is not ā€œonā€ or ā€œoffā€ we assume that it is the programmed disconnection time (in minutes) and the else part of the script is run. This part is very similar to the script posted initially with some modifications to retrieve the PID of the arpspoof process, saving it in a temp file in order to kill the process when the disconnection time passes. What about the ā€œonā€ or ā€œoffā€ conditions? Keep on reading.

The two last switches in the card account for manual (and instantaneous) disconnection as @datamonkey suggested. They are command line switches that I place in .homeassistant/includes/switches/manual_control_internet.yaml and contains:

# Internet manual disconnect switches
- platform: command_line
  switches:
    internet_ipad1:
      command_on: "/home/homeassistant/.homeassistant/includes/shell_scripts/control_internet.sh DE:VI:CE:ONE:MAC on"
      command_off: "/home/homeassistant/.homeassistant/includes/shell_scripts/control_internet.sh DE:VI:CE:ONE:MAC off"
      friendly_name: Disconnect iPad 1 (manual)

- platform: command_line
  switches:
    internet_ipad2:
      command_on: "/home/homeassistant/.homeassistant/includes/shell_scripts/control_internet.sh DE:VI:CE:TWO:MAC on"
      command_off: "/home/homeassistant/.homeassistant/includes/shell_scripts/control_internet.sh DE:VI:CE:TWO:MAC off"
      friendly_name: Disconnect iPad 2 (manual)

As you can observe each switch uses one MAC and calls the bash script commented before but in this case the second argument is ā€œonā€ or ā€œoffā€; to active the spoofing or stop it. Again, we save the PID information to a temp file in order to know which process we have to kill.

If we want to have a toogle switch in the frontend and not two separate lighting bolt icons, we have to add to our customization file this:

switch.internet_ipad1:
  assumed_state: false
switch.internet_ipad2:
  assumed_state: false

Extrapolation to more devices should be straightforward since no modification to the bash script is needed; just add the elements in the frontend and make slight modifications to the corresponding files. I have tested it and it works for me, although maybe there are combinations that I havenā€™t tried. Thanks for reading.

1 Like

I am trying this command - nmap -sP 192.168.0.124 >/dev/null && arp -an | grep xx:xx:xx:xx | awk ā€˜{print $2}ā€™ | sed ā€˜s/[()]//gā€™

but i am not getting nay results. Tried 2-3 mac address connected to wlan0. I have given static IP to ipads.

Assuming your router IP is 192.168.0.1 it lacks a / symbol, that is:

nmap -sP 192.168.0.1/24

Assuming your router IP is 192.168.0.124 it lacks a /24 string, that is:

nmap -sP 192.168.0.124/24

Could that be the issue?

Oh sorry that was a typo . I am giving command with 192.168.0.1/24 . If i do nmap -nsP 192.168.0.1/24 it is showing the ip i am using for ipads

Good to know! Iā€™m looking at the man page for nmap and itā€™s huge. I found this: In previous releases of Nmap, -sn was known as -sP.
So maybe the args that I used in nmap (which I took somewhere on the Internet) are outdated or do not fit in every situation. So, thanks for telling that nmap -nSP works for you. As stated in my disclaimer, unfortunately, Iā€™m good at Google-fu but my knowledge in these topics is very limited :pensive:

:slight_smile: You got me wrong. I only get result of all the networkā€™s ipā€™s connected to the router if i give command nmap -nsP 192.168.0.1/24 but if i give nmap -sP 192.168.0.1/24 >/dev/null && arp -an | grep xx:xx:xx:xx | awk ā€˜{print $2}ā€™ | sed ā€˜s/[()]//gā€™ i get nothing on the screen.

Ok, it seems Iā€™m also rather limited at understanding other people :slight_smile:
In my case I obtain the same output using nmap -nsP or nmap -sP:

Starting Nmap 6.47 ( http://nmap.org ) at 2017-06-21 09:49 CEST
Nmap scan report for 192.168.0.1
Host is up (0.0058s latency).
Nmap scan report for 192.168.0.8
Host is up (0.0017s latency).
Nmap scan report for 192.168.0.192
Host is up (0.012s latency).
Nmap scan report for 192.168.0.196
Host is up (0.045s latency).
Nmap scan report for 192.168.0.197
Host is up (0.024s latency).
Nmap scan report for 192.168.0.200
Host is up (0.010s latency).
Nmap done: 256 IP addresses (6 hosts up) scanned in 3.47 seconds

The only difference is the time it takes to scan: 15.76 seconds (with -sP) and 3.47 seconds (with -nsP)
Anyway, what happens if you type arp -an?
I get this:

[...]
? (192.168.0.86) at <incomplete> on eth0
? (192.168.0.61) at <incomplete> on eth0
? (192.168.0.71) at <incomplete> on eth0
? (192.168.0.34) at <incomplete> on eth0
? (192.168.0.192) at JUST:SOME:MAC:ADD:RESS:HERE [ether] on eth0
? (192.168.0.10) at <incomplete> on eth0
[...]

I got it workingā€¦ Thanks :slight_smile:

1 Like