Mar 6, 2021

宅EdgerouterXへアクセスしているIPアドレスをGrafa自naで地図上にプロットしてみた。

自宅の EdgerouterX にアクセスしている IP アドレスを GeoLite2+Rsyslog+Prometheus+Grafana で地図上にプロットするダッシュボードを作って見ました。日によって違いますが、アクセスが多いのは、Netherlands、United States、China、United Kingdonm、Russia の順ですかね。

GeoLite2を使ってルータにアタックアクセスしている IP を元に地域を割り出し Grafana で表示したいと思います。以前、grok-exporter に自前で mmdlookup を組み込んで実現したことはありますが、今回は rsyslog-mmdblookup を利用してさっと実現したいと思います。

EdgerouterX の監視そのものは snmp-exporter を使ってトラフィック量など監視していますが、今回は、EdgerouterX のシステムログを解析して実現することにします。

前回、Prometheus と Grafana をインストール済みなので、今回は、rsyslog、promtail を追加したいと思います。

コンテナの構成!

なお victoria(Victoria Metrics)と Loki も合わせて追加します。victoria は prometheus のデータ永続化を狙って入れてます。Grafana も promethus 経由ではなく今回から victoria 経由にします。loki は、ログの確認に利用します。

全体構成!

rsyslog コンテナの作成

rsyslog は豊富な機能と高度な処理を高速にできる素晴らしいソフトウェアだと思います。Alpine に GeoLite2 を利用するためのモジュールを組み込んでコンテナ化したいと思います。コンテナは Fedora CoreOS の Podman で構築していきます。

ビルド用フォルダ

nginx_rule.dat は別途 nginx のログも解析したいと思いますので rsyslog で取り込めるように入れてあります。

backend
:
├── rsyslog
│ ├── Dockerfile
│ └── build
│ ├── nginx_rule.dat
│ ├── router_rule.dat
│ └── rsyslog.conf

rsyslog Dockerfile

Alpine イメージを利用してそこに必要なパッケージをインストールします。ローカルタイムを利用したいのでタイムゾーンパッケージも入れました。

backend/rsyslog/Dockerfile
FROM alpine
LABEL version="1.0" "akiboi.net"="akiboI"
 
RUN apk add --update --no-cache rsyslog rsyslog-mmdblookup rsyslog-mmnormalize liblognorm tzdata && \
  cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
  apk del tzdata
RUN mkdir /var/lib/geodb && \
  mkdir /etc/rsyslog.d
