LGTM

Looks Good To Me

高速にnetflow を吐く

最近 netflow コレクターをさわる機会が多く「コレクターをベンチマークしたい」と思っている. 「netflow v5 なら何 flow/s まで受けられる」「v9 ならこのくらい」など,性能限界を知っておきたい.

2016/02/20 追記: NetFlow-Generator

戦略

いくつか考えられる方法があって

  1. ネットワークデバイスのサンプリングレートを変え,多めにnetflow export する
  2. netflow データを用意し,高速に送信する

1 は実網に影響あるかもしれないためパス.2 の方法にする. netflow データの作り方もいくつか思いつく.

2-1. ネットワークデバイスが吐いたnetflow を再利用する
2-2. ウソデータをでっち上げる

ネットワークデバイスが吐いたnetflow を再利用する

nfdump というツールを利用する.

$ nfcapd -l . -p <ポート>

データが溜まるのを待つ.1M flowほどあれば十分だが,溜まるのに時間がかかる場合は-t <interval> オプションをつけておく. これはnetflow データを1 ファイルにまとめるため.(デフォルトでは5分でファイルローテートしてしまう)

1M flowも溜めるのは,netflow データストアによって重複netflow を無視する可能性があるため. なるべく重複レコードが少ないほうがよい.

実行すると nfcapd.201602180335 のようなファイルができ,

$ nfreplay -H <IP アドレス> -p <ポート> -r nfcapd.201602180335

のようにするとnetflow パケットを送信できる.デフォルトではnetflow v5 で,-v9 オプションをつけるとnetflow v9 になる.

OSX の場合は

Error sending data: No buffer space available

のようなエラーが出るかもしれない.手元ではLinuxからnetflow を吐き,ベンチマークした.

ウソデータをでっち上げる

nfdump に加え,flow-tools というツールを利用する.

$ nfcapd -l .

しておき,

$ flow-gen -n 1000000 -V 5 | flow-send 0/127.0.0.1/9995

すると各フィールドがシーケンシャルなnetflow データが作れる.

srcIP            dstIP            prot  srcPort  dstPort  octets      packets
0.0.0.0          255.255.0.0      17    0        65280    1           1
0.0.0.1          255.255.0.1      17    1        65281    2           2
0.0.0.2          255.255.0.2      17    2        65282    3           3
0.0.0.3          255.255.0.3      17    3        65283    4           4
0.0.0.4          255.255.0.4      17    4        65284    5           5
0.0.0.5          255.255.0.5      17    5        65285    6           6
0.0.0.6          255.255.0.6      17    6        65286    7           7
0.0.0.7          255.255.0.7      17    7        65287    8           8
0.0.0.8          255.255.0.8      17    8        65288    9           9
0.0.0.9          255.255.0.9      17    9        65289    10          10
0.0.0.10         255.255.0.10     17    10       65290    11          11
0.0.0.11         255.255.0.11     17    11       65291    12          12
0.0.0.12         255.255.0.12     17    12       65292    13          13
  • flow-gen が作るnetflow のタイムスタンプはエポックタイムで0 だが,nfcapd が一定時間シフトさせてしまう (バグの可能性ある)
  • flow-gen が作るnetflow は30 flow/pkt
    • MTU1500 ではこれが限界
    • 変更するオプションはなさそう

これをネットワークデバイスが吐いたnetflow を再利用する と同様にnfreplay すればよい. 手元のFE NIC 環境ではwire rate 出た.(およそ 250k flow/s)

さらにスピードを出す

250k flow/s も出れば十分すぎるが,さらにコレクターをいじめるためにtcpreplay をつかう. 前述のnfreplay 時にパケットキャプチャーしておき,

$ tcpreplay -t -i eth0 nfreplay.pcap

すれば1G NIC 環境でもwire rate 出た.(およそ 2.5M flow/s)

ちなみにdst IP アドレス,dst MAC アドレスを変えつつ送信してもwire rate 出る.

$ tcpreplay-edit -tC -i eth0 -D 0.0.0.0/0:<IP アドレス>/32 --enet-dmac <MAC アドレス> nfreplay.pcap

netmap を使えば10Gbps ちかく出る可能性 はあるが,やめてあげてください.コレクターが動くわけがない.

スピードを調整する

tcpreplay が便利なのは送信スピードを変えられること.

-x string, --multiplier=string
-p number, --pps=number
-M string, --mbps=string

オプションを使う.

課題

nfreplay を使った方法ではIPFIX,sflow を吐けない.対応しているのはnetflow v5/v9 のみ. 実パケットをキャプチャーし,tcpreplay で高速送信する方法ならOK.

2016/02/20 追記: NetFlow-Generator

NetFlow-Generator というのがあるよ」と教えてもらいました.

ウソデータをでっち上げる ときのflow-tools (のflow-gen) の代わりに使える.

  • タイムスタンプをいじれる
  • flow/pkt をいじれる
  • flow-gen → nfcapd でフローデータ保存 → nfreplay と同等のスループットで動く

柔軟でよさそう.

$ flowgen -h
Usage: flowgen [options] [flowrec-options] <collector>
 options:
   -n, --count <num>
   -p, --port <num>
   -V, --version <version>
   -f, --flowrec <# of flow records in packet>
   -d, --debug <debug level>
   -N, --nosend
   -h, --help
 flowrec-options:
   -w, --wait <wait time>
   -i, --interval <interval>
   --enginetype <engine type>
   --engineid <engine id>
   --srcaddr <src ip address>
   --dstaddr <dst ip address>
   --nexthop <nexthop ip address>
   --inputif <input IfIndex>
   --outputif <output IfIndex>
   --packets <# of packets>
   --octets <# of octets>
   --firstseen <first seen>
   --lastseen <last seen>
   --srcport <src port>
   --dstport <dst port>
   --tcpflags <tcp flags>
   --protocol <protocol number>
   --tos <tos value>
   --srcas <src AS#>
   --dstas <dst AS#>
   --srcmask <src subnet mask length>
   --dstmask <dst subnet mask length>

  Numbers can be expressed using the following meta characters:
    111      (static)
    111-222  (sequential)
    111:222  (random)
    100@70,200@20,300@10   (probabilistic)

