ラズパイとTailscaleで自宅見守りカメラ

## この記事について

ラズパイ5とTailscaleで自宅リビング見守りカメラを更新しました。

スマホのRealVNCからTailscale経由で自宅のラズパイカメラ映像を見るお手軽Cam serverです。
* OSがBookwermになって変化点が多かった
* AnyDeskが使いにくくなって(有償化含めて)Tailscaleに乗り換えた

のでメモを残しておきたいと思います。

Raspberry Pi Connectも試しましたが現時点ではRealVNCとTailscaleの組合せを選択しました。

## 環境  

### RaspberryPi  

```sh: cat /proc/cpuinfo
Revision        : c04170
Serial          : xxxxxxxxxxxxxxxx
Model           : Raspberry Pi 5 Model B Rev 1.0
```
### RaspberryPi OS Imager

* Device：RASPBERRY PI 5  
* OS：RASPBERRY PI OS(64-BIT)  
* Storage：microSDカード32GB  
* Wi-Fi使用  
* 公開鍵認証のみ使用でSSH設定  

### OS バージョン
```sh: cat /proc/version
Linux version 6.6.31+rpt-rpi-2712 (serge@raspberrypi.com)
(gcc-12 (Debian 12.2.0-14) 12.2.0, 
GNU ld (GNU Binutils for Debian) 2.40) 
#1 SMP PREEMPT Debian 1:6.6.31-1+rpt1 (2024-05-29)
```

## 構築

今回は公開鍵認証とLinux firewallのみの普段使い用のライトな設定となります。
ラズパイのuser directoryに以下のプロジェクトをWinSCP等でコピーしてshを実行してゆきます。

ファイルコピー後にシェルスクリプトを順番に実行することで環境を構築できます。

```
user
 ┣ staticip.conf
 ┣ ini
 ┃ ┣ set_nm.sh
 ┃ ┗ set_pipconf.sh
 ┣ env
 ┃ ┣ environment.sh
 ┃ ┣ set_nft.sh
 ┃ ┣ set_postfix.sh
 ┃ ┗ set_ssh.sh
 ┣ cam
 ┃ ┣ cam_prov.sh
 ┃ ┗ cam_server.py
 ┣ protect
 ┃ ┣ nonprotect
 ┃ ┗ protect
 ┣ ini_ip.sh
 ┣ env.sh
 ┣ cam.sh
 ┣ vnc.sh
 ┗ protect.sh
```

### NetworkManager

NetworkManagerで固定化するipを事前にconfにまとめておきます。
```sh:staticip.conf
MYIP_WLAN0=192.168.1.111
WLAN0_ROUTER=192.168.1.1
MY_NAME_SERVER=192.168.1.1
MYIP_LOCALETH=169.254.1.111
```

wlan0とeth0の接続設定でip addressを固定します。