COPY build/rsyslog.conf /etc
COPY build/*_rule.dat /etc/rsyslog.d
 
#
CMD ["rsyslogd","-f","/etc/rsyslog.conf","-n"]

router_rule.dat の設定

rsyslog.conf ファイルでアクションで使用するルールを設定します。ログからアクセス元の IP をパースしたいので、mmnormailze モジュールを利用しています。IP のみ取り出せれば良いので他はダミーとしてます。パースのルールは、Liblognormを参照してください。

backend/rsyslog/build/router_rule.dat
rule=: [%dummy0:word% %dummy1:word% %dummy2:word% SRC=%clientip:ipv4% %dummy3:rest%

rsyslog.conf

アクセス元の IP を元にmmdblookupモジュールを使って continent code、continent、country code、country、city、latitude、longitude を取り出します。latitude、longitude は Grafana で地図の該当場所に表示するために利用します。

設定ファイル全文を載せていますが、関連するものは 40-42 行当たりになります。他は nginx のログ解析用とあとは私の環境に合わせて対象のログを取り出すために記載しています。44 行はコメントアウトしていますが、テスト時には JSON ファイルで確認すると該当フィールドが正しいか確認に役立ちます。

#### Global directives ####
 
# Sets the directory that rsyslog uses for work files.
$WorkDirectory /var/lib/rsyslog
 
# Sets default permissions for all log files.
$FileOwner root
$FileGroup adm
$FileCreateMode 0640
$DirCreateMode 0755
$Umask 0022
 
#### Modules ####
module(load="imudp")
module(load="mmnormalize")
module(load="mmdblookup")
 
#### Input ####
input(type="imudp" port="52200")
 
template( name="etcfile" type="string" string="/var/log/rsyslog/%hostname%/%programname%_msg.log")
template( name="locationlog" type="string" string="%timegenerated:::date-rfc3339% %hostname% %programname%: \"%$!iplocation!continent!code%\" \"%$!iplocation!continent!names!en%\" \"%$!iplocation!country!iso_code%\" \"%$!iplocation!country!names!en%\" \"%$!iplocation!city!names!en%\" \"%$!iplocation!location!latitude%\" \"%$!iplocation!location!longitude%\"%msg%\n")
template(name="all-json" type="list"){
  property(name="$!all-json")
}
 
if $programname == "proxy_access" or $programname == "web_access" then {
    if $msg startswith " -" or $msg startswith " 192.168.1" or $msg startswith " 127.0.0"  or $msg startswith " 24●●:●●●●:●●●●:●●●●" then {
        action(type="omfile" dynaFile="etcfile")
    }
    else {
        action(type="mmnormalize" rulebase="/etc/rsyslog.d/nginx_rule.dat")
        action(type = "mmdblookup" mmdbfile = "/var/lib/geodb/GeoLite2-City.mmdb"
             fields=["!continent!code","!continent!names!en","!country!iso_code","!country!names!en","!city!names!en","!location!latitude","!location!longitude"] key="!clientip" )
        action(type="omfile" template="locationlog" File="/var/log/rsyslog/nginx_access.log")
#        action(type="omfile" template="all-json" File="/var/log/nginx_json_access.log")
    }
} else if $hostname == "akiboiRT" and $programname == "kernel" then {
    if ($msg contains "[DSLite" or $msg contains "[PPPoE" or $msg contains "[WANv6") and not ($msg contains "DST=255.255.255.255") then {
        action(type="mmnormalize" rulebase="/etc/rsyslog.d/router_rule.dat")
        action(type = "mmdblookup" mmdbfile = "/var/lib/geodb/GeoLite2-City.mmdb"
            fields=["!continent!code","!continent!names!en","!country!iso_code","!country!names!en","!city!names!en","!location!latitude","!location!longitude"] key="!clientip" )
        action(type="omfile" template="locationlog" File="/var/log/rsyslog/router_access.log")
#       action(type="omfile" template="all-json" File="/var/log/router_json_access.log")
    } else {
            action(type="omfile" dynaFile="etcfile")
    }
} else {
    action(type="omfile" dynaFile="etcfile")
}

rsyslog の.gitlab-ci.yml

私の開発環境の gitlab-ci.yml を載せておきます。前回作成した他のコンテナとほとんど同じで、ビルド判断するフォルダとコンテナ名、ボリュームマッピングが違うのみです。ホスト側にマッピングしているフォルダにあらかじめGeoLite2 の DBを入手して登録しておきます。

grafana:
  stage: buildContainer
  only:
    changes:
      - backend/rsyslog/**/*
  script:
    - HOST="xxxx@yyyy"
    - POD="backend"
    - BASEDIR="/vol"
    - HOMEDIR=`pwd`
    - WORKDIR=`echo ${HOMEDIR} | sed -e "s/^\/home\/gitlab-runner/\/vol\/gitlab-runner\/data/g"`
    - CONTAINER=rsyslog
    - VOLUME="-v $BASEDIR/$CONTAINER/data:/var/lib/geodb:Z -v $BASEDIR/$CONTAINER/log:/var/log:Z "
    - REGISTRY="registry.akiboi.net:5000"
    - ssh $HOST mkdir -p $BASEDIR/$CONTAINER/data $BASEDIR/$CONTAINER/log
    - ssh $HOST podman build -t $REGISTRY/$CONTAINER $WORKDIR/$POD/$CONTAINER
    - ssh $HOST podman push $REGISTRY/$CONTAINER
    - ssh $HOST podman stop -i $CONTAINER
    - ssh $HOST podman rm -i $CONTAINER
    - ssh $HOST podman run -dt --pod $POD --restart=always --name $CONTAINER $VOLUME $REGISTRY/$CONTAINER

