はじめに
このネタは自宅の Ubuntu 鯖に SoftEther のパッケージを使っていますが「アクセス制限が無料版だと使えない…」1と思い Qiita はじめ色んなサイトを見てなんとなく浮かんだ「firewalld と knock を使った方法」をまとめたものです。
と言っても、ほぼほぼ「あぁどっかで見たわー」って感じの内容がほとんどなので自分がやった部分だけ(多少はぼかしたり設定値を変えていますが)大まかに書かせていただきます。
あ、いつものように2021年の自分の誕生日ネタですw
下準備
準備するもの
- firewalld
- knock
- swatch
- incron
主要な Linux ディストリビューションだったら YUM(DNF) や apt などを用いてインストールするだけで済みます。
knock
[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 するためのポートは「乱数メーカー」というサイトで範囲を49152から65535までの番号を生成して設定に埋め込んでいます。
firewalld
…
<rule family="ipv4">
<source ipset="vpn"/>
<service name="openvpn"/>
<accept/>
</rule>
…
ファイアウォールのルールは ipset のリストにして読み込むようにしています。他のルールもあったので必要な部分以外は割愛しました。
swatch / incron
# 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
/var/log/softether/server_log IN_CREATE /usr/bin/systemctl restart vpnwatch
本来入れる予定はありませんでしたが、色々検索したらログ解析で切断したあとの処理があったら面白そうだったので急遽入れることにしました。
実装
knock
#!/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
#!/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
#!/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
#!/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";
#!/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
[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 なアドレスはポートを開ける予定がまったくないので試していませんが多分いけそうな気がするのは(ry
組み込み
と書きましたが、どのサービスでも同じように↑の service ファイルを systemd に読み込ませて各サービスを有効にしてスタートさせたり incron のジョブを読み込ませるだけなので細かいことは Google 先生に聞いてみましょう(ぉぃ
確認
スマホ回線のテザリングや MAP-E などの線から knock して VPN 接続できるか確認します。
ちなみに自分は「KnockOnD」という iOS アプリを使って動作するか確認しました。
おまけ
ログを syslog に落とすのに機能制限かけられているので「何かしらの方法を取らなきゃなのか…」という顔をしたのは秘密です。
あとは「VPN Client details」ログにユーザー ID を入れてくれたら「誰々が仮想ハブのxxxxxに〜〜」みたいなことを Slack にポスト出来たら面白いなぁと思ったのは(ry
-
色々検索すると「ソースコードをいじってリビルドするやり方」や「環境変数でごまかす方法」があるらしいようですが今回はあえてその方法は取りませんでした。 ↩