iptables VPN接続許可アドレスを追加するスクリプト

##背景
RaspberryPiにSoftEther VPN https://ja.softether.org/ を入れて、遠隔地からRaspberryPi配下のLANのNasneやブルーレイレコーダをDiXiMソフトで再生したり、RaspberryPiのSSDにローカルアクセスして便利に使っている。iOS/Android標準のVPN設定や、FireHDでOpenVPNconnectアプリなどサクッとVPN接続できるのはありがたい。

しかしVPNのポートを全世界に向け開けっぱなしにすると、SoftEtherのログファイルserver_log/vpn_YYYYMMDD.logに毎日数十万ラインのアクセスを試みる怪しいIPアドレスが出現する。iptablesの豊富な機能で抑え込む方法もあるかもしれないが、ここでは、アクセスを許すホワイトリストのIPアドレスにのみVPNのポートの接続を許可するbashスクリプトを用意することにした。

##前提
iptablesコマンドで、WAN側との管理が出来ている環境に、
iptables -A INPUT -p udp -m multiport --dports ポート -s IPアドレス -j ACCEPT
を追加して、IPアドレスからのポートを接続を許可させる。

遠隔地のIPアドレスは、https://ipinfo.io/what-is-my-ip からグローバルIPアドレスAA.BB.CC.DD(たいていはDynamicアドレス)を得る。また、このページの Connection > Route の AA.BB.0.0/NN 表示を採用するか、whois AA.BB.CC.DD からCIDR表記のIPアドレス範囲を得る。

##使い方例
AA.BB.0.0/NN 行を含む ipt_PROVIDER1.txt を用意する。
　但しAA.BB.CC.DD/32 のときは、/32を付けずに、
　　　AA.BB.CC.DD とする。

```shell-session
# cat /usr/local/IPT_DATA/ipt_PROVIDER1.txt
AA.BB.0.0/NN # comment
# ipt_ALLOW.sh -a ipt_PROVIDER1.txt と実行すると
1: iptables -A INPUT -p udp -m multiport --dports 500,1194,4500 -s AA.BB.0.0/NN -j ACCEPT
1: iptables -A INPUT -p tcp -m multiport --dports 443,992,1194,5555 -s AA.BB.0.0/NN -j ACCEPT
```

 ipt_PROVIDER1.txt  ipt_PROVIDER2.txt  ipt_PROVIDER3.txt とか
個別にポートを接続を許可のテスト確認ができたなら、ipt_addr.txt に集めておく。RaspberryPiが再起動された場合など、
`ipt_ALLOW.sh -a` と実行して設定の復帰が ipt_addr.txt から行われる。

##実行のイメージ

```shell-session
# egrep -v '^\s*#|^\s*$' /usr/local/IPT_DATA/ipt_addr.txt
AAA.BBB.0.0/16  # PROVIDER1
CC.DD.0.0/16    # PROVIDER2
EEE.FFF.GG.0/19 # PROVIDER3

# ipt_ALLOW.sh -a
1: iptables -A INPUT -p udp -m multiport --dports 500,1194,4500 -s AAA.BBB.0.0/16 -j ACCEPT
1: iptables -A INPUT -p tcp -m multiport --dports 443,992,1194,5555 -s AAA.BBB.0.0/16 -j ACCEPT
2: iptables -A INPUT -p udp -m multiport --dports 500,1194,4500 -s CC.DD.0.0/16 -j ACCEPT
2: iptables -A INPUT -p tcp -m multiport --dports 443,992,1194,5555 -s CC.DD.0.0/16 -j ACCEPT
3: iptables -A INPUT -p udp -m multiport --dports 500,1194,4500 -s EEE.FFF.GG.0/19 -j ACCEPT
3: iptables -A INPUT -p tcp -m multiport --dports 443,992,1194,5555 -s EEE.FFF.GG.0/19 -j ACCEPT

# ipt_ALLOW.sh -a -s http
1: iptables -A INPUT -p tcp --dport 80 -s AAA.BBB.0.0/16 -j ACCEPT
2: iptables -A INPUT -p tcp --dport 80 -s CC.DD.0.0/16 -j ACCEPT
3: iptables -A INPUT -p tcp --dport 80 -s EEE.FFF.GG.0/19 -j ACCEPT
# ipt_ALLOW.sh -d -s http
1: iptables -D INPUT -p tcp --dport 80 -s AAA.BBB.0.0/16 -j ACCEPT
2: iptables -D INPUT -p tcp --dport 80 -s CC.DD.0.0/16 -j ACCEPT
3: iptables -D INPUT -p tcp --dport 80 -s EEE.FFF.GG.0/19 -j ACCEPT
```