EdgerouterX でシステムログを監視サーバに転送する設定をするとアクセス元 IP をキーに地域情報を出力することができます。

/var/log/rsyslog/router_access.log
2021-03-06T18:00:42.776830+09:00 akiboiRT kernel: "NA" "North America" "MX" "Mexico" "Castanos" "26.504000" "-101.156600"...
2021-03-06T18:00:45.820711+09:00 akiboiRT kernel: "NA" "North America" "MX" "Mexico" "Castanos" "26.504000" "-101.156600"...
2021-03-06T18:01:10.228675+09:00 akiboiRT kernel: "EU" "Europe" "SE" "Sweden" "Örebro" "59.274000" "15.211900"...

Promtail コンテナの作成

Promtail は、各アプリのログを読み取りログ可視化のために Loki に転送する役割がありますが、ログからメトリックスを作成して Prometheus に API で提供しています。今回は rsyslog で backend ポッドにログが転送されていますので、このログを元に設定していきます。通常はアプリ側に Promtail を配置してログを転送すると思いますが、EdgerouterX に余計なものは入れたくない、rsyslog で地域情報を付加するのでこのような構成にしています。

ビルド用フォルダ

backend
:
├── promtail
│ ├── Dockerfile
│ └── build
│ └── config.yml

Promtail Dockerfile

grafana/promtail イメージをそのまま利用します。

backend/promtail/Dockerfile
FROM grafana/promtail
 
LABEL version="1.0" "akiboi.net"="akiboI"
 
COPY build/config.yml /etc/promtail

config.yml の設定

clients は、Loki サーバを指定します。同じポッド内に Loki コンテナを構築しますので、転送先は localhost:3100 となっています。scrape ステージは nginx 用と router 用を設定。地域情報を付加したログそのものだけでもよかったのですが、国別や市別などサマリで検索しやすくするために 3 種類用意しました。max_idle_duration を入れているのは、余計なメトリックスを増やさないために、同じキーのものが 24 時間アクセスなければメトリックスから外すために指定しています。メトリックスの詳しい情報はGlafanaLabs のドキュメントを参照してください。Labels は Prometheus で検索フィールドとしても利用できますが、Grafana(Loki)で Labels をキーにすぐにセレクトしてログを見れるので便利だと思います。大量のログから特定のものを探すのに Loki は素晴らしいと感じます。latitude と longitude の Labels は、Grafana で Worldmap Panel で利用します。

backend/promtail/build/config.yml
server:
  http_listen_port: 9080
  grpc_listen_port: 0
 
positions:
  filename: /tmp/positions.yaml
 
clients:
  - url: http://localhost:3100/loki/api/v1/push
 
scrape_configs:
  - job_name: nginx
    static_configs:
      - targets:
          - localhost
        labels:
          job: varlogs
          __path__: /var/log/rsyslog/nginx*.log
  - job_name: router
    static_configs:
      - targets:
          - localhost
        labels:
          job: varlogs
          __path__: /var/log/rsyslog/router*.log
    pipeline_stages:
      - regex:
          expression: '^(?P<time>.*) (?P<server>.*) (?P<facility>.*) "(?P<continetcd>.*)" "(?P<continent>.*)" "(?P<countrycd>.*)" "(?P<country>.*)" "(?P<city>.*)" "(?P<latitude>.*)" "(?P<longitude>.*)" .*]IN=(?P<IF>.*) OUT.*SRC=(?P<SRC>.*) DST=(?P<DST>.*) LEN.*PROTO=(?P<PROTO>.*) SPT=.*$'
      - labels:
          continetcd:
          continent:
          countrycd:
          country:
          city:
          latitude:
          longitude:
      - metrics:
          router_log_counter:
            type: Counter
            description: total number of router logs
            max_idle_duration: 24h
            config:
              match_all: true
              action: inc
      - labels:
          country:
      - metrics:
          router_log_country:
            type: Counter
            description: total number of router logs
            max_idle_duration: 24h
            source: country
            config:
              action: inc
      - labels:
          city:
      - metrics:
          router_log_city:
            type: Counter
            description: total number of router logs
            max_idle_duration: 24h
            source: city
            config:
              action: inc

