note.

調べたことの頭の整理

Cumulus Linux - 基本的な L3 設定(FRR, Static) - 1

Cumulus Linux に興味が湧いたので触っています。ここでは頭の整理として基本的な部分をまとめました。

また、長くなったので記事を分割しました。

  • Cumulus Linux - 基本的な L3 設定(FRR, Static) - 1
  • Cumulus Linux - 基本的な L3 設定(OSPF) - 2
  • Cumulus Linux - 基本的な L3 設定(BGP) - 3

ここでは FRR と Static について扱います。

FreeRangeRouting

OSPF, BGP など一般的に求められる機能がサポートされていますが、そのドキュメントを読むと随所にFRRという単語が出てきます。

プロトコルの前にまずはFRRについて整理します。

概要

FRRとはQuagga(ソフトウェアルータ)をルーツとした LinuxFoundation が管理している Linux/Unix のプラットフォームに向けた IP Routing Protocol Suite です。簡単にいうとライブラリです。

名前の通り Free のためGitHubにソースが公開されています。

サポートしている RFC をみたいときにはFeatureMatrixが参考になるかもしれません。

このことから各種プロトコルの設定は FRR の設定ファイルに当たる /etc/frr/frr.confに設定されます。

アーキテクチャ

bgpd, ospfd, ospf6dといったようにプロトコル単位に daemon があり、それらをzebraが差分を吸収し、Kernel Routing Tableに経路情報を設定します。

利用している daemon は yes となります。

cumulus@leaf01:mgmt:~$ sudo cat /etc/frr/daemons
...
bgpd=yes
ospfd=yes
ospf6d=yes
ripd=no
ripngd=no
isisd=no
fabricd=no
pimd=no
ldpd=no
nhrpd=no
eigrpd=no
babeld=no
sharpd=no
pbrd=no
fabricd=no
vrrpd=no

このことから設定の変更を反映させるには各 daemon を再起動する必要がありますが、frr.serviceを再起動することで、結果的に依存している各種サービスも再起動することができます。

詳細はArchitectureを参照ください。

設定の変更について

Cumulus Linux のユーザーには3つの設定方法が用意されています。そのため、各種マニュアルには3つの方法が記載されています。

  • net(NCLU)コマンドを通してfrr.confに設定
  • vtsh(FRR)コマンドを通してfrr.confに設定
  • vi /etc/frr/frr.confで直接編集

netコマンドと、vtshコマンドの比較はココに書いてあります。

個人的な意見ではvtshの方が慣れればシンプルなので高速に思いますが、他の操作もあるのでnetコマンドでまずは慣れるほうがいいと感じています。

ちなみに、/etc/network/interfacesからインターフェース情報をルーティングテーブルに継承する仕組みのため、vtshやfrr.confではプロトコル"のみ"変更を行います。

ログについて

以上のことからプロトコルのログは/var/log/frr/frr.logで確認できます。出力レベルはinfoとなり、debugに変更したい場合はマニュアルを参照してください。

Route Entries

設計にあたって重要なスペックですが、Broadcom など汎用チップと共に利用するため、その仕様に合わせる必要があります。

詳細はマニュアルを参照ください。

簡単にいうと MAC アドレス、IPv4、IPv6、Neighbor 数に応じてメモリの割り当て量をセットしたパターンから1つ選択します。例えば L2 スイッチで使うなら、L3 関連のリソースは不要なのでl2-heavy-1を選ぶ、、といった考え方です。

ECMP

ECMP は次の仕様となります。

  • デフォルトで有効
  • 同じプロトコルで等コストであること
  • フローベーすハッシュ
  • Hash*は IP protocol, 入力 IF, src IP, dst IP, src TCP/UDP, dst TCP/UDP

* Meallnox などの Spectrum ASIC 搭載の L2 では src/dst MAC, Etye Type, VLAN ID も評価

hash-seed の変更は次の通りです。範囲が広いでの loopback から求めるような仕組みにしてもいいかもしれません。

cumulus@spine01:mgmt:~$ net add forwarding ecmp hash-seed
    <0-4294967295>  :  An integer from 0 to 4294967295

ハッシュのカスタマイズは Mellanox のスイッチでサポートされています。詳細はマニュアルを参照ください。