##既知の問題
CIDR IPアドレス表記は、AA.BB.CC.DD/NN のうち数字ピリオドフォーマットしかチェックしておらず、論理的な誤りがある場合は、iptablesに想定の登録ができない。
-c | --check) # ルール反映チェックである程度誤りは検出できるハズ。

##bash スクリプトコード
```sh:/usr/local/bin/ipt_ALLOW.sh
#!/bin/bash
### $Id: ipt_ALLOW.sh,v 1.00 2021/06/12 16:50:50 rapiq $
# -*- coding: utf-8 -*-
PROGNAME=$(basename $0)
# 本ipt_ALLOW.shスクリプトは、CIDR IPアドレスからの
# VPNポート接続をiptablesで許可操作(ルール追加削除)する
# -A, -a ルール追加      -D, -d ルール削除
usage() {
    echo "Usage: $PROGNAME [OPTIONS] FILE"
    echo "  This script is 'iptables -A|-D ' From FILE."
    echo "  FILE[ipt_addr.txt] ... CIDR IP address text."
    echo "Options:"
    echo "  -h, --help"
    echo "  -a, --add_rule"  # -A, -a ルール追加
    echo "  -d, --del_rule"  # -D, -d ルール削除
    echo "  -v, --verbose"   # 操作ログ表示
    echo "  -c, --check"     # ルール反映チェック
    echo "  -s, --service"   # サービス名 vpn ssh http smtp で開放ポートを選ぶ
}

# SoftEther VPN Port udp 500,1194,4500 / tcp 443,992,1194,5555 
vpn_ADD_DEL() {
    UDP_CIDR=`grep udp $ACCEPT_Ln|grep 500,1194,4500|grep $CIDR`
    if [ $? = $JUDGE ]; then  # udp での登録状態はどうか
	if [ $JUDGE = 0 ]; then  # 登録があるのに追加しようとしている
	    SKIP_UDP="DUP. `echo "$UDP_CIDR"|awk '{print $1,$4,$8}'`"
	else  # 登録がないのに削除しようとしている
	    SKIP_UDP="REGISTER rule NOTHING $CIDR"
        fi
	echo "${CUR}: ${IPARAM}!SKIP udp $SKIP_UDP" | tee -a $LOGF  # 操作はスキップ
    else
	COM_UDP="iptables $IPARAM INPUT -p udp -m multiport --dports 500,1194,4500 -s $CIDR -j ACCEPT"
	echo "${CUR}: ${COM_UDP}" | tee -a $LOGF
	if [ ! -z $ARG_V ]; then   # -z	文字列長が 0 なら真
	    echo "${CUR}: ${COM_UDP}"
	fi
	# udp 追加 削除 を実行する
	$COM_UDP
	if [ $? != 0 ]; then  # コマンドは正常終了か
	    echo "${CUR}: iptables $IPARAM $CIDR ERROR." | tee -a $LOGF
	else  # udp 追加 削除 できているか
	    if [ ! -z $ARG_C ]; then  # ルール反映チェック
		# 期待通りにiptables -A/-D が実行できている確認をする(くどい)
		UDP_GREP=`iptables -L -n|egrep '^ACCEPT|udp'|grep 500,1194,4500|grep $CIDR`
		if [ $? != $JUDGE ]; then  # udp での登録状態はどうか
		    if [ $JUDGE = 0 ]; then  # 追加したが登録できていない
			SKIP_UDP=" NOT rule ADD. `echo "$UDP_CIDR"|awk '{print $1,$4,$8}'`"
		    else  # 削除したが登録が残っている
			SKIP_UDP=" NOT rule DELETE. `echo "$UDP_CIDR"|awk '{print $1,$4,$8}'`"
		    fi
		    echo "${CUR}: !SKIP udp $SKIP_UDP" | tee -a $LOGF  # 操作はスキップ
		    if [ ! -z $ARG_V ]; then
			echo "${CUR}: !SKIP udp $SKIP_UDP"
		    fi
		else  # ルール反映チェックは成功
		    if [ $JUDGE = 1 ]; then  # 削除できている Check OK
			echo "${CUR}: Check DELETE rule OK. $CIDR"
		    else  # 追加できている Check OK
			echo "${CUR}: Check ADD rule OK. $CIDR"
		    fi
		fi
	    fi
	fi
    fi

    TCP_CIDR=`grep tcp $ACCEPT_Ln|grep 443,992,1194,5555|grep $CIDR`
    if [ $? = $JUDGE ]; then  # tcp での登録状態はどうか
	if [ $JUDGE = 0 ]; then  # 登録があるのに追加しようとしている
	    SKIP_TCP="DUP. `echo "$TCP_CIDR"|awk '{print $1,$4,$8}'`"
	else  # 登録がないのに削除しようとしている
	    SKIP_TCP="REGISTER rule NOTHING $CIDR"
        fi
	echo "${CUR}: ${IPARAM}!SKIP tcp $SKIP_TCP" | tee -a $LOGF  # 操作はスキップ
    else
	COM_TCP="iptables $IPARAM INPUT -p tcp -m multiport --dports 443,992,1194,5555 -s $CIDR -j ACCEPT"
	echo "${CUR}: ${COM_TCP}" | tee -a $LOGF
	if [ ! -z $ARG_V ]; then   # -z	文字列長が 0 なら真
	    echo "${CUR}: ${COM_TCP}"
	fi
	# tcp 追加 削除 を実行する
	$COM_TCP
	if [ $? != 0 ]; then
	    echo "${CUR}: iptables $IPARAM $CIDR ERROR." | tee -a $LOGF
	else  # tcp 追加 削除 できているか
	    if [ ! -z $ARG_C ]; then  # ルール反映チェック
		# 期待通りにiptables -A/-D が実行できている確認をする(くどい)
		TCP_GREP=`iptables -L -n|egrep '^ACCEPT|tcp'|grep 500,1194,4500|grep $CIDR`
		if [ $? != $JUDGE ]; then  # tcp での登録状態はどうか
		    if [ $JUDGE = 0 ]; then  # 追加したが登録できていない
			SKIP_TCP=" NOT rule ADD. `echo "$TCP_CIDR"|awk '{print $1,$4,$8}'`"
		    else  # 削除したが登録が残っている
			SKIP_TCP=" NOT rule DELETE. `echo "$TCP_CIDR"|awk '{print $1,$4,$8}'`"
		    fi
		    echo "${CUR}: !SKIP tcp $SKIP_TCP" | tee -a $LOGF  # 操作はスキップ
		    if [ ! -z $ARG_V ]; then
			echo "${CUR}: !SKIP tcp $SKIP_TCP"
		    fi
		else  # ルール反映チェックは成功
		    if [ $JUDGE = 1 ]; then  # 削除できている Check OK
			echo "${CUR}: Check DELETE rule OK. $CIDR"
		    else  # 追加できている Check OK
			echo "${CUR}: Check ADD rule OK. $CIDR"
		    fi
		fi
	    fi	    
	fi
    fi
}
tcp1_ADD_DEL() {   # TCP 1 PORT 操作
    TCPPORT1=$1
    TCP_CIDR=`grep tcp $ACCEPT_Ln|grep dpt:$TCPPORT1|grep $CIDR`
    if [ $? = $JUDGE ]; then  # tcp での登録状態はどうか
	if [ $JUDGE = 0 ]; then  # 登録があるのに追加しようとしている
	    SKIP_TCP="DUP. `echo "$TCP_CIDR"|awk '{print $1,$4,$8}'`"
	else  # 登録がないのに削除しようとしている
	    SKIP_TCP="REGISTER rule NOTHING $CIDR"
        fi
	echo "${CUR}: ${IPARAM}!SKIP tcp $SKIP_TCP" | tee -a $LOGF  # 操作はスキップ
    else
	COM_TCP="iptables $IPARAM INPUT -p tcp --dport $TCPPORT1 -s $CIDR -j ACCEPT"
	echo "${CUR}: ${COM_TCP}" | tee -a $LOGF
	if [ ! -z $ARG_V ]; then   # -z	文字列長が 0 なら真
	    echo "${CUR}: ${COM_TCP}"
	fi
	# tcp 追加 削除 を実行する
	$COM_TCP
	if [ $? != 0 ]; then
	    echo "${CUR}: iptables $IPARAM $CIDR ERROR." | tee -a $LOGF
	else  # tcp 追加 削除 できているか
	    if [ ! -z $ARG_C ]; then  # ルール反映チェック
		# 期待通りにiptables -A/-D が実行できている確認をする(くどい)
		TCP_GREP=`iptables -L -n|egrep '^ACCEPT|tcp'|grep dpt:${TCPPORT1}|grep $CIDR`
		if [ $? != $JUDGE ]; then  # tcp での登録状態はどうか
		    if [ $JUDGE = 0 ]; then  # 追加したが登録できていない
			SKIP_TCP=" NOT rule ADD. `echo "$TCP_CIDR"|awk '{print $1,$4,$8}'`"
		    else  # 削除したが登録が残っている
			SKIP_TCP=" NOT rule DELETE. `echo "$TCP_CIDR"|awk '{print $1,$4,$8}'`"
		    fi
		    echo "${CUR}: !SKIP tcp $SKIP_TCP" | tee -a $LOGF  # 操作はスキップ
		    if [ ! -z $ARG_V ]; then
			echo "${CUR}: !SKIP tcp $SKIP_TCP"
		    fi
		else  # ルール反映チェックは成功
		    if [ $JUDGE = 1 ]; then  # 削除できている Check OK
			echo "${CUR}: Check DELETE rule OK. $CIDR"
		    else  # 追加できている Check OK
			echo "${CUR}: Check ADD rule OK. $CIDR"
		    fi
		fi
	    fi	    
	fi
    fi
}
# SSH Port tcp 22
ssh_ADD_DEL() {
    tcp1_ADD_DEL 22   # SSH
}
# HTTP Port tcp 80
http_ADD_DEL() {
    tcp1_ADD_DEL 80   # HTTP
}
# SMTP Port tcp 25
smtp_ADD_DEL() {
    tcp1_ADD_DEL 25   # SMTP
}

#
# 引数 オプション ファイル名 の処理
#
OPT=`getopt -o hadvcs: -l help,add_rule,del_rule,verbose,check,service: -- "$@"`
if [ $? != 0 ] ; then
    exit 1
fi
eval set -- "$OPT"

while true
do
    case "$1" in
	-h | --help)
            usage; exit 1
            ;;
        -a | --add_rule)  # -A, -a ルール追加
            ARG_A=1; shift 1
            ;;
        -d | --del_rule)  # -D, -d ルール削除
            ARG_D=1; shift 1
            ;;
        -v | --verbose)   # 操作ログ表示
            ARG_V=1; shift 1
            ;;
        -c | --check)     # ルール反映チェック
            ARG_C=1; shift 1
            ;;
        -s | --service)   # サービス名 vpn ssh http smtp で開放ポートを選ぶ
            ARG_S=1; shift 1; VALUE_S="$1"; shift 1
            ;;
	--)
            shift; param+=( "$@" ); break
            ;;
	*)
            echo "ERR_F0020 Internal error!" 1>&2; exit 1
            ;;
    esac
done
if [ "$ARG_A" = "" ] && [ "$ARG_D" = "" ]; then
    usage
    echo "ERR_F0030 !!! MUST USE -a or -d"
    exit 1
fi
if [ ${EUID} != 0 ]; then  # 実行UIDが "0"(root)で管理者権限
    echo "ERR_F0010 User not root."
    exit 1
fi
if [ ! -z $ARG_V ]; then
   echo "START: $PROGNAME at `date`"
fi
# サービス名 (-s --service) vpn ssh http smtp で開放ポートを選ぶ
if [ "$VALUE_S" = "" ]; then
    SRVNAME="vpn"    ### 省略時サービス名
else
    SRVNAME="$VALUE_S"
fi
IPTDIR="/usr/local/IPT_DATA"
if [ ! -d ${IPTDIR} ]; then
    echo "ERR_F0040 $IPTDIR NOTEXISTS."
    exit 1
fi
ACCEPT_Ln="${IPTDIR}/ACCEPT_Ln"
iptables -L -n|egrep ^ACCEPT|sort -k 4 > $ACCEPT_Ln
cd ${IPTDIR}
if [ $? != 0 ]; then
    echo "ERR_F0050 cd $IPTDIR."
    exit 1
fi
LOGDIR="${IPTDIR}/LOG"
if [ ! -d ${LOGDIR} ]; then
    mkdir -p ${LOGDIR}
    if [ $? != 0 ]; then
        echo "ERR_F0060 $LOGDIR Cannot mkdir."
	exit 1
    fi
fi
YMDHM=`date '+%Y%m%d_%H%M'`
if [ "$ARG_A" != "" ]; then
   IPARAM="-A"
   LOGF="${LOGDIR}/${SRVNAME}_${YMDHM}_A.log"
   JUDGE=0   # grep exists ERROR  0 if a line is selected
fi
if [ "$ARG_D" != "" ]; then
   IPARAM="-D"
   LOGF="${LOGDIR}/${SRVNAME}_${YMDHM}_D.log"
   JUDGE=1   # grep NOT exists ERROR  1 if no lines were selected
fi
if [ "$param" = "" ]; then
    IPTFILE="${IPTDIR}/ipt_addr.txt"
else
    IPTFILE="${IPTDIR}/${param}"
fi
if [ ! -f "$IPTFILE" ]; then
    echo "ERR_F0070 $IPTFILE NOT FOUND."
    exit 1
fi
LSFILE=`ls -l ${IPTFILE}|awk '{print $5,$6,$7,$8,$9}'`

echo "" >> $LOGF
echo "${YMDHM} IPARAM=${IPARAM} $LSFILE" >> $LOGF
CUR=0              #########################################################
while read pline   #               $IPTFILE を一行読み込む                #
do                 #########################################################
    # egrep で#で始まるコメント行および空白行を削除する
    echo $pline | egrep -q '^\s*#|^\s*$'
    if [ $? = 0 ]; then
	continue
    fi
    if [ "$pline" = "EOF" ]; then
	if [ ! -z $ARG_V ]; then
	   echo "${IPTFILE} EOF Detected."
	fi
	break
    fi

    CUR=`expr ${CUR} + 1`
    if [ ! -z $ARG_V ]; then
       echo "${CUR}: ${pline}"
    fi
    echo "${CUR}: ${pline}"  >> $LOGF
    CIDR=`echo ${pline}|awk '{print $1}'`
    echo $CIDR | perl -ne '/^\d+\.\d+\.\d+\.\d+\/\d+$/ and exit 1';RET=$?
    # Classless Inter-Domain Routing サイダー クラスレスアドレッシング
    if [ $RET != 1 ]; then ### 数字のみで整合性のチェックではない
	echo $CIDR | perl -ne '/^\d+\.\d+\.\d+\.\d+/ and exit 1';RET2=$?
	if [ $RET2 != 1 ]; then ### /NN がない場合もある
	    echo "${CUR}: $CIDR ADDRESS Missing." | tee -a $LOGF
	    exit 0
	fi
    fi

    case "$SRVNAME" in
	vpn)  # SoftEther VPN Port udp 500,1194,4500 / tcp 443,992,1194,5555 
	    vpn_ADD_DEL ;;
	ssh)  # SSH Port tcp 22
	    ssh_ADD_DEL ;;
	http)  # HTTP Port tcp 80
	    http_ADD_DEL ;;
	smtp)  # SMTP Port tcp 25
	    smtp_ADD_DEL ;;
	*)
            echo "ERR_F0080 $SRVNAME service NOT FOUND."
            exit 1 ;;
    esac

done < $IPTFILE

SEQ=`printf "%03d" ${CUR}`
ENDM1="${IPTFILE} iptables $IPARAM COUNT=${SEQ} END." >> $LOGF
ENDM2="LOGFILE: $LOGF" >> $LOGF
ENDM3="END: $PROGNAME at `date`" >> $LOGF
if [ ! -z $ARG_V ]; then
   echo $ENDM1
   echo $ENDM2
   echo $ENDM3
fi
# end of ipt_ALLOW.sh
```