```sh:ini_ip.sh
#!/bin/bash

sudo cp -r ./ini/* ~/
sudo chown $USER:$USER *.*
sudo chmod u+x *.sh
sudo chown $USER:$USER *.*

./set_pipconf.sh
./set_nm.sh

sudo reboot
```
作ったら放置するのでvenv使わずにズルします()
```sh:set_pipconf.sh
#!/bin/bash
mkdir ~/.pip
echo "[global]" | tee -a ~/.pip/pip.conf
echo "break-system-packages = true" | tee -a ~/.pip/pip.conf
```
本記事ではwlan0をインターネットに、eth0をリンクローカルに固定ipで接続します。
```sh:set_nm.sh
#!/bin/bash

####################################################################################
# wlan0をパブリック接続でip固定
####################################################################################
CON_NAME="w0-inet"
# 現在のワイヤレスデバイスを識別
WIFI_DEV=$(iw dev | grep Interface | awk '{print $2}')
# 接続中のSSIDを取得
ESSID=$(iw dev "$WIFI_DEV" link | grep -oP 'SSID: \K.+')

# /etc/NetworkManager/system-connections/preconfigured.nmconnectionの存在をチェック
if [ -f "/etc/NetworkManager/system-connections/preconfigured.nmconnection" ]; then
    # SSIDとPSKを抽出
    SSID=$(sudo grep ssid= /etc/NetworkManager/system-connections/preconfigured.nmconnection | awk -F'=' '{print $2}' | sed -e "s/[\r\n]\+//g")
    PSK=$(sudo grep psk= /etc/NetworkManager/system-connections/preconfigured.nmconnection | awk -F'=' '{print $2}' | sed -e "s/[\r\n]\+//g")
    echo "preconfigured exist"
# /etc/NetworkManager/system-connections/$ESSID.nmconnectionの存在をチェック
else
    if [ -f "/etc/NetworkManager/system-connections/$ESSID.nmconnection" ]; then
    # SSIDとPSKを抽出
        SSID=$(sudo awk -F= '/^ssid=/ {print $2}' /etc/NetworkManager/system-connections/$ESSID.nmconnection)
        PSK=$(sudo awk -F= '/^psk=/ {print $2}' /etc/NetworkManager/system-connections/$ESSID.nmconnection)
    fi
    echo "$ESSID exist"
fi

echo "SSID: $SSID"
echo "PSK: $PSK"
STATIC_IP="$(cat ./staticip.conf | grep MYIP_WLAN0 | awk -F'=' '{print echo $2}' | sed -e "s/[\r\n]\+//g")/24"
STATIC_ROUTE="$(cat ./staticip.conf | grep WLAN0_ROUTER | awk -F'=' '{print echo $2}' | sed -e "s/[\r\n]\+//g")"
STATIC_DNS="$(cat ./staticip.conf | grep MY_NAME_SERVER | awk -F'=' '{print echo $2}' | sed -e "s/[\r\n]\+//g")"

sudo nmcli connection add con-name $CON_NAME \
type wifi \
ifname wlan0 \
connection.autoconnect yes \
wifi.mode infrastructure \
wifi.ssid $SSID \
802-11-wireless-security.auth-alg open \
802-11-wireless-security.key-mgmt wpa-psk \
802-11-wireless-security.psk $PSK \
ipv4.addresses $STATIC_IP \
ipv4.gateway $STATIC_ROUTE \
ipv4.method manual \
ipv4.dns "$STATIC_DNS" \
ipv6.method disabled

####################################################################################
# eth0をリンクローカルでip固定
####################################################################################
CON_NAME_ETH0="e0-lo"
STATIC_IP_ETH0="$(cat ./staticip.conf | grep MYIP_LOCALETH | awk -F'=' '{print echo $2}' | sed -e "s/[\r\n]\+//g")/16"

sudo nmcli connection add con-name $CON_NAME_ETH0 \
type ethernet \
ifname eth0 \
connection.autoconnect yes \
ipv4.addresses $STATIC_IP_ETH0 \
ipv4.method manual \
ipv6.method disabled

sudo nmcli c reload
sudo nmcli c up w0-inet
sudo nmcli c up e0-lo

####################################################################################
# 自動認識の接続設定を削除
####################################################################################
if [ -f "/etc/NetworkManager/system-connections/preconfigured.nmconnection" ]; then
    sudo nmcli c delete preconfigured
fi
if [ -f "/etc/NetworkManager/system-connections/$ESSID.nmconnection" ]; then
    sudo nmcli c delete $ESSID
fi
sudo nmcli c delete Wired\ connection\ 1
sudo nmcli c delete Wired\ connection\ 2

sudo reboot
```

### アップデート/パッケージ導入

```sh:env.sh
#!/bin/bash

sudo cp -r ./env/* ~/
sudo chown $USER:$USER *.*
sudo chmod u+x *.sh
sudo chown $USER:$USER *.*

./environment.sh

sudo reboot
```
Live Cam用途に不要なものが沢山入ってますがそのままのせておきます。

