Why block your own camera?
With home automation it’s now easy and fun to create your own home security setup. To that end you’ll want to have a number of sensors of various kinds, but in all likelihood, you’ll complement that with an IP camera to record pictures of the bad guys if they ever break in.
Now, there’s a few models of cheap IP cameras out there that can do the job. The one I’m using is TP-Link’s Tapo C110 one, which is very cheap on Amazon.
However, we’ve all read how IP camera makers suck at security, and people get their home feeds hacked without even knowing it, because the camera opens a tunnel to the maker’s infrastructure to allow for remote access. Remote access from an app is convenient, but if security is not taken seriously, the cost can be high.
Now, in my case, the camera’s motion detection and stream are set up and consumed by my own server, inside my LAN. If the alarm goes off, recording takes place locally, and then the server pushes copies of the videos to a remote place. For all of that to happen, the camera does not need to access the outside world. And because it does it anyway by default without a way to block it, it’s putting my private data at risk.
So let’s use some cool Linux stuff and prevent it from doing so, by turning a cheap server into a WiFi access point that controls who talks to what.
Overview of the setup
I’m addressing the problem with the following:
- a Raspberry Pi (in this case a model 4B but something smaller would work)
- connected to the main LAN via ethernet
- creating a WiFi access point specifically for the cameras (and any other devices in need of similar “taming”)
- Raspberry Pi OS bullseye (11)
- the main protagonists will be
dnsmasq
andhostapd
, and a custom script setting things up withiptables
. With those guys we’ll turn the Raspberry into a router.
Note that nowadays I set up all my server stuff with Ansible
. So I’ll be explaining things here both as if running the steps manually, and provide the ansible configuration that corresponds at the end (but I won’t show a standalone Ansible playbook, that’s beyond the scope of this article).
We’ll go about the setup in the following order:
- installing stuff
- setting up the WiFi hot spot
- setting up DHCP
- setting up routing
Preparing the server
We’re going to install the following packages:
sudo apt update && sudo apt install dnsmasq hostapd iptables
And we need to ensure WiFi is not being suppressed on the Raspberry:
sudo rfkill list
# 0: phy0: Wireless LAN
# Soft blocked: no
# Hard blocked: no
# 1: hci0: Bluetooth
# Soft blocked: yes
# Hard blocked: no
If the Wifi has a yes
in soft or hard blocked, we need to unblock it with
sudo rfkill unblock 0
This setting is both applied immediately and saved for future reboots, so running the command once is enough.
Setting up the WiFi access point
Let’s set up hostapd
. We’ll need to set the contents of /etc/hostapd/hostapd.conf
to this:
interface=wlan0
driver=nl80211
ssid=my_hotspot
# 2.4GHz
hw_mode=g
channel=6
# set to 1 to enable QOS
wmm_enabled=0
macaddr_acl=0
# WPA, not WEP
auth_algs=1
ignore_broadcast_ssid=0
# WPA-2
wpa=2
wpa_passphrase=somesecurepassword
wpa_key_mgmt=WPA-PSK
wpa_pairwise=TKIP
rsn_pairwise=CCMP
To be honest, I don’t know what some of these lines are for. I found this somewhere and it works for me. Of particular relevance will be:
ssid
andwpa_passphrase
need to be adjusted to what you want to set up as hotspot name and passwordchannel
is set to 6, that works okay for me since my main WiFi access point is sitting next to the Raspberry and is on channel 1.- WEP is disabled via
auth_algs=1
andwpa
version is 2, which should be okay
Now we can enable the service. By default after installation it is masked (since it has no valid configuration before we set up the file above). So we’ll do:
sudo systemctl unmask hostapd
sudo systemctl enable --now hostapd
Now you should be able to see the hotspot from WiFi devices like your phone. If you connect, however, you won’t have internet (or any kind of) access. We’ll address that in the next section.
If you leave this as it is, however, after rebooting the hotspot won’t be visible anymore. That’s because of a race condition between the DHCP client and hostapd
, since the DHCP client, at least in my case, starts after hostapd
. To prevent this we’ll edit /etc/dhcpcd.conf
and add the following at the end:
interface wlan0
nohook wpa_supplicant
I’m not 100% sure of everything that’s happening here, but I believe this avoids dhcpcd triggering wpa_supplicant
which in turns would override the WiFi module’s settings and changing it back from hot post to client.
Setting up a DHCP server with dnsmasq
Now on to setting up a DHCP server. Your cameras and other devices connecting to the WiFi hotspot will rely on DHCP to get their IP address. dnsmasq
is a lightweight server that is capable of simple DNS services, but also of DHCP. We’ll set it up in such a way that it doesn’t do anything related to DNS but simply handles DHCP server tasks. Edit /etc/dnsmasq.conf
as follows:
interface=wlan0
port=0
dhcp-range=10.0.0.50,10.0.0.150,12h
dhcp-option=6,1.1.1.1
dhcp-host=B4:B0:24:AA:BB:CC,10.0.0.84
dhcp-host=5C:A6:E6:AA:BB:CC,10.0.0.85
Here we’re telling dnsmasq
to:
- use the range of IP’s 10.0.0.50 to 10.0.0.150, with a lease time of 12 hours (the time after which devices have to ask for a renewal of the IP)
- use 1.1.1.1 as DNS server – note: I’m actually using a LAN address in my case since I host my own DNS server. Consider that when we block internet access, access to an external DNS like 1.1.1.1 will be blocked too – but then, that’s probably not a problem anyway.
- set up permanent addresses (within the range!) for a couple of MAC addresses corresponding to the cameras. This is not mandatory, but I do assign permanent IP addresses to my cameras so that home automation knows where to find them.
We also need to set up an IP address for the wlan0
interface. dnsmasq
does require one before we can use it, and it should be set up with a subnet that covers the IP address range we’ve configured.
sudo ip addr add 10.0.0.1/24 dev wlan0
Now save and start dnsmasq
:
sudo systemctl enable --now dnsmasq
Connected devices will now receive an IP address. Don’t get impatient though, we still don’t have full internet connectivity as we need to set up routing!
Setting up routing
Now we just need to tell our Raspberry to act as a router between its 2 interfaces, eth0
and wlan0
, which are on different subnets.
First, we’ll let the kernel know about our intentions. By default packet forwarding is disabled and we enable it with:
sudo sysctl -w net.ipv4.ip_forward=1
This will take effect immediately and persist beyond reboot.
Let’s get another thing out of the way. Assuming the IP address of the Pi on eth0
is 192.168.1.20
, we are going to turn it into a router that will forward traffic from the 192.168.1.0/24
subnet to the 10.0.0.0/24
one. That means that traffic directed at our IP cameras on 10.0.0.84
need to reach our Pi on 192.168.1.20
. How will other devices know?
Well, the easiest way for me was to tell my home router (the “internet box”) to route traffic meant for 10.0.0.0/24
via 192.168.1.20
. Each home router is different but usually the advanced options allow you to set up a static route. The benefit is that for every device in the main network, 10.0.0.0/24
is nothing special, and to send traffic there, they need to direct layer-2 frames to the main router, like they would for any target outside the 192.168.1.0/24
subnet. The router will then send this to the Pi. Make sure you set up your router accordingly.
Now, on to adding the route on our Raspberry. It’s as easy as this:
sudo route add -net 10.0.0.0/24 dev wlan0
Essentially we’re saying: anything that goes to our new subnet needs to be routed out through the WiFi interface. By default, there’s already a route for everything else through the Ethernet interface, so traffic coming back from the IP cameras will get routed in the other direction without additional settings. Those 2 lines in the route
output confirm this:
sudo route |grep " eth0" # grep just to filter out the 2 lines I want to highlight
# default 192.168.1.1 0.0.0.0 UG 202 0 0 eth0
# 192.168.1.0 0.0.0.0 255.255.255.0 U 202 0 0 eth0
Finally we’re ready for the plat de rรฉsistance: setting up iptables
rules! Here’s the policy we want:
- drop any forward packet by default
- but accept the forwarding from eth0 to wlan0
- and accept the forwarding from wlan0 to eth0… only if it’s meant for the local LAN!
So concretely, we want this sequence of commands:
# general rule
iptables -P FORWARD DROP
# allow that way
iptables -A FORWARD -i eth0 -o wlan0 -j ACCEPT
# allow the other way but on the one condition
iptables -A FORWARD -o eth0 -i wlan0 -d 192.168.1.0/24 -j ACCEPT
If we wanted to fully open communication, which is going to be useful for the initial setup of the cameras, for example, we could change that last line for:
# allow the other way unconditionally
iptables -A FORWARD -o eth0 -i wlan0 -j ACCEPT
And that’s it. If you’ve followed all the steps, you should now be able to “jail” your cameras, while retaining full bidirectional access from your LAN. Test it by connecting a phone to the hotspot!
iptables
commands are lost upon reboot. Now we finally tackle this.
Writing a script to turn internet on/off for the camera
We’ve configured and enabled a few services that will keep running after a reboot. But we’ve encountered a few commands that don’t make changes permanent:
- adding the IP address
- adding the route from eth0 to wlan0
- setting
iptables
rules
To fix that, we’re going to write a short script that will take care of setting things up as needed. And we’ll then create a service that will start after each boot to run said script.
Here goes the full script. Store it where you want; I’ll assume /root/wifirouter.sh later on.
#!/usr/bin/bash
undosetup () {
iptables -D FORWARD -o eth0 -i wlan0 -j ACCEPT 2>/dev/null
iptables -D FORWARD -i eth0 -o wlan0 -j ACCEPT 2>/dev/null
iptables -D FORWARD -o eth0 -i wlan0 -d 192.168.1.0/24 -j ACCEPT 2>/dev/null
route del -net 10.0.0.0/24
}
setupbasic () {
ip addr add 10.0.0.1/24 dev wlan0
iptables -A FORWARD -i eth0 -o wlan0 -j ACCEPT 2>/dev/null
iptables -A FORWARD -o eth0 -i wlan0 -d 192.168.1.0/24 -j ACCEPT 2>/dev/null
iptables -P FORWARD DROP 2>/dev/null
route add -net 10.0.0.0/24 dev wlan0
}
setupfull () {
ip addr add 10.0.0.1/24 dev wlan0
iptables -A FORWARD -o eth0 -i wlan0 -j ACCEPT 2>/dev/null
iptables -A FORWARD -i eth0 -o wlan0 -j ACCEPT 2>/dev/null
iptables -P FORWARD DROP 2>/dev/null
route add -net 10.0.0.0/24 dev wlan0
}
if [ "$1" = "undo" ]; then
echo Unsetting packet forwarding for wifi point
undosetup
exit 0
fi
if [ "$1" = "block" ]; then
echo Setting up iptable for blocking access to internet
undosetup
setupbasic
exit 0
fi
if [ "$1" = "allow" ]; then
echo Setting up iptable for allowing access to internet
undosetup
setupfull
exit 0
fi
echo Use one of these options:
echo " undo"
echo " block"
echo " allow"
exit 1
If I’m being honest, this can certainly be written more elegantly. But it works for me. I’m defining 3 functions which I call based on the arguments the script is run with (block
which will be our startup setting, allow
which will enable cameras to access internet, and undo
which cancels the whole settings).
I’m deleting the rules and adding new ones back when setting up, so that we can call the script several times without ending up with conflicting rules.
Now, we’ll just create a systemd
service file to run this at startup. Create /etc/systemd/system/wifirouter.service
with the following content:
[Unit]
Description=Restore iptables rules
Wants=network-online.target
After=network-online.target hostapd.service
Before=dnsmasq.service # this script will set up the IP that dnsmasq needs to start properly
[Service]
Type=oneshot
ExecStart=/root/wifirouter.sh block
[Install]
WantedBy=multi-user.target
This is a one-shot service: it will run once and be happy with itself until the next boot, which is what we want.
Also, we want systemd
to run this after the WiFi hotspot is active, but before the DHCP server is running. This is because our script sets up the IP address for the wlan0
interface, and we need that IP for dnsmasq
to start properly.
With that file save we can enable it:
sudo systemctl daemon-reload
sudo systemctl enable wifirouter
Use enable --now
if you’ve skipped the manual configuration of IP, route and iptables
in previous steps.
We’re done. You can reboot if you want to check everything still works, and you should be able to connect your IP cameras after running sudo /root/wifirouter.sh allow
, and then sudo /root/wifirouter.sh block
once you’re done.
The Ansible version
The Ansible version of everything that precedes is as follows:
# generated by chatgpt! needs customisation
- name: Install required packages
apt:
name:
- dnsmasq
- hostapd
- iptables
tags: packages
- name: Create dnsmasq config
template:
src: templates/dnsmasq.conf.j2
dest: /etc/dnsmasq.conf
register: dnsmasq
tags: config
- name: Create hostapd config
template:
src: templates/hostapd.conf.j2
dest: /etc/hostapd/hostapd.conf
register: hostapd
tags: config
- name: unblock wifi
command: rfkill unblock 0
tags: config
- name: set up wlan ip address
blockinfile:
path: /etc/dhcpcd.conf
block: |
interface wlan0
nohook wpa_supplicant
tags: config
- name: Enable dnsmasq and hostapd services
service:
name: "{{ item }}"
state: "{% if hostapd.changed or dnsmasq.changed %}re{% endif %}started"
masked: no
enabled: yes
with_items:
- dnsmasq
- hostapd
tags: service
- name: Enable IP forwarding
sysctl:
name: net.ipv4.ip_forward
value: 1
state: present
tags: fwding
- name: Set up iptables rules script
template:
src: "templates/wifirouter.sh.j2"
dest: "/root/wifirouter.sh"
mode: "0755"
tags: iptables,script
- name: Create wifirouter service
template:
src: templates/wifirouter.service.j2
dest: /etc/systemd/system/wifirouter.service
register: serv
tags: iptables
- name: Enable iptables restore service
service:
name: wifirouter
enabled: yes
state: started
daemon_reload: "{% if serv.changed %}yes{% else %}no{% endif %}"
tags: iptables
copy
instead of template
.
For the content of the files that are copied refer to the previous sections.
Conclusion
So, is it worth it? To be honest I started doing this without knowing for sure if my cameras would keep working locally if I put them in jail like this.
It’s only been a few days, but for now they do. I’ve noticed that if I open the app immediately after setting up the jail, it searches for the cameras and complains, but then stops immediately if I release the cameras from their jail. However after more time (hours), the app struggles to locate the cameras again even after opening up the gates, for a minute or 2, and only then finds them again. But so far I haven’t lost access via the app overall, and the cameras keep working.
Streaming and activation of motion detection from Home Assistant keeps working perfectly when the camera is in jail.
We’ll have to see within a few weeks if the camera ends up giving up at some point, but things are looking good so far.
Join the conversation on this article over there !