アクセス制限がけしからん「無料版 SoftEther」を firewalld と knock とその他諸々を使ってセキュリティを気休め程度に良くする会

# はじめに
このネタは自宅の Ubuntu 鯖に [SoftEther のパッケージ](https://code.launchpad.net/~paskal-07/+archive/ubuntu/softethervpn)を使っていますが「アクセス制限が無料版だと使えない…」[^1]と思い Qiita はじめ色んなサイトを見てなんとなく浮かんだ「firewalld と knock を使った方法」をまとめたものです。
と言っても、ほぼほぼ「あぁどっかで見たわー」って感じの内容がほとんどなので自分がやった部分だけ(多少はぼかしたり設定値を変えていますが)大まかに書かせていただきます。
~~あ、いつものように2021年の自分の誕生日ネタですｗ~~

# 下準備
## 準備するもの
* firewalld
* knock
* swatch
* incron

主要な Linux ディストリビューションだったら YUM(DNF) や apt などを用いてインストールするだけで済みます。

## knock
```ini:/etc/knockd.conf
[options]
	UseSyslog

[openOVPN]
	sequence    = 61615:udp,55505:udp,49357:udp
	command     = /home/vpn/bin/openOVPN  %IP%
	seq_timeout = 5
	tcpflags    = syn

[closeOVPN]
	sequence    = 55505:udp,49357:udp,61615:udp
	command     = /home/vpn/bin/closeOVPN %IP%
	seq_timeout = 5
	tcpflags    = syn
```
knock するためのポートは「[乱数メーカー](http://mpnets.net/rand/)」というサイトで範囲を49152から65535までの番号を生成して設定に埋め込んでいます。

## firewalld
```xml:/etc/firewalld/zones/public.xml
…
  <rule family="ipv4">
    <source ipset="vpn"/>
    <service name="openvpn"/>
    <accept/>
  </rule>
…
```
ファイアウォールのルールは ipset のリストにして読み込むようにしています。~~他のルールもあったので必要な部分以外は割愛しました。~~

## swatch / incron
```text:/home/vpn/conf/swatch.conf
# Connect
watchfor /\[HUB.* Session .*: VPN Client details:/
	pipe /home/vpn/bin/swatch_action 'connect'

# Disconnect
watchfor /Session deleted./
	pipe /home/vpn/bin/swatch_action 'disconnect'

# authentication failed(3/60)
watchfor /\[HUB.* Connection .*: User authentication failed/
	pipe /home/vpn/bin/swatch_action 'fail'
	threshold track_by=/\[HUB.* Connection .*: User authentication failed/,type=both,count=3,seconds=60
```
```text:/var/spool/incron/root
/var/log/softether/server_log IN_CREATE /usr/bin/systemctl restart vpnwatch
```
本来入れる予定はありませんでしたが、~~色々検索したらログ解析で切断したあとの処理があったら面白そうだったので~~急遽入れることにしました。

# 実装
## knock
```bash:/home/vpn/bin/openOVPN
#!/bin/bash

if [ $# -ne 1 ]; then
	echo "USAGE $0 IP_Address"
	exit 99999
fi
IP=$1

in_entry=`firewall-cmd --permanent --ipset=vpn --get-entries|grep $IP`
if [ "$IP" == "$in_entry" ]; then
	echo "this IP($IP) is opened."
	exit 1
fi

firewall-cmd --quiet --permanent --ipset=vpn --add-entry=$IP
#firewall-cmd --quiet --permanent --ipset=docomo --add-entries-from-file=/home/vpn/share/spmode.lst 
firewall-cmd --quiet --reload

exit 0
```
```bash:/home/vpn/bin/closeOVPN
#!/bin/bash

if [ $# -ne 1 ]; then
	echo "USAGE $0 IP_Address"
	exit 1
fi

IP=$1

in_entry=`firewall-cmd --permanent --ipset=vpn --get-entries|grep $IP`
if [ "$IP" != "$in_entry" ]; then
	echo "this IP($IP) is not found."
	exit 1
fi

firewall-cmd --quiet --permanent --ipset=vpn --remove-entry=$IP
firewall-cmd --quiet --reload

exit 0
```

## swatch
```bash:/home/vpn/bin/swatch_action
#!/bin/bash

if [ $# -ne 1 ]; then
	echo "USAGE: $0 mode{connect|disconnect|fall}"
	exit 1
fi
MODE=$1
RET=0

read LOG
LOG=`echo $LOG | /home/vpn/bin/extractIP`
IPADDR=`echo $LOG|cut -d : -f 1`
USERNAME=`echo $LOG|cut -d : -f 2`

if [ "$MODE" == "disconnect" ]; then
	/home/vpn/bin/closeOVPN $IPADDR
	RET=$?
fi

exit $RET
```
```perl:/home/vpn/bin/extractIP
#!/usr/bin/perl --



my $log = "";
foreach my $line(<STDIN>){
	$log .= $line;
}
#print $ARGV[0]." - $log";

my $ip_address;
my $username;

if($log =~ /Client IP address: "(\S+)"/){
	$ip_address = $&;
	$ip_address =~ s|^Client IP address: "(\S+)"|\1|;
}

if($log =~ /\[(\w+)\] (\S+):(\d+) -> (\S+):(\d+) \((\w+)\): Session deleted./){
	$ip_address = $&;
	$ip_address =~ s|^\[(\w+)\] (\S+):(\d+) -> (\S+):(\d+) \((\w+)\): Session deleted.|\2|;
}

if($log =~ /User authentication failed. The user name that has been provided was "(\S+)", from (\S+)\./){
	$ip_address = $&;
	$ip_address =~ s|User authentication failed. The user name that has been provided was "(\S+)", from (\S+)\.|\2|;
	$username   = $&;
	$username   =~ s|User authentication failed. The user name that has been provided was "(\S+)", from (\S+)\.|\1|;
}

print "$ip_address:$username\n";
```
```bash:/home/vpn/bin/vpnwatch
#!/bin/bash

name=vpnwatch
SWATCH_DIR=/home/vpn
SWATCH_CONF=${SWATCH_DIR}/conf/${name}.conf
PID_FILE=/var/run/${name}.pid

LOG_DIR=/var/log/softether/server_log
LOG_FILE=${LOG_DIR}/vpn_`date '+%Y%m%d'`.log

# ログファイルが存在しなければ監視しない
if [ ! -e ${LOG_FILE} ]; then
	echo "not found logfile : ${LOG_FILE}"
	exit 1
fi

start() {
	# Start daemons.
	ls ${PID_FILE} > /dev/null 2>&1
	if [ $? -eq 0 ]; then
		echo "${name} is already started"
		return 0
	fi

	echo "Starting ${name}."
	/usr/bin/swatch  \
		--config-file ${SWATCH_CONF} \
		--script-dir  /tmp \
		--pid-file    ${PID_FILE} \
		--tail-file   ${LOG_FILE} \
		--daemon
	RETVAL=$?
	return $RETVAL
}

stop() {
	# Stop daemons.
	ls ${PID_FILE} > /dev/null 2>&1
	if [ $? -ne 0 ]; then
		echo "${name} is not running"
		return 1
	fi

	echo "Shutting down ${name}"
	kill $(cat ${PID_FILE})
	rm -f ${PID_FILE}
	return 0
}

case "$1" in
	start)
		start
		;;
	stop)
		stop
		;;
	restart)
		stop
		start
		;;
	reload)
		stop
		start
		;;
	*)
		echo "Usage: $0 {start|stop|restart}"
		exit 1
esac

exit 0
```
```systemd:/home/vpn/conf/vpnwatch.service
[Unit]
Description=vpnwatch
After=network.target
After=softether-vpnserver.service

[Service]
Type=forking
ExecStart=/home/vpn/bin/vpnwatch start
ExecStop=/home/vpn/bin/vpnwatch stop
ExecReload=/home/vpn/bin/vpnwatch reload

[Install]
WantedBy=multi-user.target
```
実装としては書いてあるとおりの他にポートの開け閉めのイベントが発生したら Slack などに書き込まれるような仕組みを仕込んでおこうと企んでいます。
一応 IPv4 なアドレスで動作はしています。~~が、IPv6 なアドレスはポートを開ける予定がまったくないので試していませんが多分いけそうな気がするのは（ｒｙ~~

# 組み込み
と書きましたが、どのサービスでも同じように↑の service ファイルを systemd に読み込ませて各サービスを有効にしてスタートさせたり incron のジョブを読み込ませるだけなので細かいことは Google 先生に聞いてみましょう（ぉぃ

# 確認
スマホ回線のテザリングや MAP-E などの線から knock して VPN 接続できるか確認します。
ちなみに自分は「[KnockOnD](https://apps.apple.com/jp/app/id333206277)」という iOS アプリを使って動作するか確認しました。

# おまけ
ログを syslog に落とすのに機能制限かけられているので「何かしらの方法を取らなきゃなのか…」という顔をしたのは秘密です。
あとは「VPN Client details」ログにユーザー ID を入れてくれたら「誰々が仮想ハブのxxxxxに〜〜」みたいなことを Slack にポスト出来たら面白いなぁと思ったのは（ｒｙ


[^1]: 色々検索すると「ソースコードをいじってリビルドするやり方」や「環境変数でごまかす方法」があるらしいようですが今回はあえてその方法は取りませんでした。