```sh:environment.sh
#!/bin/bash

sudo apt update  
sudo apt -y upgrade  

sudo apt install -y python3-dev python3-pip git unzip
sudo python3 -m pip install --upgrade pip setuptools wheel typing-extensions

echo iptables-persistent iptables-persistent/autosave_v4 boolean true | sudo debconf-set-selections
echo iptables-persistent iptables-persistent/autosave_v6 boolean false | sudo debconf-set-selections
sudo apt install -y iptables iptables-persistent

./set_postfix.sh

sudo apt install -y libgl1-mesa-glx
sudo apt install libatlas-base-dev libjasper-dev -y
sudo apt install ffmpeg libcanberra-gtk3-module v4l-utils qv4l2 -y
sudo apt install -y fswebcam
pip3 install numpy --upgrade
pip3 install opencv-python --upgrade
pip3 install pillow
pip3 install scikit-image
pip3 install torch
pip3 install torchvision
pip3 install matplotlib
pip3 install keyboard
pip3 install openpyxl
pip3 install pytk
sudo apt install -y libxcb-xinerama0 libxkbcommon-x11-0 libx11-xcb1 libx11-xcb-dev libglu1-mesa-dev --reinstall

cd /usr/bin
sudo rm python
sudo ln -s python3 python
cd
```
cronを使うときのためにpostfixをインストールします。
```sh:set_postfix.sh
#!/bin/sh

debian_install_postfix()
{
  cat <<EOF | sudo debconf-set-selections
postfix postfix/main_mailer_type select No configuration
EOF
  sudo apt install -y postfix

  # shellcheck disable=SC2016
  cat <<EOF | sudo tee /etc/postfix/main.cf
myhostname = ${1}
mydomain = ${2}
myorigin = \$myhostname.\$mydomain
mydestination = localhost, localhost.\$mydomain, \$myhostname, \$mydomain, \$myorigin
compatibility_level = 2
command_directory = /usr/sbin
daemon_directory = /usr/lib/postfix/sbin
data_directory = /var/lib/postfix
mail_owner = postfix
inet_interfaces = all
local_recipient_maps = unix:passwd.byname \$alias_maps
unknown_local_recipient_reject_code = 550
mynetworks_style = subnet
mynetworks = 127.0.0.0/8
alias_maps = hash:/etc/aliases
alias_database = hash:/etc/aliases
smtpd_banner = \$myhostname ESMTP \$mail_name (Debian/GNU)
debugger_command =
         PATH=/bin:/usr/bin:/usr/local/bin:/usr/X11R6/bin
         ddd \$daemon_directory/\$process_name \$process_id & sleep 5
sendmail_path = /usr/sbin/sendmail
newaliases_path = /usr/bin/newaliases
mailq_path = /usr/bin/mailq
setgid_group = postdrop
inet_protocols = ipv4
EOF
  sudo newaliases
  sudo systemctl restart postfix
  sudo apt install -y mutt
}

debian_main()
{
  debian_install_postfix $(uname -n) localdomain 
}

debian_main
```
### Cam Server

```sh:cam.sh
#!/bin/bash

sudo chown $USER:$USER *.*
sudo chmod u+x ./cam/*.sh
sudo chown $USER:$USER ./cam/ ./cam/*

cd cam
./cam_prov.sh
cd
```
ラズパイを６時間おきに再起動させます。
Cam Serverはsystemdに登録します。
```sh:cam_prov.sh
#!/bin/bash

echo "59 23 * * * sudo reboot" | tee -a cron_mod.conf
echo "59 5 * * * sudo reboot" | tee -a cron_mod.conf
echo "59 11 * * * sudo reboot" | tee -a cron_mod.conf
echo "59 17 * * * sudo reboot" | tee -a cron_mod.conf
echo "" | tee -a cron_mod.conf

crontab -r
crontab ./cron_mod.conf

echo \[Unit\] | tee cam_server.service
echo Description=Run Camera Script at Startup | tee -a cam_server.service 
echo After=network.target | tee -a cam_server.service
echo "" | tee -a cam_server.service
echo \[Service\] | tee -a cam_server.service
echo Type=simple | tee -a cam_server.service
echo User=$USER | tee -a cam_server.service
echo Environment=\"DISPLAY=:0\" | tee -a cam_server.service
echo ExecStartPre=/bin/sleep 5 | tee -a cam_server.service
echo ExecStart=/usr/bin/python3 /home/$USER/cam/cam_server.py | tee -a cam_server.service
echo Restart=on-failure | tee -a cam_server.service
echo "" | tee -a cam_server.service
echo \[Install\] | tee -a cam_server.service
echo WantedBy=multi-user.target | tee -a cam_server.service

sudo cp cam_server.service /etc/systemd/system

sudo systemctl enable cam_server.service
sudo systemctl start cam_server.service
sudo systemctl status cam_server.service
```
PythonでシンプルなCam serverを立てます。
```python:/home/$USER/cam/cam_server.py
# -*- coding: utf-8 -*-
import cv2
import subprocess
import time
import datetime


time.sleep(15)

# VideoCaptureオブジェクト
DEVICE_ID = -1
cap = cv2.VideoCapture(DEVICE_ID)

if True==cap.isOpened():
    #解像度/フレームレート
    WIDTH = 1920
    HEIGHT = 1080
    FPS = 30
else:
    time.sleep(90)
    subprocess.call(["sudo","reboot"])

def main():
    # フォーマット・解像度・FPSの設定
    cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M','J','P','G'))
    cap.set(cv2.CAP_PROP_FRAME_WIDTH, WIDTH)
    cap.set(cv2.CAP_PROP_FRAME_HEIGHT, HEIGHT)
    cap.set(cv2.CAP_PROP_FPS, FPS)

    while True:
        # カメラ画像取得
        _, frame = cap.read()
        if(frame is None):
            continue
        # 画像表示
        now = datetime.datetime.now()
        cv2.putText(frame, now.strftime('%Y/%m/%d/%H:%M:%S'), (1150, 1020), cv2.FONT_HERSHEY_SIMPLEX, 2, (255, 255, 255), 1, cv2.LINE_AA)
        cv2.imshow('Live Cam1', frame)
        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
    
    # VideoCaptureオブジェクト破棄
    cap.release()
    cv2.destroyAllWindows()

if __name__ == '__main__':
    time.sleep(15)
    main()
```

