iptablesファイルに書いたドメインを、cronで定期的に解決しiptablesに反映する 

# 本記事について

本記事は、`/etc/sysconfig/iptables`にドメイン名を直接書いたものを、iptablesに定期的に反映させる方法について私見を述べたものです。

### 環境

Amazon Linux 2
Python3

# 背景

### iptablesの罠

以下のように、iptablesをドメイン名で記載している箇所がある。
変更する際は`sudo vi /etc/sysconfig/iptables`コマンドで直接編集し`sudo service iptables restart`で反映させる運用となっている。

```text:/etc/sysconfig/iptables
-A OUTPUT -p tcp -d dev.**.***.net -m owner --uid-owner userName -j ACCEPT
-A OUTPUT -p tcp -d prd.**.***.net -m owner --uid-owner userName -j ACCEPT
-A OUTPUT -p tcp -d ************.**.net -m owner --uid-owner userName -j ACCEPT
```

上記のように書くと一見、DNSのAレコードの向き先を宛先とするパケットをACCEPTするかのように見える。
しかし実際には、iptablesサービスを起動したタイミングと、iptables設定を「反映」したタイミングでしかDNS解決が行われない。

### 問題点

目的としては、上記設定に記載されたサーバへSSHを行う。
サービス再起動などでiptables設定が反映された直後は、DNSが解決されて間もないため、上記設定は有効なものであり、正常にSSH接続を行うことができる。

しかし時間経過やその他何らかの要因により相手サーバのIPアドレスが変わってしまうと・・・
その後接続する際にSSHコマンド側でDNS解決されたIPアドレスと、iptablesに既に読み込まれたIPアドレスが食い違うため接続が出来なくなる。

都度`sudo service iptables restart`すりゃいいという話かもしれないが、当該端末を使用するすべてのユーザがこのコマンドを実行できるとは限らない。
そのためSSH接続が出来る、という仕様をそうしたユーザに提供できないという問題があった。

# 方針

 - きたなくてもいいので、とにかく速く実装する。着手したその日に終わらす。
 - `/etc/sysconfig/iptables`ファイルを原本とする。
 - 3時間ごとに下記処理を行う
    - `/etc/sysconfig/iptables`ファイルを読み込み、ドメイン名の文字列を抽出する。
    - 上記ドメイン名を名前解決し、AレコードのIPアドレスを取得する。
    - 前回（3時間前）の取得結果と比較し、IPアドレスの変わったものがあればiptablesを更新する。差分はテキストファイルに保存する。
    - 取得結果をテキストファイルに保存し、次回実行時に用いる。
 - ドメイン名を正規表現で指定し、該当するドメイン名については処理を行わない

# 実装

### 概要

`pydig`モジュールを用いて名前解決を行う。それ以外はPythonのbultin functionと標準モジュールでベタ書きする。

※要pip install

```
pip install pydig
```

### ディレクトリ構成

 - /root/python　・・・ソースファイルを格納
    - conf　・・・設定ファイルを格納
    - log　・・・ログファイル（？）を格納
    - tmp　・・・とりあえず置き場所に困ったもので処理によって生成されるファイル、ならびに一時ファイルを格納
    - venv　・・・`python3 -m venv venv`の結果が格納されている

### 環境構築

```shell:dig_refreshIptables.sh
cd /root/python
./venv/bin/python3 dig_refreshIptables.py
```

cronからPythonを呼ぶためだけのシェルスクリプト