junoser internals

junoser というRuby gem をメンテしている. これはJUNOS Config のPEG パーサーで,標準入力やファイルを JUNOS Config として構文解析できる.解析後,文法チェックやスタイルの変換もできる.

たとえば,

構文チェック.display | set 形式とデフォルトの構造化スタイル両方をサポート.

$ junoser -c config.invalid
Invalid syntax:  set protocols bgp group e1 neighbor 10.0.26.1 exprot s2b
zsh: exit 1     junoser -c config.invalid

スタイルの変換.display | set スタイル → 構造化スタイル

junoser -s config.set

スタイルの変換.構造化スタイル → display | set スタイル

junoser -d config

さっとチェックしたり,CI に使ったり,レビュー時 display | set スタイルがつらい時に重宝してる.

文法定義はどこから来るか

CLI と,NETCONF 用のXML が構造的に似ていることを利用して,<get-schema> で取れるXML Schema(XSD) を使っている.

<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" xmlns:junos="http://xml.juniper.net/junos/12.1X47/junos">
  <xsd:import schemaLocation="junos.xsd" namespace="http://xml.juniper.net/junos/12.1X47/junos"/>
  <xsd:element name="configuration">
    <xsd:complexType>
      <xsd:sequence>
        <xsd:choice minOccurs="0" maxOccurs="unbounded">
          <xsd:element name="interfaces" minOccurs="0">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:choice minOccurs="0" maxOccurs="unbounded">
                  <xsd:element name="interface" minOccurs="0" maxOccurs="unbounded" type="interfaces-type">
                  </xsd:element>
                </xsd:choice>
              </xsd:sequence>
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:sequence>
    </xsd:complexType>
  </xsd:element>
  <xsd:complexType name="interfaces-type">
    <xsd:sequence>
      <xsd:element name="name">
        <xsd:complexType>
          <xsd:simpleContent>
            <xsd:restriction base="key-attribute-string-type">
              <xsd:enumeration value="$junos-interface-ifd-name">
              </xsd:enumeration>
              <xsd:enumeration value="interface-name">
              </xsd:enumeration>
            </xsd:restriction>
          </xsd:simpleContent>
        </xsd:complexType>
      </xsd:element>
      <xsd:choice minOccurs="0" maxOccurs="unbounded">
        <xsd:element name="unit" minOccurs="0" maxOccurs="unbounded">
          <xsd:complexType>
            <xsd:sequence>
              <xsd:element name="name">
                <xsd:complexType>
                  <xsd:simpleContent>
                    <xsd:restriction base="key-attribute-ulong-type">
                      <xsd:enumeration value="$junos-underlying-interface-unit">
                      </xsd:enumeration>
                      <xsd:enumeration value="$junos-interface-unit">
                      </xsd:enumeration>
                      <xsd:enumeration value="interface-unit-number">
                      </xsd:enumeration>
                    </xsd:restriction>
                  </xsd:simpleContent>
                </xsd:complexType>
              </xsd:element>
              <xsd:choice minOccurs="0" maxOccurs="unbounded">
                <xsd:element name="family" minOccurs="0">
                  <xsd:complexType>
                    <xsd:sequence>
                      <xsd:choice minOccurs="0" maxOccurs="unbounded">
                        <xsd:element name="inet" minOccurs="0">
                          <xsd:complexType>
                            <xsd:sequence>
                              <xsd:choice minOccurs="0" maxOccurs="unbounded">
                                <xsd:element name="address" minOccurs="0" maxOccurs="unbounded">
                                  <xsd:complexType>
                                    <xsd:sequence>
                                      <xsd:element name="name">
                                        <xsd:complexType>
                                          <xsd:simpleContent>
                                            <xsd:extension base="ipv4prefix">
                                              <xsd:attribute name="key" type="xsd:string" fixed="key"/>
                                            </xsd:extension>
                                          </xsd:simpleContent>
                                        </xsd:complexType>
                                      </xsd:element>
                                    </xsd:sequence>
                                  </xsd:complexType>
                                </xsd:element>
                              </xsd:choice>
                            </xsd:sequence>
                          </xsd:complexType>
                        </xsd:element>
                      </xsd:choice>
                    </xsd:sequence>
                  </xsd:complexType>
                </xsd:element>
              </xsd:choice>
            </xsd:sequence>
          </xsd:complexType>
        </xsd:element>
      </xsd:choice>
    </xsd:sequence>
  </xsd:complexType>
</xsd:schema>

上記のXSD はごく一例*1だが,XML パーサーを使って,これを下のruby コードに変換する. (junoser に含めたパーサージェネレーターで自動生成)

module Junoser
  class Parser < Parslet::Parser
    rule(:configuration) do
      c(
        b(str("interfaces"),
          c(
            interfaces_type
          )
        )
      )
    end

    rule(:interfaces_type) do
      b((arg | str("interface-name")).as(:arg),
        c(
          a(str("unit"), arg | arg | str("interface-unit-number"),
            c(
              b(str("family"),
                c(
                  b(str("inet"),
                    c(
                      b(a(str("address"), arg))
                    )
                  )
                )
              )
            )
          )
        )
      )
    end
  end
end

全体で60万行くらいのXSD が,4万行くらいのPEG パーサーになる.これがjunoser の本体だが,パーサージェネレーターの生成物でもある.

パーサージェネレーターの闇