Static Routing

Static Routing です。一般的な方法かと思います。

net add routing route 0.0.0.0/0 192.168.0.2
net add routing route 192.168.1.0/24 192.168.0.2

route に追加されます。

cumulus@spine01:mgmt:~$ net show route static
RIB entry for static
====================
...

S>* 0.0.0.0/0 [1/0] via 192.168.0.2, swp1, weight 1, 00:01:29
S>* 192.168.1.0/24 [1/0] via 192.168.0.2, swp1, weight 1, 00:08:46

また next-hop には IP だけではなく、blackholeやrejectがあるので、再配信の時に利用するかもしれませんね。

cumulus@leaf01:mgmt:~$ net add routing route 192.168.200.0/24
    <interface>  :  An interface name "swp1" or glob "swp1-4,6,10-12"
    <ipv4>       :  an IPv4 address
    lo           :  interface
    eth0         :  LLDP peer oob-mgmt-switch
    swp1         :  interface
    swp2         :  interface
    swp3         :  interface
    swp49        :  LLDP peer leaf02
    swp50        :  LLDP peer leaf02
    swp51        :  LLDP peer spine01
    swp52        :  LLDP peer spine02
    swp53        :  LLDP peer spine03
    swp54        :  LLDP peer spine04
    Null0        :  Null interface
    blackhole    :  Silently discard pkts when matched
    g-ebgp       :  interface
    group-ibgp   :  interface
    mgmt         :  interface
    reject       :  Emit an ICMP unreachable when matched

IPv6 は同じ方法なので省略します。

まとめ

ざっと最低限使う機能を整理しました。他のプロトコルも触っていこうと思います。

よければ他の記事も参照ください。

Cumulus Linux - 基本的なL2設定方法

Cumulus Linuxに興味が湧いたので触っています。ここでは頭の整理として基本的な部分をまとめました。

概要

この記事はCumulus Linux 4.2でテストしています。

Interface Type

次の Type があり、各ベンダがサポートしている内容と同じかと思います。

種類 概要
Access アクセスポート
Trunk トランクポート
SVI VLAN 内の Layer3 Interface
Bond Port Channel / Link Aggregation
Bridge ブリッジドメイン

Layer2 Mode

Cumulus Linux には 2 種類の設定 Mode がありまます。

  • VLAN-aware Bridge
  • Traditional Bridge

簡単に比較表を作りました。

手法 概念 ポイント
VLAN-aware 1 ブリッジ内で VLAN グループを作成してポートを所属 - 2000 VLAN まで利用可能
- シンプルな設定でスケールしやすい
Traditional ブリッジ単位が VLAN グループとしてポートを所属 - 200VLAN まで利用可能
- Cumulus Linux 3.1 以前で VXLAN を利用している
- PVSTP+を利用している
- VLAN で LAN を指定するというよりブリッジで LAN を決めるイメージ

上記のことから、そもそもの使い方してVLAN-awareでどーんっと VLAN を通してあげることが推奨された使い方に感じました。

詳細はココを参照してください。

設定

比較の結果、VLAN-Awareのみで実施しています。興味のある方はTraditional Bride Modeを参照してください。

Access/Trunk

bridge を作成します。ここで所属させた port が全てTrunkで動作します。

net add bridge bridge ports swp49,51
net add bridge bridge vids 100,200
net add bridge bridge vlan-aware

swp51をAccessに変更する場合は、上記に加えて次のように指定します。

net add interface swp51 bridge access 100

以下のように状態が正しくセットされたことが確認できます。

cumulus@leaf01:mgmt:~$ net show bridge vlan

Interface  VLAN  Flags
---------  ----  ---------------------
swp49         1  PVID, Egress Untagged
            100
            200
swp51       100  PVID, Egress Untagged

続いてswp49でTrunkの範囲をVLAN100に絞り込みます。

net add interface swp49 bridge vids 100

これによって、先ほどswp49は VLAN100, 200 でしたが、VLAN100 のみ通るようになりました。

ncumulus@leaf01:mgmt:~$ net show bridge vlan

Interface  VLAN  Flags
---------  ----  ---------------------
swp49         1  PVID, Egress Untagged
            100