rsyslog.conf とこの config.yml の設定が今回の肝でここさえうまく設定できればあとは単純作業でさっと終わります。

このため、他のファイルは参考に載せておきますが解説はなくてもいいでしょう。

その他のファイル

ビルド用フォルダ

backend
├── .gitlab-ci.yml ← add promtail,loki,victory
├── blackbox-exporter
│ └── Dockerfile
├── grafana
│ ├── Dockerfile
│ └── build
│ └── grafana.ini
├── loki ← add
│ ├── Dockerfile ← add
│ └── build
│ └── local-config.yaml ← add
├── pr1
│ ├── Dockerfile
│ ├── build
│ │ ├── conf.d
│ │ │ ├── default.conf
│ │ │ ├── header.conf
│ │ │ └── log.conf
│ │ ├── nginx.conf
│ │ └── vhost.d
│ │ ├── 01_ap-Server.conf
│ │ ├── 03_test-Server.conf
│ │ └── 99_default.conf
│ └── html
│ ├── index4.html
│ └── index6.html
├── prometheus
│ ├── Dockerfile
│ └── build
│ └── prometheus.yml ← add pormtail,victoria
├── promtail
│ ├── Dockerfile
│ └── build
│ └── config.yml
├── rsyslog
│ ├── Dockerfile
│ └── build
│ ├── nginx_rule.dat
│ ├── router_rule.dat
│ └── rsyslog.conf
├── snmp-exporter
│ ├── Dockerfile
│ └── build
│ └── snmp.yml
└── victoria ← add
└── Dockerfile ← add

.gitlab-ci.yml

:
promtail:
  stage: buildContainer
  only:
    changes:
      - backend/promtail/**/*
  script:
    - HOST="xxxx@yyyy"
    - POD="backend"
    - BASEDIR="/vol"
    - HOMEDIR=`pwd`
    - WORKDIR=`echo ${HOMEDIR} | sed -e "s/^\/home\/gitlab-runner/\/vol\/gitlab-runner\/data/g"`
    - CONTAINER=promtail
    - VOLUME=" -v $BASEDIR/rsyslog/log:/var/log:ro " ← rsyslogのログを利用する。
    - REGISTRY="registry.akiboi.net:5000"
    - ssh $HOST podman build -t $REGISTRY/$CONTAINER $WORKDIR/$POD/$CONTAINER
    - ssh $HOST podman push $REGISTRY/$CONTAINER
    - ssh $HOST podman stop -i $CONTAINER
    - ssh $HOST podman rm -i $CONTAINER
    - ssh $HOST podman run -dt --pod $POD --restart=always --name $CONTAINER $VOLUME $REGISTRY/$CONTAINER
 
loki:
  stage: buildContainer
  only:
    changes:
      - backend/loki/**/*
  script:
    - HOST="xxxx@yyyy"
    - POD="backend"
    - BASEDIR="/vol"
    - HOMEDIR=`pwd`
    - WORKDIR=`echo ${HOMEDIR} | sed -e "s/^\/home\/gitlab-runner/\/vol\/gitlab-runner\/data/g"`
    - CONTAINER=loki
    - VOLUME="-v $BASEDIR/$CONTAINER/data:/victoria-metrics-data:Z "
    - REGISTRY="registry.akiboi.net:5000"
    - ssh $HOST mkdir -p $BASEDIR/$CONTAINER/data
    - ssh $HOST podman build -t $REGISTRY/$CONTAINER $WORKDIR/$POD/$CONTAINER
    - ssh $HOST podman push $REGISTRY/$CONTAINER
    - ssh $HOST podman stop -i $CONTAINER
    - ssh $HOST podman rm -i $CONTAINER
    - ssh $HOST podman run -dt --pod $POD --restart=always -- name $CONTAINER $VOLUME $REGISTRY/$CONTAINER
 