ここから本題です.ここまでは比較的スッキリと書ける.処理する対象がゴチャゴチャしているため一見ややこしいが,やっていることはシンプル.

問題1: CLI とXSD の微妙なちがい

interfaces {
    ge-0/0/0 {
        description "interface 1";
    }
}

上のようなJUNOS Config がある.よく見ますね. description ステートメントの後ろに文字列が来る. これをdisplay | xml すると,

<interfaces>
    <interface>
        <name>ge-0/0/0</name>
        <description>interface 1</description>
    </interface>
</interfaces>

なるほど,<description/> 要素に文字列が入っている.

routing-options {
    autonomous-system 64500;
}

いっぽう,上のようなJUNOS Config がある. autonomous-system ステートメントの後ろに文字列が来る. これをdisplay | xml すると,

<routing-options>
    <autonomous-system>
        <as-number>64600</as-number>
    </autonomous-system>
</routing-options>

なるほど,こちらは<autonomous-system/> 要素の中で,新たに<as-number/> 要素で文字列を包んでいて interface description と違う.

では2つのXSD を見てみましょう.

<xsd:complexType name="interfaces-type">
  <xsd:sequence>
    <xsd:choice minOccurs="0" maxOccurs="unbounded">
      <xsd:element name="description" minOccurs="0" type="xsd:string"/>
    </xsd:choice>
  </xsd:sequence>
</xsd:complexType>
<xsd:element name="autonomous-system" minOccurs="0">
  <xsd:complexType>
    <xsd:sequence>
      <xsd:choice minOccurs="0" maxOccurs="unbounded">
        <xsd:element name="as-number" type="xsd:string"/>
      </xsd:choice>
    </xsd:sequence>
  </xsd:complexType>
</xsd:element>

ほぼ同じなわけです.「as-number はただのラベルであって,CLI 上には現れないこと」がXSD からは分からない.

junoser では文脈をくみとりながら「これはただのラベル要素」「これは必要な要素」と選別していて,そこが泥臭いハックになっている.

問題2: そもそもXSD にない定義

junoser はvSRX のXSD をベースにしているため,vSRX にない機能はパースできない.

<xsd:element name="service-set" minOccurs="0" maxOccurs="unbounded">
  <xsd:complexType>
    <xsd:sequence>
      <xsd:choice minOccurs="0" maxOccurs="unbounded"/>
    </xsd:sequence>
  </xsd:complexType>
</xsd:element>

たとえば,上はset services service-set ... に対応するXSD だが,vSRX には定義が存在しない.

問題3: ライセンス

XSD で定義される構文木に泥臭いハックを入れて,CLI にあてはめるためのパーサーを作った.このコードのライセンス,微妙ですよね. いちおうJuniper のかたに確認して公開しているが…大丈夫かな? 詳しいかたがいればアドバイスいただけると嬉しいです.

Juniper はよくてもCisco がNG とかだと,IOS-XR 版が作れなくて困ってしまう.

あと,junoser は個人プロジェクトとして進めているので,Juniper にフィードバックしたくてもできないことがあって困ってる.

おわりに

junoser をメンテするにあたって,けっこう泥臭いことをやっています.でも,けっこう動くレベルのはずです.

もし興味をもってもらえたら ぜひ使ってみてください.パースできないCLI コマンドがあれば,フィードバックよろしくお願いします.自分ではあまり使わないConfig については,問題1 を潰しきれていません.

また,手元にXSD がないのが原因で動かないケース(問題2) もあるので,その際にはXSD の提供に協力してもらえると助かります.

こちらが安定してきたら,IOS-XR 版にも着手予定です.

*1:関係のない要素も省略

netconf / restconf 時代にもxlogin 使いまくってる話

NetOpsCoding Advent Calendar 2015 18日目のエントリーです. ハードウェア製品を使ったネットワーキングの話.

さまざまなコンポーネントについて YANG Model の標準化が進み,NETCONF 使える気運が高まっています.さらにRESTCONF も待っていて,ハードウェアで高速にルーティングしつつ運用を自動化できそうな予感がしています.

私は,条件がそろえばNETCONF を使います.XML といえど エラーや出力が構造化されていて きちんと取れるのはうれしい.

一方で 日常のネットワーク運用はCLI に頼っていて,判断をはさみつつ手続き (= コマンド) を変えます.このような用途にはNETCONF / RESTCONF はたぶん向きません.大がかりすぎです.

今回は,自動化できないときでも便利な xlogin を紹介します.

( ようやく,これ自動化の話じゃないわ と気づきました… 気にせず行きます すみません )

xlogin ?

rancid に同梱されている clogin, jlogin, flogin, ... のことをまとめて xlogin と呼んでいます.これらはrancid のためのサポートツールで,各種デバイスにログインするためのexpect スクリプトセットです.

ssh / telnet を薄くwrap するだけの単純なexpect ですが,めちゃ便利なので, いまはネットワークデバイスにログインするとき ssh / telnet を呼ぶことはほとんどありません.たぶんrancid 本体より便利です.

つかいかた

ログインする例 (IOS 系)

xlogin はデバイス種別ごとに実行ファイルが分かれており,IOS 系にはclogin を使います.

clogin <デバイス名>

ログインする例 (IOS 系)

パスワードを自動入力しつつ telnet でログインしています.よく見れば,ログインと同時にenable になっていることがわかると思います.

上はQuagga の例ですが,IOS でも同じ動きです.

複数デバイスにコマンド実行する例 (Junos)

複数のJunos に対し,show version を実行します.Junos にはjlogin を使います.

jlogin -c "show version" <デバイス名1> <デバイス名2>

複数デバイスにコマンド実行する例 (Junos)

2台のJunos に順にssh ログインし,show version を実行し,ログアウトします.結果は標準出力されるので,grep に渡したり,ファイルにリダイレクトしたり,| vi - して開く → 編集 → 保存したりできます.