swp51       100  PVID, Egress Untagged

また、VLAN の利用にはデフォルトで 3600-3999 が予約済み(変更可能)のようなので注意が必要です。

Native-VLAN

デフォルトで Native VLAN は 1 が利用されるようになっており、自動的に設定されます。明示的に設定する場合は次のようにします。

net add bridge bridge pvid 1

もし通したくない場合は次のようにします。

net add interface swp49 bridge allow-untagged no

次のように swp49 から VLAN 1 が削除されました。

cumulus@leaf01:mgmt:~$ net show bridge vlan

Interface  VLAN  Flags
---------  ----  ---------------------
swp49       100
swp51       100  PVID, Egress Untagged

SVI

VLAN 上に Interface を設定します。

net add vlan 100 ip add 192.168.0.100/24

route に追加されました。

cumulus@tor01:mgmt:~$ net show route 192.168.0.0/24
RIB entry for 192.168.0.0/24
============================
Routing entry for 192.168.0.0/24
  Known via "connected", distance 0, metric 0, best
  Last update 00:03:39 ago
  * directly connected, vlan100


FIB entry for 192.168.0.0/24
============================
192.168.0.0/24 dev vlan100 proto kernel scope link src 192.168.0.100

Storm-Control

ループしないように設計を行っても、Storm-Control は念の為入れておくかと思います。

/etc/cumulus/switchd.confの設定項目を編集して機能を利用します。

cumulus@leaf01:mgmt:~$ cat /etc/cumulus/switchd.conf  | grep storm
interface.swp1.storm_control.broadcast = 400
interface.swp1.storm_control.multicast = 3000
interface.swp1.storm_control.unknown_unicast = 2000

編集後はswitchdの restart を行いますが、通信断が発生するため注意が必要です。

cumulus@leaf01:mgmt:~$ sudo systemctl restart switchd

一応 ping で試してみましたが、loss にはなりませんでしたね。

cumulus@spine01:mgmt:~$ ping 192.168.0.2
vrf-wrapper.sh: switching to vrf "default"; use '--no-vrf-switch' to disable
PING 192.168.0.2 (192.168.0.2) 56(84) bytes of data.
...
64 bytes from 192.168.0.2: icmp_seq=8 ttl=64 time=1.35 ms
64 bytes from 192.168.0.2: icmp_seq=9 ttl=64 time=1.75 ms
64 bytes from 192.168.0.2: icmp_seq=10 ttl=64 time=2.14 ms <----- ここ
64 bytes from 192.168.0.2: icmp_seq=11 ttl=64 time=1.34 ms
64 bytes from 192.168.0.2: icmp_seq=12 ttl=64 time=1.49 ms
...
^C
--- 192.168.0.2 ping statistics ---
14 packets transmitted, 14 received, 0% packet loss, time 36ms
rtt min/avg/max/mdev = 1.341/1.722/2.135/0.222 ms

Bond(LAG)

リンク冗長の定番は LAG かと思います。早速、swp49とswp50で設定してみましょう。

net add bond bond0 bond slaves swp49-50

Bond Detailsを見ると、Bond で変更できるオプションがわかりますね。

Bond Modeは LACP に当たるIEEE802.3adが基本ですが、対向装置が使えない場合はbalance-xorで静的接続に変更が必要です。

cumulus@leaf01:mgmt:~$ net show interface | grep bond
UP     swp49    1G   9216   BondMember    leaf02 (swp49)           Master: bond0(UP)
UP     swp50    1G   9216   BondMember    leaf02 (swp50)           Master: bond0(UP)
UP     bond0    2G   9216   802.3ad                                Bond Members: swp49(UP)
       bond0                                                       Bond Members: swp50(UP)

cumulus@leaf01:mgmt:~$ net show interface bond0
    Name   MAC                Speed  MTU   Mode
--  -----  -----------------  -----  ----  -------
UP  bond0  76:ed:2a:8a:67:24  2G     9216  802.3ad

Bond Details
------------------  --------
Bond Mode:          802.3ad
Load Balancing:     layer3+4
Minimum Links:      1
LACP Sys Priority:
LACP Rate:          1
LACP Bypass:        Inactive