lovictoriaki:
  stage: buildContainer
  only:
    changes:
      - backend/victoria/**/*
  script:
    - HOST="xxxx@yyyy"
    - POD="backend"
    - BASEDIR="/vol"
    - HOMEDIR=`pwd`
    - WORKDIR=`echo ${HOMEDIR} | sed -e "s/^\/home\/gitlab-runner/\/vol\/gitlab-runner\/data/g"`
    - CONTAINER=victoria
    - VOLUME=""
    - REGISTRY="registry.akiboi.net:5000"
    - ssh $HOST podman build -t $REGISTRY/$CONTAINER $WORKDIR/$POD/$CONTAINER
    - ssh $HOST podman push $REGISTRY/$CONTAINER
    - ssh $HOST podman stop -i $CONTAINER
    - ssh $HOST podman rm -i $CONTAINER
    - ssh $HOST podman run -dt --pod $POD --restart=always -- name $CONTAINER $VOLUME $REGISTRY/$CONTAINER
:

loki ファイル

backend/loki/Dockerfile
FROM grafana/loki
LABEL version="1.0" "akiboi.net"="akiboI"
 
COPY build/local-config.yaml /etc/loki
backend/loki/build/local-config.yaml
auth_enabled: false
 
server:
  http_listen_port: 3100
 
ingester:
  lifecycler:
    address: 127.0.0.1
    ring:
      kvstore:
        store: inmemory
      replication_factor: 1
    final_sleep: 0s
  chunk_idle_period: 5m
  chunk_retain_period: 30s
  max_transfer_retries: 0
 
schema_config:
  configs:
    - from: 2018-04-15
      store: boltdb
      object_store: filesystem
      schema: v11
      index:
        prefix: index_
        period: 168h
 
storage_config:
  boltdb:
    directory: /loki/index
 
  filesystem:
    directory: /loki/chunks
 
limits_config:
  enforce_metric_name: false
  reject_old_samples: true
  reject_old_samples_max_age: 168h
 
chunk_store_config:
  max_look_back_period: 0s
 
table_manager:
  retention_deletes_enabled: false
  retention_period: 0s

victoria ファイル

backend/vicroria/Dockerfile
FROM victoriametrics/victoria-metrics
LABEL version="1.0" "akiboi.net"="akiboI"

prometheus.yml に promtail、victoria 追加

scrape_configs:
: - job_name: 'promtail'
    static_configs:
      - targets:
          - localhost:9080
 
# Remote write configuration (for victoria)
remote_write:
  - url: http://localhost:8428/api/v1/write

テスト

環境ができましたので、http://prometheus サーバ:9080 にアクセスしてメトリックスを確認してみます。メトリックス名は、promtail の config.yml には prefix を設定していませんので Default の promtail_custom が先頭に付きます。promtail_custom_router_log_counter と入力して Execute ボタンを押すとメトリックスが表示されれば OK です。Labels に記載されたものがキーととしてカウントアップされています。

prometheusテスト!

同様に、promtail_custom_router_country、promtail_custom_router_city も検索できれば OK です。

Grafana でダッシュボード作成

以下のようなダッシュボードを作りたいと思います。 Grafanaダッシュボード-Worldmap Panel-!

データソース

データソースは、Prometheus ではなく、victori を立ち上げしたのでこちらから読み込むことにします。データタイプに Prometheus を選択して、以下のように設定しました。

No.項目 設定内容備考
1NameVicroria
2URLhttp://localhost:8428
3AccessServer(default)

タイトルパネルの作成

タイトルは、Visualization に text を選んで html で記載しました。