### VNC

執筆時点(2024.6.11)ではこれだけで設定出来るようになっています。

```sh:vnc.sh
#!/bin/bash

# waylandのバージョン確認
wayvnc --version
# vncを有効化 (0=有効, 1無効)
sudo raspi-config nonint do_vnc 0
# 証明書の生成
sudo ssh-keygen -y -m pem -f $HOME/.config/wayvnc/rsa_key.pem -t rsa -N ""

sudo reboot
```
### FW設定/SSH port変更/microSDカードROM化設定

ROM化用のスクリプトを仕込みます。Desktopにプレインストール済みのブラウザーは利用できなくしておきます。

```sh:protect.sh
#!/bin/bash

sudo cp ~/protect/protect /bin
sudo cp ~/protect/nonprotect /bin

sudo chmod u+x /bin/protect /bin/nonprotect
sudo chown root:root /bin/protect /bin/nonprotect

./set_nft.sh
./set_ssh.sh

sudo apt remove -y --auto-remove --purge chromium-browser
sudo apt autoremove -y
sudo apt clean
sudo apt update && sudo apt upgrade -y

sudo rm ~/*.sh

sudo rm -r ./env
sudo rm -r ./ini
sudo rm -r ./protect
sudo rm .bash_history

sudo protect
```
ちょっとユルイですが汗
AWS IoTとつなげたくなった時用に8883とlocalの1883を開けておきます。
```sh:set_nft.sh
#!/bin/bash

sudo cp /etc/iptables/rules.v4 /etc/iptables/rules.v4.bak
sudo rm /etc/iptables/rules.v4

STATIC_IP=$(ip addr show wlan0 | grep "inet " | awk '{print $2}' | cut -d/ -f1)
cidr=$(ip addr show wlan0 | grep "inet " | awk '{print $2}' | cut -d/ -f1 | awk -F'.' '{print $1"."$2"."$3".0"}')

sudo iptables -F
sudo iptables -X
sudo iptables -P INPUT DROP
sudo iptables -P FORWARD DROP
sudo iptables -P OUTPUT ACCEPT
sudo iptables -A INPUT -i lo -j ACCEPT
sudo iptables -A INPUT -d 127.0.0.0/8 ! -i lo -j REJECT --reject-with icmp-port-unreachable
sudo iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
sudo iptables -A INPUT -p tcp -m tcp --dport 8883 -j ACCEPT
sudo iptables -A INPUT -s 127.0.0.0/8 -p tcp -m tcp --dport 1883 -j ACCEPT
sudo iptables -A INPUT -s ${cidr}/24 -p tcp -m tcp --dport 1883 -j ACCEPT
sudo iptables -A INPUT -s ${cidr}/24 -p udp -m udp --dport 53 -j ACCEPT
sudo iptables -A INPUT -s 169.254.0.0/16 -p udp -m udp --dport 53 -j ACCEPT
sudo iptables -A INPUT -s ${cidr}/24 -p udp -m udp --dport 123 -j ACCEPT
sudo iptables -A INPUT -s ${cidr}/24 -p udp -m udp --dport 5353 -j ACCEPT
sudo iptables -A INPUT -s 169.254.0.0/16 -p udp -m udp --dport 5353 -j ACCEPT
sudo iptables -A INPUT -s ${cidr}/24 -p tcp -m tcp --dport 5900 -j ACCEPT
sudo iptables -A INPUT -s 169.254.0.0/16 -p tcp -m tcp --dport 5900 -j ACCEPT
sudo iptables -A INPUT -s ${cidr}/24 -p udp -m udp --dport 5900 -j ACCEPT
sudo iptables -A INPUT -s 169.254.0.0/16 -p udp -m udp --dport 5900 -j ACCEPT
if [ '$STATIC_IP | grep "192.168"' ]; then
    sudo iptables -A INPUT -s ${cidr}/24 -p tcp -m state --state NEW -m tcp --dport 62000 --tcp-flags FIN,SYN,RST,ACK SYN -m hashlimit --hashlimit-upto 1/min --hashlimit-burst 10 --hashlimit-mode srcip --hashlimit-name t_sshd --hashlimit-htable-expire 120000 -j ACCEPT
fi
sudo iptables -A INPUT -s 169.254.0.0/16 -p tcp -m state --state NEW -m tcp --dport 62000 --tcp-flags FIN,SYN,RST,ACK SYN -m hashlimit --hashlimit-upto 1/min --hashlimit-burst 10 --hashlimit-mode srcip --hashlimit-name t_sshd --hashlimit-htable-expire 120000 -j ACCEPT
sudo iptables -A INPUT -s ${cidr}/24 -p icmp -m icmp --icmp-type 8 -j ACCEPT
sudo iptables -A INPUT -s 192.168.0.0/16 -p icmp -m icmp --icmp-type 8 -j ACCEPT
sudo iptables -A INPUT -s 169.254.0.0/16 -p icmp -m icmp --icmp-type 8 -j ACCEPT
sudo iptables -A INPUT -m limit --limit 5/min -j LOG --log-prefix "iptables denied: " --log-level 7
sudo iptables -A INPUT -j REJECT --reject-with icmp-port-unreachable

sudo service netfilter-persistent reload
sudo service netfilter-persistent save
sudo sh -c "iptables-save >/etc/iptables.ipv4.filter"
sudo iptables -L
```
気休めにSSH portを変更します。
```sh:set_ssh.sh
#!/bin/bash

sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
sudo sed -i "s/#Port 22/Port 62000/g" /etc/ssh/sshd_config
sudo sed -i "s/#PubkeyAuthentication yes/PubkeyAuthentication yes/g" /etc/ssh/sshd_config
```
ROM化の設定/解除に[root-ro](https://github.com/josepsanzcamp/root-ro)使わなくなり久しいですね(大変お世話になりました)。

```sh:protect (ROM化)
#!/bin/sh

sudo raspi-config nonint enable_overlayfs
sudo reboot
```
```sh:nonnprotect (ROM化解除)
#!/bin/sh

sudo raspi-config nonint disable_overlayfs
sudo reboot
```

### Tailscale

#### Tailscaleアカウント作成
[tailscaleの公式サイト](https://tailscale.com/)にアクセスしてサインアップします。Apple、Google、Microsoft、GitHub、Okta等のSSO ID プロバイダーアカウントが必要です。

#### ラズパイにTailscaleインストール
2024.6.11現在、Bookwermにも[これだけ](https://tailscale.com/download/linux)でインストールできます。
```sh
curl -fsSL https://tailscale.com/install.sh | sh
```
インストール後の呪文
```sh
sudo tailscale up
```
tailscale upコマンドを実行するとURLが発行されます。自分のTailscaleアカウントでSSO loginするとラズパイがTailscaleアカウントに登録されます。

<img width= 500 src = https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565396/c5897b67-5af7-c772-1bcc-1f8880b35364.png>


## 動作確認

スマホにTailscaleとRealVNCのアプリをインストールしてライブ映像を確認できました。WireGuardを使っているだけあって流石に軽いですね。設定も楽過ぎてもうopenVPNや素のWireGuardに戻れません。。。

<img width= 700 src = https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565396/36e8d79c-e447-6161-4e7d-8963f0aaee20.jpeg>

構築手順のメモは以上となります。