All VLANs on L2 Port
--------------------
1,100,200

Untagged
--------
1

cl-netstat counters
-------------------
RX_OK  RX_ERR  RX_DRP  RX_OVR  TX_OK  TX_ERR  TX_DRP  TX_OVR
-----  ------  ------  ------  -----  ------  ------  ------
  701       0      20       0    697       0       0       0

LLDP Details
------------
LocalPort  RemotePort(RemoteHost)
---------  ----------------------
swp49      swp49(leaf02)
swp50      swp50(leaf02)

Routing
-------
  Interface bond0 is up, line protocol is up
  Link ups:       3    last: 2020/12/19 07:23:19.93
  Link downs:     6    last: 2020/12/19 07:17:33.85
  PTM status: disabled
  vrf: default
  index 15 metric 0 mtu 9216 speed 2000
  flags: <UP,BROADCAST,RUNNING,MULTICAST>
  Type: Unknown
  HWaddr: 76:ed:2a:8a:67:24
  Interface Type bond
  Master interface: bridge PVID: 1
  protodown: off (n/a)

cumulus@leaf01:mgmt:~$ ip link
...
6: swp49: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 9216 qdisc pfifo_fast master bond0 state UP mode DEFAULT group default qlen 1000
    link/ether 76:ed:2a:8a:67:24 brd ff:ff:ff:ff:ff:ff
7: swp50: <BROADCAST,MULTICAST,SLAVE,UP,LOWER_UP> mtu 9216 qdisc pfifo_fast master bond0 state UP mode DEFAULT group default qlen 1000
    link/ether 76:ed:2a:8a:67:24 brd ff:ff:ff:ff:ff:ff
...
15: bond0: <BROADCAST,MULTICAST,MASTER,UP,LOWER_UP> mtu 9216 qdisc noqueue master bridge state UP mode DEFAULT group default qlen 1000
    link/ether 76:ed:2a:8a:67:24 brd ff:ff:ff:ff:ff:ff

また、デフォルトで利用できるハッシュは次の通りです。

  • IP の場合: src/dst IP
  • TCP/UDP の場合: TCP/UDP src/dst port, src/dst IP

Meallnoxなど利用する HW によってはカスタマイズが可能のようなので、環境に応じてドキュメントの確認が必要です。

LLDP

デフォルトで Linux のlldpdが起動します。そのため、設定を変えたい場合は/etc/lldpd.confを作成します。詳細はマニュアルを参照ください。

netコマンドでも確認できるように次の内容が用意されています。

cumulus@tor01:mgmt:~$ net show lldp

LocalPort  Speed  Mode        RemoteHost       RemotePort
---------  -----  ----------  ---------------  ----------
eth0       1G     Mgmt        oob-mgmt-switch  swp10
swp49      1G     BondMember  leaf02           swp49
swp50      1G     BondMember  leaf02           swp50
swp51      1G     Access/L2   spine01          swp1
swp52      1G     Default     spine02          swp1
swp53      1G     Default     spine03          swp1
swp54      1G     Default     spine04          swp1

まとめ

ざっと最低限使いそうな機能を整理しました。まだnet操作に慣れていませんが、全体的にシンプルでいいですね。

そのほかにも以下の機能がありますので、興味ある方はマニュアルを読んでみてください。

  • STP
  • MLAG
  • LACP bypass
  • IGMP/MLD snooping

Juniper/go-netconfをざっと触る

Junos の自動化に Python ライブラリの PyEZ があることはご存知(ネットワークエンジニアの世界)の方が多いと思います。最近は Go 言語を触る機会が多いので、ふと調べたらJuniper/go-netconfが出てきたのでざっと触ってみました。

試した際のログはsourjp/lab-networkに置きました。

go-netonf?

This library is a simple NETCONF client based on RFC6241 and RFC6242 (although not fully compliant yet). Note: this is currently pre-alpha release. API and features may and probably will change. Suggestions and pull requests are welcome.

Juniper 公式パッケージで、netconf 接続を go 言語で実装されています。簡単に言えば PyEZ の Go 言語版に位置しますが、実装は比べると少なく、記載の通り pre-alpha です。

基本的な使い方