## SoftEtherのログserver_log/の調査
数十万ラインのアクセスを試みる怪しいIPアドレスが出現することは、もうなくなったが、どんなログで、どこから忍び寄って来ていたのか調べてみた。

```shell-session
# cd /usr/local/vpnserver/server_log
　　　　　　　　　　　　　　　　　　＃(1) もっとも行数多いログ・ファイルは？
# wc -l vpn_2021*log | sort -nr | sed -n 2,2p
   532919 vpn_20210517.log
　　　　　　　　　　　　　　　　　　＃(2) どんな行がたくさん出現するか？
# cat vpn_20210517.log|awk '{print $3,$4}'|cut -c -16|sort|uniq -c|sort -nr|head -12
 318204 IPsec IKE
 214457 IPsec Client
     61 Connection "CID-
     59 The connection
     59 On the
     59 For the
     10 OpenVPN Module:
      8 OpenVPN Session
      2 SSL communicatio
　　　　　　　　　　　　　　　　　　＃(3) "IPsec IKE" "IPsec Client"が多い
# grep "IPsec IKE" vpn_20210517.log|head -1
2021-05-17 00:10:26.541 IPsec IKE Session (IKE SA) 269587 (Client:
88330) (3.6.XXX.YYY:33277 -> 192.168.0.8:4500): A new IKE SA (Main
Mode) is created. Initiator Cookie: 0xC9832C40B1D51852, Responder
Cookie: 0x7FC03F33EC4C6452, DH Group: MODP 1024 (Group 2), Hash
Algorithm: SHA-1, Cipher Algorithm: 3DES-CBC, Cipher Key Size: 192
bits, Lifetime: 4294967295 Kbytes or 1 seconds
# grep "IPsec Client" vpn_20210517.log|head -1
2021-05-17 00:10:26.531 IPsec Client 88330 (3.6.XXX.YYY:33277 ->
192.168.0.8:4500): A new IPsec client is created.

　　　　　　　　　　　　　　　　　　＃(4) IPアドレス抜き出しスクリプト
# cat ipa_read.pl
#!/usr/bin/perl
use strict;
use warnings;
my $lc=0;
while (my $line = <STDIN>){
  chomp($line);
  $lc++;
  if($line =~ /\b((?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))\b/) {
      print "$1\n";
  }
}
print STDERR "LINE COUNT = $lc\n";
　　　　　　　　　　　　　　　　　　＃(5) IPアドレスの集計 上位3つ
# cat vpn_20210517.log|./ipa_read.pl|sort|uniq -c|sort -nr|head -3
LINE COUNT = 532919
 197438 3.6.XXX.YYY
 125396 5.254.XX.ZZZ
  23868 18.221.YYY.ZZZ
#
```

コメント＃(5) IPアドレスの集計で、19万回,12万回も探られて侵入しようと試みていると思われる。サーバを立てたなら、不正アクセスを監視して情報漏えいや踏み台にされることのないように注意したい。