```python:dig_refreshIptables.py
import os
import shutil
import re
import subprocess
from datetime import datetime as dt

import pydig

RAW_IPTABLES = '/etc/sysconfig/iptables'
DIG_RESULT_FILE = './tmp/iptables_past_dig_result.txt'
DIG_RESULT_FILE_BAK = lambda x: f'./tmp/iptables_past_dig_result_{x}.txt'
LOG_FILE = f'./log/iptables_dig_diff.log'
EXCLUDE_CONF = f'./conf/iptables_dig_exclude_pattern.txt'

def dig(name):
    result = pydig.query(name, 'A')
    return result[-1]

shutil.copy(RAW_IPTABLES, './tmp')

with open('./tmp/iptables', mode='r') as f:
    lines = f.readlines()

# iptables記述からアドレス指定を抽出
addrs = []
for line in lines:
    recs = line.split(' ')
    if line.startswith('#') or len(recs) == 0:
        continue
    for i, rec in enumerate(recs):
        if rec == '-d':
            addr = recs[i + 1]
            addrs.append(addr)
            break

def checkRegex(pattern, content):
    m = re.match(pattern, content)
    if m is None:
        return False
    return True

# アドレス指定からDNS文字列を抽出
names = []
for addr in addrs:
    if checkRegex('.*?[a-zA-Z].*?', addr):
        names.append(addr)

# dig実行
digResult = {name: dig(name) for name in names}

def makeDigResultFile(digResult):
    with open(DIG_RESULT_FILE, mode='w', encoding='utf-8') as f:
        f.write('name\tinA\n')
        for k, v in digResult.items():
            f.write(f'{k}\t{v}')
            f.write('\n')

if not os.path.exists(DIG_RESULT_FILE):
    print('first exec, exiting...')
    makeDigResultFile(digResult)
    exit(0)

def readDigResultFile():
    with open(DIG_RESULT_FILE, mode='r', encoding='utf-8') as f:
        i = 0
        result = {}
        for line in f.readlines():
            i += 1
            if i == 1:
                continue
            rec = line.split('\t')
            rec = [x.strip() for x in rec]
            result[rec[0]] = rec[1]

        return result

def loadExcludeConfig():
    with open(EXCLUDE_CONF, mode='r') as f:
        return [x.strip() for x in f.readlines()]

#print('reading dig result file...')
old = readDigResultFile()
excludes = loadExcludeConfig()
difference = []
needRefresh = False
for k, v in digResult.items():
    isExcluded = False
    for ex in excludes:
        if checkRegex(ex, k):
            isExcluded = True
            break
    if isExcluded:
        continue
    if k not in old:
        continue
    if v != old[k]:
        difference.append(f'{k}\t{v}\t{old[k]}')
        needRefresh = True

ts = dt.now().strftime('%Y%m%d_%H%M%S')
#shutil.copy(DIG_RESULT_FILE, DIG_RESULT_FILE_BAK(ts))
makeDigResultFile(digResult)

with open(LOG_FILE, mode='a', encoding='utf-8') as f:
    for d in difference:
        f.write(f'{ts}\t{d}')
        f.write('\n')

def refreshIptables():
    print(f'{ts}\trefreshing iptables...')
    subprocess.run(['./_refreshIptables.sh'], shell=True)
    print(f'iptables refresh has finished!!!')

if needRefresh:
    refreshIptables()
```

メインの処理。
処理内容は[前述](#方針) 

```shell:_refreshIptables.sh
sudo iptables-restore < /etc/sysconfig/iptables
```

iptablesを更新するコマンドを記載。Pythonからsubprocessモジュールを用いて呼び出している。

```text:conf/iptables_dig_exclude_pattern.txt
.*?\.elb\..*?
```
処理を行わないドメイン名を正規表現で指定。（改行区切りで複数指定することができる。）
正規表現はPythonの`re`モジュールの正規表現を用いる。

この例では、`.elb.`を含むドメイン名は本処理の対象としない、ということを示している。

### 定期実行の設定

crontabの設定を行う。

```
# crontab -e
```

viエディタが開くので、以下を追記する。

```
0 */3 * * * bash /root/python/dig_refreshIptables.py
```

`tail -f /var/log/cron`コマンドを用いることで、設定が行われたことを確認できる。
条件をみたす時刻まで待つと、上記コマンドが実行された旨が表示される。
（ちなみに動作確認だけしたければ`* * * * *`として毎分実行すればいい）

```
Feb 17 20:37:14 ****** crontab[******]: (root) END EDIT (root)
Feb 17 20:37:42 ****** crontab[******]: (root) BEGIN EDIT (root)
:
:
Feb 17 21:00:02 ****** CROND[******]: (root) CMD (bash /root/python/dig_refreshIptables.py)
```

# 結果

上記cron実行後、相手サーバへ正常にSSHすることが出来た。

```
[userName@****** ~]$ ssh -i key.pem user@dev.**.***.net
Last login: *** *** ** **:**:** 2022 from ip-**-**-**-***.ap-northeast-1.compute.internal

       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-2/
[user@ip-**-**-**-*** ~]$
```

# 参考

 - [pythonでネームサーバを指定してdigするためのライブラリ - Qiita](https://qiita.com/mollinaca/items/d25732a08c306a544a08)
 - [pydig · PyPI](https://pypi.org/project/pydig/)
 - [「/etc/sysconfig/iptables」ファイル - Linux技術者認定 LinuC | LPI-Japan](https://linuc.org/study/knowledge/540/)
 - [AmazonLinuxでcron実行させる(Linuxで設定) - Qiita](https://qiita.com/shinon_uk/items/ff38f84ff857dae9a55d)
 - [クーロン(cron)をさわってみるお - Qiita](https://qiita.com/katsukii/items/d5f90a6e4592d1414f99)