基本的な使い方は PyEZ と同じ感覚です。

  1. Connection を作成
  2. XML メッセージを投げる

とりあえずサンプルを書きました。

package main

import (
    "fmt"
    "log"

    "github.com/Juniper/go-netconf/netconf"
    "golang.org/x/crypto/ssh"
)

func main() {
    sshConfig := &ssh.ClientConfig{
        User:            "root",
        Auth:            []ssh.AuthMethod{ssh.Password("Juniper")},
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    }

    s, err := netconf.DialSSH("localhost:2830", sshConfig)
    if err != nil {
        log.Fatal(err)
    }
    defer s.Close()

    r, err := s.Exec(netconf.RawMethod("<get-chassis-inventory/>"))
    if err != nil {
        log.Fatal(err)
    }
    fmt.Printf("Reply: %+v", r)
}
"""
$ go run main.go
[]
0
Reply: &{XMLName:{Space:urn:ietf:params:xml:ns:netconf:base:1.0 Local:rpc-reply} Errors:[] Data:
<chassis-inventory xmlns="http://xml.juniper.net/junos/12.1X47/junos-chassis">
<chassis junos:style="inventory">
<name>Chassis</name>
...
"""

まず、netconf.DialSSH()でコネクションを作成します。 作成したコネクションのExec()により RPC を通して XML メッセージを送信します。

このExec()で受け取る型はRPCMethodで、実体はstringです。 そしてこの型を作成する方法は次の 4 種類です。

// RawMethod defines how a raw text request will be responded to
type RawMethod string

// MethodLock files a NETCONF lock target request with the remote host
func MethodLock(target string) RawMethod {
    return RawMethod(fmt.Sprintf("<lock><target><%s/></target></lock>", target))
}

// MethodUnlock files a NETCONF unlock target request with the remote host
func MethodUnlock(target string) RawMethod {
    return RawMethod(fmt.Sprintf("<unlock><target><%s/></target></unlock>", target))
}

// MethodGetConfig files a NETCONF get-config source request with the remote host
func MethodGetConfig(source string) RawMethod {
    return RawMethod(fmt.Sprintf("<get-config><source><%s/></source></get-config>", source))
}

このことから次の使い分けをして情報を取得するようです。

メソッド 用途
netconf.RawMethod() show コマンドなど情報を取得したい場合
netconf.MethodLock() 設定を変える場合
netconf.MethodUnlock() MethodLock()をしたい場合は忘れずに
netconf.MethodGetConfig() config を取得する場合

RPC の調べ方

先ほどのサンプルのように情報を取得したい場合は、特定のコマンドが Junos XML API のどれに当たるのかを調べる必要があります。

Junos XML API Explorerで、コマンドで検索すれば一発です。(昔からありましたっけ?)

ただ古い OS を利用しているとマトリクスに存在しないので、(きっと新しい ver と同じでしょうが)調べる方法があります。次のように CMD | display xml rpcとつけることで、何を使っているかがわかります。

root@vsrx1> show interfaces terse | display xml rpc
<rpc-reply xmlns:junos="http://xml.juniper.net/junos/12.1X47/junos">
    <rpc>
        <get-interface-information>
                <terse/>
        </get-interface-information>
    </rpc>
    <cli>
        <banner></banner>
    </cli>
</rpc-reply>

上記の場合であれば、次のようにすることで show interface terse の結果が XML で得ることができます。

r, err := s.Exec(netconf.RawMethod("<get-interface-information><terse/></get-interface-information>"))

応用的な使い方

レポジトリにもいくつかサンプルがあります。

その中で面白かったのが CLI の作成で、ちょっとしたサンプルを書いてみました。

引数で接続先(port 番号)を与えて、取りたい情報を-chassisや-ifで指定しています。

$ ./main -chassis -port=2830
Chassis : 340603e5d1fa

$ ./main -chassis -port=2831
Chassis : 8c7c5874f721

$ ./main -if -port=2830
ge-0/0/0 : up
ge-0/0/1 : up
ge-0/0/2 : up

長いので chassis の serial number 取得する部分だけ抜粋します。