<div style="background-image: url('https://xxx.xxx.xxx/bg01.jpg')">
  <div
    style="
    padding: 1rem;
    color: #fff;
    font-size:32px;
    font-family: sans-serif;
    font-weight:bold;
    text-align:center;
    text-shadow:
      0 0 5px #fff,
      0 0 10px #fff,
      0 0 20px #fff,
      0 0 40px #0ff,
      0 0 60px #0ff;
    "
  >
    EdgeRouterX monitoring
  </div>
</div>

Traffic Map の作成

アクセス元の場所を地図上にプロットしたいと思います。Worldmap Panelをインストールします。イントールは、grafana のコンテナから grafana-cli でインストールします。

$ podman exec -it grafana sh
grafana-cli plugins install grafana-worldmap-panel

メトリックスは以下のように delta 関数で 2 点間の時間(Absolute time range)の差を city,latitude,longitude 単に集計して利用します。また Format を Table にしておきます。

worldmapメトリックス!

次に Panel で Vusualization を Worldmap を選択して以下のように設定ましす。

No.項目 設定内容備考
1Location Datatable
2Aggregationcurrent
3Table Query Formatcoordinates
4Location Name Fieldcity
5Metric FieldValue
6Latitude Fieldlatitude
7Longitude Fieldlongitude
8Thresholds1,10,100お好みで
9Colors青、黄色、オレンジ、赤お好みで

これで、地図上にアクセス元をプロットすることができました。なお、今回の query は空白の city を除きましたが、city が空白でも latitude,longitude の値が入っていることが多くありますので、最終的には city が空白のものもプロットするようにしました。マウスで該当箇所に持って行った時に city が n/a と表示するだけです。無料の GeoLite2 はそれほど精度は高くないと思いますので自宅で見る分は十分かと思います。

国別パネルの作成

集計していたメトリックスを使って表示させています。設定内容は以下の要領です。

No.項目 設定内容備考
1Metricssum(delta(promtail_custom_router_log_country{country!=""})) by (country) > 0Query
2Legend{{country}}Query
3FormatTime seriesQuery
4Panel titleCountry TrafficPanel
5VisualizationStatPanel
6CaluculationMaxPanel
7FieldsNumeric FieldsPanel
8Text modeAutoPanel
9UnitshortField
10Color schemeGreen-Yellow-Red(by value)Field

市別パネルの作成

集計していたメトリックスを使って表示させています。設定内容は以下の要領です。

No.項目 設定内容備考
1Metricssort_desc(sum(delta(promtail_custom_router_log_city{city!=""})) by (city) > 0)Query
2Legend{{city}}}Query
3FormatTime seriesQuery
4Panel titleCity TrafficPanel
5VisualizationBar gaugePanel
6CaluculationMaxPanel
7FieldsNumeric FieldsPanel
8OrientationHorizontalPanel
9Dislplay modeRetro LCDPanel
8UnitshortField
10Color schemeGreen-Yellow-Red(by value)Field

Request/Sec パネルの作成

集計していたメトリックスを使って表示させています。設定内容は以下の要領です。

No.項目 設定内容備考
1Metricssum(rate(promtail_custom_router_log_country{country!=""}[1s])) by (country)Query
2Legend{{country}}}}Query
3FormatTime seriesQuery
4Panel titleRequest/SecPanel
5VisualizationGraphPanel
6showOnPanel-Axes-LeftY
7Unitrequests/sec(rps)Panel-Axes-LeftY
8Scalelog(base 10)Panel-Axes-LeftY
9showOnPanel-X-Axis
10ModeTimePanel-X-Axis
11Unitrequests/sec(rps)Field

以上で完了です。今回は、アクセス元の IP アドレスを Rsyslog を使って緯度経度に変換して Grafan の Worldmap Panel で実現してみました。マップでの表示により視覚的でわかりやすくなったと感じます。GeoIP2 の IPv6 対応はされているようですが、どの程度の範囲がカバーされているのか、また今回の Rsyslog では IPv6 にはまだ対応していないかと思われます。IPv6 のアクセスも最近多くなっていますので、こちらは課題として残しておきたいと思います。