よく見ると ssh のまえにtelnet を試しているのがわかると思いますが,xlogin はデフォルトでは「telnet / ssh 両方を試してログインできるほうを使う」という動きをします.

初めてssh するデバイスについては,自動でhostkey のfingerprint を~/.ssh/known_hosts に保存します.fingerprint が違っていることを検出した場合はログインしません.

複数コマンドを実行する例 (IOS-XR)

IOS-XR にログインし,interface description を変更します.

! ./cmd
show interfaces description

configure
interface Gi0/0/0/0
description interface1
commit
exit
exit

show interfaces description
exit

上のような一連のコマンド群を実行する必要がありますが,ファイルに書いておいてxlogin に渡すだけ.

clogin -x <コマンドファイル> <デバイス名>

複数コマンドを実行する例 (IOS-XR)

単純に逐次実行するだけなので,失敗時にrollback しない点に注意してください

  • バイス固有のコメント行をいれておくと便利
  • 引数をふやせば,複数デバイスに対して実行できる
  • jlogin -c "show version; show chassis hardware" router1 のようにコマンドラインからも渡せる
  • vty にコピー&ペーストしたときのバッファ溢れが気になる時など,vi cmd; clogin -x cmd <デバイス名> すれば安心.プロンプトが返ってきたら1行実行,また1行実行…という動きをする

以上,xlogin の使用例をならべてみました.

clogin, jlogin, ... を使い分けるのは 少々めんどうですが,便利そうじゃないですか?

対応デバイス

xlogin は対応デバイスが多くてスゴい.めんどくさいプロンプト処理をやってくれてます.

xlogin Vendor
alogin Alteon WebOS switch login
anlogin Arbor Networks login
avologin Avocent login
blogin Bay Networks(Nortel) login
clogin Cisco login
complogin Compass login
dllogin D-Link login
elogin ADC EZT3 login
flogin foundry login
fnlogin fortinet login
hlogin hp login
htlogin Hitachi router login
jlogin juniper login
mrvlogin MRV optical switch login
mtlogin MikroTik router login
nlogin netscreen login
nslogin Netscaler login
panlogin Palo Alto Networks login (based on alogin)
rivlogin Riverstone (and Enterasys SSR) login
rscmd Riverstone Networks Automated login
tlogin Netopis login
wlogin Cisco Wireless Lan Controller login
xilogin Xirrus login

残念なのは,github などにレポジトリがなくてソーシャルコーディングできないこと. ここにないデバイスをサポートしても,本家にマージしてもらうのがつらい.

認証情報

認証情報の持たせかたについても触れておきましょう.

認証情報は,あらかじめ~/.cloginrc という設定ファイルに書いておかないといけません.

  • ログイン方法 (ssh or telnet)
  • 接続先ポート
  • ユーザー名
  • ログインパスワード / enable シークレット
  • ...

などを書いておきます.ログインする際 「xlogin に渡したデバイス を上から順に探し,最初にマッチした設定を使う」という動きをします. サーチドメインをつけた名前や,A レコードを引いたあとのIP アドレスではないことに注意です.デバイス名にはワイルドカードが使えます.

# ~/.cloginrc

add method    router4  {telnet:2605}
add password  router4  {zebra} {zebra}

# default

add user      *        {koji}
add password  *        {login_passwd} {enable_secret}

~/.cloginrc が他者に読まれそうなパーミッションであれば,xlogin 実行時に警告します.

設定方法の詳細はrancid web サイト にあります.プロンプトのカスタマイズや enable するかどうかなど,けっこう細かく設定できます.

その他のヒント