func main() {
    target := flag.String("target", "localhost", "target")
    port := flag.String("port", "2830", "port")
    chassisCmd := flag.Bool("chassis", false, "get chassis serial number from 'show chassis hardware'")
    ifCmd := flag.Bool("if", false, "get if admin from 'show interface terse'")

    flag.Parse()
    c, err := NewConn(*target, *port)
    if err != nil {
        log.Fatalln(err)
    }
    defer c.conn.Close()

    if *chassisCmd {
        if err := c.GetChassisSN(); err != nil {
            log.Fatal(err)
        }
    }
    if *ifCmd {
        if err := c.GetIfAdmin(); err != nil {
            log.Fatal(err)
        }
    }
}

// Conn netconf.Session
type Conn struct {
    conn *netconf.Session
}

// NewConn Entrypoint of netconf.Session
func NewConn(target, port string) (Conn, error) {
    conf := &ssh.ClientConfig{
        User:            "root",
        Auth:            []ssh.AuthMethod{ssh.Password("Juniper")},
        HostKeyCallback: ssh.InsecureIgnoreHostKey(),
    }
    d := fmt.Sprintf("%v:%v", target, port)
    s, err := netconf.DialSSH(d, conf)
    if err != nil {
        return Conn{}, err
    }
    return Conn{conn: s}, nil
}

// GetChassisSN get chassis serial number from "show chassis hardware"
func (c *Conn) GetChassisSN() error {
    out, err := c.conn.Exec(netconf.RawMethod("<get-chassis-inventory/>"))
    if err != nil {
        return err
    }
    var ci ChassisInventory
    if err = xml.Unmarshal([]byte(out.Data), &ci); err != nil {
        return err
    }

    chassisName := strings.ReplaceAll(ci.Chassis.Name, "\n", "")
    chassisSN := strings.ReplaceAll(ci.Chassis.SerialNumber, "\n", "")
    fmt.Printf("%v : %v\n", chassisName, chassisSN)

    return nil
}

// ChassisInventory struct for "show chassis hardware"
type ChassisInventory struct {
    XMLName xml.Name `xml:"chassis-inventory"`
    Text    string   `xml:",chardata"`
    Xmlns   string   `xml:"xmlns,attr"`
    Chassis struct {
        Text          string `xml:",chardata"`
        Style         string `xml:"style,attr"`
        Name          string `xml:"name"`
        SerialNumber  string `xml:"serial-number"`
        Description   string `xml:"description"`
        ChassisModule []struct {
            Text             string `xml:",chardata"`
            Name             string `xml:"name"`
            Description      string `xml:"description"`
            ChassisSubModule struct {
                Text        string `xml:",chardata"`
                Name        string `xml:"name"`
                Description string `xml:"description"`
            } `xml:"chassis-sub-module"`
        } `xml:"chassis-module"`
    } `xml:"chassis"`
}

このように Go であれば簡単に CLI ツールを作成できますし、Go 言語の特徴としてクロスコンパイルができるので、作成した運用コマンド集を CLI として配布して、Windows も MAC ユーザーも同じように叩けるっていうのは一つかも、、しれません。 あとは API の作成も簡単ですし、xml の構造体を json に変換してもいいかもしれません。

一番手間がかかるのは Go で構造体を用意することですが、Junos から XML のレスポンスを受けて、XML to Go structに貼り付ければ簡単に作れます。

まとめ

いかがでしょうか?シンプルな実装で使いやすいですね。ただ commit 状況やその他ベンダのサポートも考えると Python ライブラリによる実装が今もメインかもしれません。

しかしながら他の言語で使えるようになると選択肢が増えるのでとてもいいことですね。gobgpもありますし、個人的には Go の方が好きだったりします。

接続が失敗する場合

次のように接続できない場合についてです。

$ go run main.go
2020/12/17 15:27:55 dial tcp [::1]:830: connect: connection refused
exit status 1

PyEZ でもそうですが接続方法が netconf 接続であるため、port 830を利用します。

Junos には次の設定を忘れずにしましょう。

set system services netconf ssh

そして Vagrantfile を利用している方は次の設定を行ってください。

vsrx.vm.network :forwarded_port, id: "ssh", guest: 22, host: 2201 # for SSH
vsrx.vm.network :forwarded_port, id: "netconf", guest: 830, host: 2830 # for NETCONF