【Ruby】EtherIPトンネルのツール作ってみた
はじめに
RubyでEtherIPトンネルを構成するツールを作ってみました。
IX2015と動作確認して、トンネルを張れることを確認しています。
EtherIPとは
レイヤ2レベルでトンネルを構成できるプロトコルです。
Q.1-1 Ethernet over IP機能とは何ですか?
Ethernet over IP機能は、受信したイーサネットフレームを、IPネットワーク上に構成したトンネルを経由してブリッジ転送する機能です。本機能を使用すれば、離れた拠点間をIPネットワークを介して同一LANとして接続することが可能になり、L2VPNの構築を実現します。
https://jpn.nec.com/univerge/ix/faq/etherip.html#Q1-1
他にレイヤ2レベルのトンネリングのプロトコルではL2TPv3がメジャーと思いますが、EtherIPはセッション管理や認証等の機能がありません。単純に受信したパケットをカプセル化するだけの非常にシンプルなプロトコルと言えると思います。
「L2TPv3」と「EtherIP」の違いは何ですか?
https://jpn.nec.com/univerge/ix/faq/etherip.html#Q1-14
EtherIPの動作イメージ
具体的には以下のような動作イメージになります。
EtherIP自体のヘッダは2Byteの非常にシンプルな構成で、値は現在固定と考えていいようです。
16ビットEtheripヘッダーフィールドは、2つの部分で構成されています。バージョンフィールドの値は3(3、 '0011' ')でなければなりません。予約フィールドの値は0(ゼロ)でなければなりません。
https://tex2e.github.io/rfc-translater/html/rfc3378.html
作ったもの
使い方
前提
- Ruby
- 動作確認したのは3.0.1
- めちゃめちゃ古くなければ多分動く
- Linux
- 動作確認したのはRaspberry Pi
$ lsb_release -a
No LSB modules are available.
Distributor ID: Raspbian
Description: Raspbian GNU/Linux 10 (buster)
Release: 10
Codename: buster
- ネットワークインターフェースが2つ以上必要
動作手順
ここをgit clone して以下のように実行します。
$ sudo ruby rb_etherip.rb -o [外側のインターフェース名] -t [トンネル側のインターフェース名] -d [トンネルの対向IPアドレス] -s [トンネル側のインターフェースのIPアドレス]
具体例
具体例の構成とコマンドを記載します。
今回のツールをインターフェースを2つ持つRaspberry Piで動かし、そのEtherIPトンネルの対向としてIX2015ルータを用います。
以下図ではクライアントA, Bが同じネットワーク帯(10.0.0.0/24)に所属しており、クライアントA,B同士はレイヤ2以上レベルでは透過的に通信できます。(クライアントAから ping 10.0.0.100
みたいな感じでクライアントBに通信できる。MACアドレスもクライアントBのものが見える)
Raspberry Pi(トンネルデバイス)で以下を実行
$ sudo ruby rb_etherip.rb -o eth1 -t eth0 -d 20.0.0.2 -s 20.0.0.1
クライアントBで以下を実行
$ ping 10.0.0.101
この時のIX2015のconfigを以下に残しておきます。
実装のポイント
基本的にはブリッジよろしく右から来たものを左に流すだけですが、この時に外側のIFで受信したパケットはEtherIPヘッダを付与してトンネル側のIFに流し、反対にトンネル側のIFで受信したパケットは外側のIFにEtherIPヘッダを外して流します。
コードを抜粋すると以下のように実装しています。
if sock.object_id === outside_sock_object_id
# add EhterIP HEader
@logger.debug("Receive from outside interface")
ehter_ip_header = EtherIPHeader.new.to_pack
tunnel_sock.send(ehter_ip_header + payload, 0, Socket.sockaddr_in(nil, @dst_addr))
else
# remove IP+EtherIP Header(22Byte)
@logger.debug("Receive from tunnel interface")
remove_header_size = 22 # IP Header + EtherIP Header
payload_excluded_etherip = payload.byteslice(remove_header_size, payload.bytesize - remove_header_size)
outside_sock.send(payload_excluded_etherip, 0)
end
その他参考(関連)
Discussion