インストール

  • deb パッケージがあるディストリビューションが多い.Debian, Ubuntu などはapt-get 一発
  • rpm パッケージは (たぶん)ないので,自分でつくらないといけない
  • OSX であれば brew にある
  • ping 127.0.0.1 の応答がないと./configure がコケる.OSXfirewall など注意
  • ソースパッケージからビルドしてもよい.なぜか./configure でxlogin が生成される.「./bin/*login だけ$PATH のどこかにコピー」とかよくやる

~/.cloginrc をやめる

ファイルに認証情報を書くのは セキュリティ上いまいちなので,私はvault に保管しています. 完全ではありませんが,他者からアクセスする機会を大幅に減らせますし,認証情報へのアクセスログが取れます.ずいぶんマシ.

やりかたは別のエントリー を参照してください.

まとめ

xlogin の使い方とヒントについて説明しました.

ログインするとき ssh / telnet がわりに使うのはもちろんですが,自動でなにかするコードを書くときにも重宝しています. そのコードには「なにかする」ことだけを記述し,認証とコマンド実行をxlogin に切り出せば 少ない手数で見通しよくなりますし,認証レイヤーを共通化できます.

NETCONF を使いたくても,サポートしていないデバイスがあったりで xlogin + CLI パーサー を選ぶ局面はまだまだありそうです.残念ですけどね.

条件を満たすとチャット通知するリマインダーをつくる

NetOpsCoding Advent Calendar 2015 3日目のエントリーです.

最近はネットワーク運用をラクにすることに興味があります.

たとえば RESTCONF のようなI-D が標準され,進化すれば ネットワークへの自動デプロイが実現できると思いますが,それはネットワーク運用の一部でしかありません.

スコープをネットワーク運用だとすれば

  1. オーダーをもらう
  2. ネットワークをつくる
  3. 監視する
  4. トラブル対応する
  5. ネットワークを拡張する
  6. 廃止する

このようなワークフロー全体をラクに回したい.デプロイツールは1つのコンポーネントなのですが,コンポーネントの進化に比べて ワークフローが進化していない(っぽい) ことに少し懸念を持っています.

雑に言えば「このコンポーネントと このコンポーネントを組み合わせれば,ほら,こんなに便利」という知見が足りないと感じています.

前置きが長くなりました.今回はそういう流れの話にしたいと思い, いろんなコンポーネントを組み合わせて リマインダーをつくってみようと思います.

やりたいこと

ネットワークを運用していると「次のタイミングでやりましょう」ということが結構あります.多くの場合は やりたいことを覚えておいて,なにかの際に,もしくは ときどき「あれ,やれるかな」というふうに判断しています.

ただ,残念ながら「あれ」が増えれば覚えきれないし 小さいことは忘れてしまいます.「タイミングがきたら絶対にここを見るから,ここに書いておこう」という場所は限られていますし,TODO リストの2ページ目以降は目に触れることすらありません.

次のステップとしては,多数の「〜のときに〜したい」を覚える代わり,「〜になったら,〜になりそうなら 勝手に通知がくる」が望ましいと思っています.

〜になったら / 〜になりそうなら のパターン

通知が欲しいタイミングと,情報ソースについてはいくつかのパターンが考えられます.

  • 定期ポーリングで条件判定すれば十分

    • ネットワークの状態から判定
    • コンフィグから判定
    • ログから判定
    • 手順書から判定
  • リアルタイムイベントをつかまえて条件判定

    • ログから判定
    • メールなどの通知時に判定

これから,いくつかの実装例を紹介します.

ポーリング: ネットワークの状態から判定

特定ポートからリンクがすべて抜けたら,ハードウェアを交換したいというケースがありますね.

というフローを考えます.

準備

Slack の場合,単純な通知であればSlackbot がおすすめです.

Slackbot

ほかにもIncoming Webhook やReal Time Messaging API がありますが,Slackbot のみ 同居しているHubot にコマンドを送れます.ここでは紹介しませんが「Slack に通知があったらHubot が勝手に手順書をつくってくれる」など 次のワークフローを回すことができるよう,Hubot を起こせるほうをおすすめします.*1

次にSlack 通知用のシェルスクリプトを準備します.トークンが含まれているのでchmod 700 しておきましょう.

# notify.sh

#!/bin/sh

notify() {
  curl -sfd "$1" 'https://codeance.slack.com/services/hooks/slackbot?token=ccvIktQ90VrBP03gzsA6WsJK&channel=%23test' > /dev/null
}

最後に条件判定スクリプト.ネットワークデバイスの状態を取ってくる部分は NETCONF でもNetOpsCoding Advent Calendar 2日目の例 でもなんでも構いません.今回はrancid に含まれるjlogin を使いました.

# poll.sh

#!/bin/sh

. ./notify.sh

poll() {
  jlogin -c 'show interfaces ge-0/0/* descriptions' 192.168.0.76 | egrep \^ge-0/0 | grep -v down > /dev/null
}

poll || notify 'PIC 0/0 交換のチャンスです'

実行例

ge-0/0/* がすべてdown であれば通知されます.

Slack

このスクリプトを,cron.daily のようにrun-parts 経由でcrond から実行すればOK.

リアルタイムイベント: syslog から判定

特定のBGP ピアでは,落ちたときに急ぎで手動オペレーションしないといけない場合があります.

  • ルーティングを確認したいが,ホントに落とすと怒られる → 自然に落ちたときに確認したい
  • max-prefix limit で落ちたら手動でclear しないといけない

残念ながら,いまのところsyslog 以上に有力なプッシュ通知はないと思いますので

  • syslog で特定のイベントを受信したら,HipChat に通知

というフローを考えます.zabbix やsensu のような監視システムを使っている場合は イベントハンドラーを書くことで簡単に実装できますが,syslog-ng などでも難しくありません.大人の事情で監視機能が外部組織にあっても大丈夫.

準備

HipChat であれば カスタムIntegration を使います.Slack とは違って,どのAPI でもHubot にコマンドを送ることができます.

HipChat Integration

次にHipChat 通知用のシェルスクリプトを準備します.トークンが含まれているのでchmod 700 しておきましょう.

# /home/codeout/hipchat/notify.sh

#!/bin/sh

notify() {
  curl -fd "{\"color\":\"green\",\"message\":\"$1\",\"notify\":false,\"message_format\":\"text\"}" -H 'Content-Type: application/json' 'https://codeance.hipchat.com/v2/room/1301314/notification?auth_token=0uOU8ATolyOMpBGx9P04bjLytOcGnZVm8FugRUEA'
}

最後に条件判定スクリプト.syslog-ng の外部プログラムとして動作させます.

# /home/codeout/hipchat/watch.sh

#!/bin/sh

. /home/codeout/hipchat/notify.sh

filter() {
  echo "$1" | egrep -v 'peer 192.168.0.73.*changed state from Established' > /dev/null
}

while read l; do
  filter "$l" || notify 'ピア(192.168.0.73) 落ちた.経路確認だ'
done

条件判定が成功して通知 / 条件判定が失敗 のどちらのケースでも0 を返せるよう,egrep something && notify よりegrep -v something || notify にします.

泥臭いパターンマッチングをしなくていいよう,構造化されたデータでもらいたいのはNetOpsCoding Advent Calendar 1日目のエントリー のとおりですね.

最後にsyslog-ng の設定例.

# /etc/syslog-ng/conf.d/watch.conf

source s_router {
  udp();
};

destination watch {
  program("/home/koji/hipchat/watch.sh");
};

log {
  source(s_router);
  destination(watch);
};

実行例

BGP ピアが落ちると,HipChat に通知が飛びます.

HipChat

ポーリング: コンフィグから判定

たとえば,トランジットやピア一覧に変化があった場合にリマインドして欲しい場合があります.「連絡先一覧 アップデートした?」のように管理情報の更新を促してもらいたい.

「コンフィグはGitLab で管理されていて,適宜更新されている」という仮定のもと

  • 定期的にgit fetch
  • トランジットやピア一覧に変化があったらSlack に通知

というフローを考えます.

準備

GitLab 上のレポジトリを,仮にssh://git@192.168.99.101:10022/codeout/netops.git とします.

まず 手元にgit clone

$ git clone ssh://git@192.168.99.101:10022/codeout/netops.git

さらに 空パスフレーズssh key を作り,deploy key として登録しておいてください.

詳しい説明は省略しますが,実装例を diff.rb に置きました. ここではJuniper を仮定していて,description "T: Transit A" のようなタグ(T:) を探す動作になっています.また,display-set 形式のほうがdiff に適しているため,junoser を使って変換しています.

内部にssh private key やSlack トークンを書いているので,chmod 700 しておきましょう.

実行例

トランジットやピア一覧に変化があった場合のみ,diff とともにSlack に通知が飛びます.

Slack

リアルタイム: 手順書から判定

最後に「〜になったら」ではなく「〜になりそうなら」の判定を考えてみます.たとえば「デバイスを再起動するときにあれもやりたい」というのはけっこうあるはず.

仮にGitLab で手順書をつくっているとすると Issue やMerge Request などに手順情報が含まれているはずですので,何かしらの方法でこれを見て判定すればよさそうです.

GitLab にはうまいぐあいにWebHook というしくみがあり,データを更新した際 あらかじめ指定したURL にJSON データをPOST させることができます.JSON データのスキーマはGitLab 独自です.

このWebHook を使えば,リアルタイム判定になってしまいますが手順書から「〜になりそうなら」を抽出することができそうです.*2 このWebHook は,GitHub など 流行のサービスにはだいたい備わっていますので,JSON スキーマにさえ注意すれば同じしくみが使えます.

さて,

  • GitLab のIssue / Merge Request のタイトルやコメントに「再起動」「reboot」の文字があったらSlack に通知する

ということをやってみたいと思います.

準備

GitLab 上でWebHook を設定します.そのURL で待ち受けるWebサービスを,これから作ることになります.

GitLab WebHook

単純な実装例を endpoint.rb に置きました.実際はApache やNginx などと連携して動くWeb アプリが好ましいですが,コンセプトの理解には十分でしょう.

これを実行すると ポート8080 でHTTP リクエストを受け付け,GitLab からPOST されたデータを見て 条件に合う場合のみSlack に通知します.

内部にSlack トークンを書いているため,動かす際にはchmod 700 しておきましょう.

実行例

50行程度の短いコードですが,きちんと動きます.

GitLab WebHook

Merge Request データには2つのブランチ名が入っており,ポーリング: コンフィグから判定 のやりかたの応用でMerge Request 中のdiff を見ることもできます.

まとめ

利用可能なコンポーネントをつかって,人間の代わりにコンピューターに覚えといてもらう しくみをいくつか紹介しました.今回の例ではワークフローをラクに回す ところまでいきませんでしたが,人間が苦手な部分 (=記憶) と,単純判断をコンピューターに任せることができるので 少し幸せになれそうです.

さらに言えば,もう次のステップが見えてますね.

「〜のときに〜したい」の後半.アクション部分.通知の代わりに単純作業をやらせましょう.もしくは 作業ための下準備をやらせましょう.

自動でやっていいこと,結構ありそうじゃないですか?

*1:rate limit にだけ注意.現在 1 メッセージ/s

*2:もちろんポーリング動作にもできますが,実装が複雑になる

rancid の.cloginrc をvault 管理にする

rancid というツールがある.ネットワーク運用するにあたって,もはや手放せなくなっている.rancid のなにが便利かについては別エントリで書こうと思うが,本来の目的よりも付随する xlogin スクリプト (clogin, jlogin, …) を気に入って使っている.

xlogin について簡単に言えば

  1. ネットワークデバイスへのログインを自動化する
  2. 指定したコマンド群を逐次実行し,標準出力に出す
  3. スクリプティングによって実行コマンドを制御する

ようなやつで,1. のための認証情報を~/.cloginrc に書いておく必要がある.パスワード入力が不要になるので「10 台のルーターshow interfaces descriptionsshow bgp summaryshow ospf neighborを打ってgrep に渡す」とか「100 台のルーターでsyslog の設定を変える」みたいなことが簡単にできる.

問題意識

ネットワークデバイスにログインするために 共有のジャンプサーバーを用意することが多く,xlogin をつかうには そこに~/.cloginrc を置く必要がある.

「共有サーバーに ファイルとして認証情報(id + password) を置く」というのがイマイチだなあと思っていた.*1 というのは,

そこでvault を手元のローカルホストで動かしておいて,.cloginrc の情報をそこに格納しておくと ずいぶんマシになった.

  • super-user に読まれてしまうのは変わらない
  • 認証情報の実体がローカルホストにあり,暗号化される.ssh セッションを切れば認証情報にアクセスできなくなる
  • 認証情報へのアクセスログが残るので,不正アクセスに気づける

vault の設定

セットアップ (ローカルホスト)

プラットフォームにあったvault をダウンロード し,PATH のどこかに置く.

設定ファイルを書く.

# ~/.vault

backend "file" {
  path = "/Users/codeout/.vault.d"
}

listener "tcp" {
  address = "localhost:8200"
  tls_disable = 1  # ssh tunnel を通すのでTLS 不要
}

vault server を起動し,初期化する.

$ vault server -config ~/.vault >&/dev/null &
$ export VAULT_ADDR=http://localhost:8200

$ vault init -key-threshold 1 -key-shares 1
vault $ vault init -key-threshold 1 -key-shares 1
Key 1: ced979c0b1c0ff62b2ac2aa08e54b8258bd4d88170ed67627b4c0fc3dd87133c
Initial Root Token: b5aae184-0fcf-3f7d-09db-9928d82eca56

Vault initialized with 1 keys and a key threshold of 1. Please
securely distribute the above keys. When the Vault is re-sealed,
restarted, or stopped, you must provide at least 1 of these keys
to unseal it again.

Vault does not store the master key. Without at least 1 keys,
your Vault will remain permanently sealed.

ローカルホストでの利用なので master key は1つにした.

デフォルト設定ファイルの評価タイミングが雑っぽくて,今日時点では-config ~/.vaultexport VAULT_ADDR=http://localhost:8200 を省略することができない.そのうち~/.vault がきちんと評価されるようになって,省略可能になる気がする.

起動直後はsealed なので unseal しておく.

$ vault unseal ced979c0b1c0ff62b2ac2aa08e54b8258bd4d88170ed67627b4c0fc3dd87133c
Sealed: false
Key Shares: 1
Key Threshold: 1
Unseal Progress: 0

ログの設定.

$ mkdir ~/.vault.d/log

vault $ vault audit-enable file path=/Users/codeout/.vault.d/log/vault_audit.log
Successfully enabled audit backend 'file'!

.cloginrc をvault に格納する.組織ごとに異なる.cloginrc を1つのvault に入れる必要があるので,key を分ける.ついでにpolicy も分けておいて 必要最小限のデータにのみアクセスを許可する.

チームA の.cloginrcsecret/team-a に格納する例:

$ vault auth b5aae184-0fcf-3f7d-09db-9928d82eca56
Successfully authenticated!
token: b5aae184-0fcf-3f7d-09db-9928d82eca56
token_duration: 0
token_policies: [root]
# ./team-a.hcl

path "sys/*" {
  policy = "deny"
}

path "secret/*" {
  policy = "deny"
}

path "secret/team-a" {
  policy = "write"
}

path "auth/token/lookup-self" {
  policy = "read"
}
$ vault policy-write team-a team-a.hcl
Policy 'team-a' written.

$ vault write secret/team-a cloginrc=@/Users/codeout/.cloginrc
Success! Data written to: secret/team-a

最後に,ポートフォワードと環境変数を転送するよう~/.ssh/config を設定しておく.

# ~/.ssh/config

Host            jump-server
  Hostname      192.168.3.102
  RemoteForward 8200 localhost:8200
  SendEnv       VAULT_TOKEN
確認

5分で失効するトークンを発行して ローカルホストから値が取れることを確認する.

$ vault token-create -lease=5m -policy=team-a
Key             Value
token           ccc70554-da81-4d2f-08be-d0d38232395c
token_duration  300
token_renewable true
token_policies  [team-a]

$ VAULT_TOKEN=ccc70554-da81-4d2f-08be-d0d38232395c vault read secret/team-a
Key             Value
lease_duration  2592000
cloginrc        ### default
add method    *   {ssh} {telnet}
add user      *   codeout
add password  *   {some!secret} {some!secret}

add method    192.168.0.79  {telnet}
add password  192.168.0.79  {another!secret} {another!secret}

ログが残ることも確認.

{"time":"2015-11-28T15:29:11Z","type":"request","auth":{"display_name":"token","policies":["team-a"],"metadata":null},"request":{"operation":"read","path":"secret/team-a","data":null,"remote_address":"127.0.0.1"},"error":""}
{"time":"2015-11-28T15:29:11Z","type":"response","error":"","auth":{"display_name":"","policies":["team-a"],"metadata":null},"request":{"operation":"read","path":"secret/team-a","data":null,"remote_address":"127.0.0.1"},"response":{"secret":{"lease_id":""},"data":{"cloginrc":"hmac-sha256:46ac8a23ac9efff3c42e5b9e31a7854e50f4a4e713810388003dbfcb1c9a607b"},"redirect":""}}

5分後には値が取れなくなっているはず.

セットアップ (ジャンプサーバー)

こちらも プラットフォームにあったvault をダウンロードし,PATH のどこかに置く.

.zshrc などにvault のURL を設定する.外部からジャンプサーバーにssh するときに張るssh tunnel を指定しておく.

# ~/.zshrc

export VAULT_ADDR=http://localhost:8200

rancid がインストールされているものとして,.cloginrc を書き換える.

.cloginrc はtcl スクリプトとして読まれるため,必要に応じてvault から認証情報を取得する動作になる.

# ~/.cloginrc

if [ catch {eval [eval exec "vault read -field=cloginrc secret/team-a"]} reason ] {
    send_user "\nError: $reason\n"
    exit 1
}

可能であれば,ssh クライアントから環境変数VAULT_TOKEN を受け付けるよう sshd を設定する.

# /etc/ssh/sshd_config

AcceptEnv LANG LC_* VAULT_TOKEN

つかいかた

ローカルホストでvault を起動し,トークンを発行する.-lease を指定しなければ30日有効なトークンが発行されるが,期間は適宜.

$ vault server -config ~/.vault >&/dev/null&
$ vault unseal ced979c0b1c0ff62b2ac2aa08e54b8258bd4d88170ed67627b4c0fc3dd87133c
$ vault auth b5aae184-0fcf-3f7d-09db-9928d82eca56

# 普段は上記の状態で動かしっぱなし

$ vault token-create -lease=24h -policy=team-a
Key             Value
token           d78015ad-e903-2122-dea9-6b1bcb970391
token_duration  86400
token_renewable true
token_policies  [team-a]

トークンを渡しつつ,ジャンプサーバーにssh

$ VAULT_TOKEN=d78015ad-e903-2122-dea9-6b1bcb970391 ssh jump-server

これでxlogin できるようになっているはず.

$ jlogin 192.168.3.103
192.168.3.103
spawn ssh -c 3des -x -l codeout 192.168.3.103
Password:
--- JUNOS 12.1X46-D10 built 2013-12-05 15:04:51 UTC
codeout@vsrx>

ローカルホストでvault がunsealed で動いており,ssh tunnel 経由でジャンプサーバーからvault にアクセスでき,トークンがある場合のみ認証情報にアクセスできるようになった. ローカルホストの~/.vault.d/log/vault_audit.log には認証情報へのアクセスがすべて記録されるので,不正アクセスの調査ができる.

別のチームB のジャンプサーバーにssh する場合は,vault に格納するkey, policy 名を切り替えればよい.

ジャンプサーバーのsshd を設定変更できない場合は クライアントから環境変数を渡せないので,ログイン後にexport VAULT_TOKEN=d78015ad-e903-2122-dea9-6b1bcb970391 する. このとき,一応コマンドヒストリーに残らないよう気をつける.たとえばzshhist_ignore_space が設定されている場合は,export の先頭に半角スペースを入れておけばヒストリーに残らない.

どうせsuper-user には/proc/<pid>/environps e などで環境変数を読まれてしまうが,いちおう余計なところに残さないほうがいい.

まとめ

.cloginrc をまるまるvault 管理にすることで,より安全にxlogin を使えるようになった.やはりsuper-user は認証情報にアクセスできてしまうため 完全ではないが

  • 認証情報の実体がローカルホストにあり,暗号化される.ssh セッションを切れば認証情報にアクセスできなくなる
  • 認証情報へのアクセスログが残るので,不正アクセスに気づける

構成を実現できた.

環境変数をsuper-user から隠す方法 もしくはうまく暗号化する方法があれば完璧なので,知見をお持ちのかたはぜひ教えてください.

*1:ssh + pubkey-auth + ssh-agent にすれば,そもそも~/.cloginrc に書かなくてよいのでは? という意見もあって,そのとおりなんだけど,super-user はssh-agent のsock-file を読めるので一緒だし,pubkey-auth 非対応のデバイスもあるし,enable password のようなplaintext なcredential も扱わないといけないので,~/.cloginrc を消すことは困難

Hubot にlongest match 機能をつける

ネットワーク運用をしていると,ホストアドレスから経路をルックアップしたいことが結構ある.

たとえばDDoS を受けたとき.いろいろな対処が考えられるが,網内のルーターでパケットフィルターする際に「src address → src prefix の変換をしてからガバッと止めたい」とか.

そんな面倒くさいことはBot にやってもらいたい.

実装のアイデア

ルーターにログインして変換してもいいし,ルーティングテーブルを別に持って検索してもいい.

ルーターにログインしてshow route するのはIO バウンドな処理なのでnode との相性はよさそうだが,「src address が100コ」みたいな場合では遅いと思われる.よくあるルーターだと「まとめて引数を渡して,まとめて結果を受け取る」みたいなことができないため「1コ引数を渡して,1コ結果を受け取る」を100回やらないといけない.

一方,ルーティングテーブルを別に持つのってどう実装しよう?とか自前でpatricia tree つくんの?という面倒くささがある.

MRT TABLE DUMP + patricia tree でやってみる

実は,後者の面倒くさいところを解決してくれるbgpdump2 というツールがある.「node バインディング書けばいいのでは」と思ってやってみたら,結構うまく動くっぽかった.(まだ最小限の実装しかできてないです)

github.com

これを使うとlongest match の結果を返してくれる.

var BGPDump = require('node-bgpdump2');
var bgpdump = new BGPDump('path_to_rib.bz2');
console.log(bgpdump.lookup('8.8.8.8'));

// =>
// { prefix: '8.8.8.0/24',
//   nexthop: '202.249.2.169',
//   origin_as: 15169,
//   as_path: '2497 15169' }

Hubot から使うのもカンタンで,

npm install node-bgpdump2 --save
npm install https://github.com/codeout/node-mrt.git --save

たとえばshow route モドキを返すやつ.

# scripts/route.coffee

BGPDump = require('node-bgpdump2')
MRT = require('node-mrt')
mrt = new MRT('wide')

module.exports = (robot) ->
  robot.respond /route (.*)/, (msg) ->
    mrt.get (text)->
      msg.send text
    , (path)->
      bgpdump = new BGPDump(path)
      msg.send JSON.stringify(bgpdump.lookup(msg.match[1]))

ちなみに node-mrt はarchive.routeviews.org からMRT アーカイブをダウンロードしてキャッシュしてくれるライブラリです.

もうちょい複雑なことを: DDoS 対策手順を作ってもらう

一例だけれど,たとえばDDoS を検知後に

  1. src address → src prefix に変換
  2. src prefix ベースで帯域制限する手順をつくり,GitHub Issue としてオープン

というところまでHubot にお願いしたい.
(今回は例としてsrc prefix だけの条件)

hubot-github-templated-issues というの流用すれば結構ラクにできて,8.8.8.8, 8.8.4.4, 64.6.64.6, 64.6.65.6 からのDDoS を検知したときに

hubot anti ddos packet_filter/ratelimit_2g DDoS対策
src: [
8.8.8.8,
8.8.8.8,
64.6.64.6,
64.6.65.6
]

とHubot にお願いすると,

手順書をIssue 化してくれる.
(↑ ではMRT アーカイブをダウンロードしているが,キャッシュがあればそれをつかう)

https://github.com/codeout/sandbox/issues/108

この手順書はテンプレート にパラメーターを埋めて作ったもので,hubot-github-templated-issues がよしなにやってくれます.

今回つかったスクリプト: scripts/anti_ddos.coffee

まとめ

node-bgpdump2 を使うと,Hubot にlongest match 機能をつけられてけっこう便利だと思う.

bgpdump2 自体,少しコンセプト実装っぽい側面があって完全にライブラリ化されていないため,できたら作者の方に今後のロードマップについて聞きたいところです.