👨‍🔧

【Ruby】EtherIPトンネルのツール作ってみた

2021/06/27に公開

はじめに

RubyでEtherIPトンネルを構成するツールを作ってみました。
IX2015と動作確認して、トンネルを張れることを確認しています。

https://github.com/kuredev/rb_etherip

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の動作イメージ

具体的には以下のような動作イメージになります。

https://qiita.com/kure/items/f43d28bbfa3a648c1461

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を以下に残しておきます。

https://gist.github.com/kuredev/1a6ec9c79a4d66f7f831d670ed9adfc8

実装のポイント

基本的にはブリッジよろしく右から来たものを左に流すだけですが、この時に外側の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

https://github.com/kuredev/rb_etherip/blob/main/lib/rb_etherip/tunnel.rb#L30-L42

その他参考(関連)

https://tex2e.github.io/rfc-translater/html/rfc3378.html

https://kure.hatenablog.jp/entry/2020/12/20/004325

Discussion