LoginSignup
19
7

More than 3 years have passed since last update.

Elixir で raw socket を扱う(準備編)

Last updated at Posted at 2019-12-08

この記事は、Elixir Advent Calendar 2019 の9日目です。昨日は @matsubara0507 さんのひっっさしぶりに古の自分のコードを動かした話(久しぶりに thank_you_stars をビルドする)でした。

今日は、とあるデバイスを触りたくなって、それが Ethernet フレームを使うので、Elixir でやるにはどうすればよいのか調べてみたという話です。残念ながら目的地まで到達できなかったので「準備編」という扱いにしました。

EtherCAT を Elixir で使いたい

みなさん EtherCAT はご存知ですか。これは Ethernet の枠組みを使う制御用の通信規格です。IoT でデバイスを制御するときには GPIO や I2C や Pmod とか使いますよね。組込みだと ModBUS とか FLnet とか言うのがあります。その一つに EtherCAT というのがあります。詳しくは EtherCAT アドベントカレンダー の記事をご覧ください。私も 温故知新 IEEE802シリーズで EtherCAT を眺め直す なる記事を書いております。

EtherCAT はモロに産業用という感じで、制御盤とかPLC用の製品が多く、これまでホビーとか IoT で気軽に使うような技術ではありませんでした。私もちょっと前までは気にはしながら遠く眺めてました。

ところが、彗星のように(なんて月並みで古臭い形容)気軽に試せるボード が発売になりました。それも python によるサンプルコード付きで。「これは Elixir でも使いたい」と早速取り寄せました。python のコードがあるから、とにかく動くレベルにするのはあまり難しくないだろう… その甘い考えを打ち砕かれて私は迷走を開始します。この記事はその顛末の前半です。

既存の Elixir による EtherCAT プログラム

検索した感じだと以下があります。

などがあります。前者は EtherLab を使うためのパッケージ、後者は Beagle Bone を使うパッケージのようです。どちらも Elixir 自体で制御部分を記述しているようではなさそうです。

EtherCAT は IP ではない

さて EtherCAT は Ethernet フレームを使いますが IP ではありません。IP でないので当然 UDP でも TCP でもありません。OSI参照モデルの第2層であるデータリンク層として Ethernet を定める規格 IEEE802.3 を使いますが、そこより上はインターネット系のプロトコルとは全く関係がないのです。つまり、いつも使ってるソケットの技はそのまま使えないのです。はて Elixir で Ethernet を直接扱うのはどうやるのでしょうか。そもそも使うことができるのでしょうか。

私が普段使ってる Elixir の実装を見ると

  • Elixir
  • Erlang VM (BEAM)
  • UNIX 系 OS (MacOS やら Linux やら)

の構造を持っています。私の環境で Elixir で EtherCAT をドライブしたいなら、Elixir で MacOS や Linux の持ってるデバイスドライバをどう扱うかがわからないとなりません。これ Python や Ruby なら割と簡単に手に入ります1。当然 Elixir も…
いえいえ、そうではなかったんです。

Ethernet フレームを OS で直接入出力したい

Ethernet フレームを直接入出力する方法は、実は UNIX 系の OS には普通にあります。ただしシステムコールを使わないとなりませんので、ややかなり上級コースになります。お手近の unix shell で man socket してみてください。それがヒントです。

MacOS で Ethernet を直接扱う

まず MacOS で socket システムコールを見てみます。

$ man socket

SOCKET(2)                   BSD System Calls Manual                  SOCKET(2)

NAME
     socket -- create an endpoint for communication

SYNOPSIS
     #include <sys/socket.h>

     int
     socket(int domain, int type, int protocol);

DESCRIPTION
     socket() creates an endpoint for communication and returns a descriptor.

という説明が出てきます。man セクションが2というのにもちょっとビビりますね。もう少し先を読んでみます。

     The domain parameter specifies a communications domain within which com-
     munication will take place; this selects the protocol family which should
     be used.  These families are defined in the include file <sys/socket.h>.
     The currently understood formats are

           PF_LOCAL        Host-internal protocols, formerly called PF_UNIX,
           PF_UNIX         Host-internal protocols, deprecated, use PF_LOCAL,
           PF_INET         Internet version 4 protocols,
           PF_ROUTE        Internal Routing protocol,
           PF_KEY          Internal key-management function,
           PF_INET6        Internet version 6 protocols,
           PF_SYSTEM       System domain,
           PF_NDRV         Raw access to network device

     The socket has the indicated type, which specifies the semantics of com-
     munication.  Currently defined types are:

           SOCK_STREAM
           SOCK_DGRAM
           SOCK_RAW

PF_INET とか SOCK_STREAM とか SOCK_DGRAM とか、ネットワークプログラミングしたことのある方ならおなじみのキーワードが出てきました。TCP や UDP でプログラムするならこのあたりを使いますね。この最後の SOCK_RAW はなんでしょうか。もう少し後ろに行くとチラと書いてあります。

SOCK_RAW sockets provide access to internal network protocols and interfaces. The type SOCK_RAW, which is available only to the super-user.

「SOCK_RAW ソケットは内部ネットワークプロトコルやインタフェースへのアクセスを提供します。SOCK_RAW タイプのソケットは管理者だけに有効です。」とありますね。これが RAW すなわち「生」のソケットを提供するということです。生のソケットを扱うのはそれなりに危険を伴うので特権モードでしか動きません。

RAW には2つの意味がある

さあ、使うなら RAW ソケットです。例えば ICMP を用いた ping のプログラムなどは様々な言語での実装がネットにころがってます。traceroute も ICMP 使ってますので、RAW ソケットの出番です。ただここで喜んでて「あれ?」となります。ICMP は Ethernet 上のプロトコルであり、Ethernet そのものではありません2
RAW と言ってるのは確かに TCP や UDP よりは下の層ですが、IP や ICMP を扱うようです。Ethernet はどうなってるのでしょうか。実は MacOS は socket システムコールでは RAW socket を使っても Ethernet フレームを直接触る入出力は出来ないのです。つまり RAW には

  • IP や ICMP を扱う
  • Ethernet を扱う

の2つの意味があるのでした。MacOS に限らず FreeBSD, NetBSD, OpenBSD 等の BSD 系 UNIX クローンには全て備わっていないようです3

BPF (Berkley Packet Filter)

でもなんか方法あるはず、と思いますよね。だって Wireshark とかのネットワークを直接嗅ぐ系のコマンドがあります。Ethernet を IP 層に持ってこずに OS 経由で見ることができるはずです。それが BPF です。これ、ちょっと前までは Ethernet フレームを読む(受信)だけだったのが、いつの間にか Ethernet フレームを書く(送信)ことができるようになっていました。

ですので、BSD 系の OS を使うなら BPF でプログラムすれば良いということになります。ところがなんと今回の話は MacOS に関してはここでおしまいです。ラズパイとかの IoT な小箱で使いたいので Mac はしばらくさようならです。

Linux の socket システムコール

Linux は BSD 系の unix クローンとは別の流れを持っています。Linux のシステムコールも調べてみましょう。正攻法で man socket するとこう出てきます。

NAME
       socket - create an endpoint for communication

SYNOPSIS
       #include <sys/types.h>          /* See NOTES */
       #include <sys/socket.h>

       int socket(int domain, int type, int protocol);

DESCRIPTION
       ... snip...
       Name                Purpose                          Man page
       AF_UNIX, AF_LOCAL   Local communication              unix(7)
       AF_INET             IPv4 Internet protocols          ip(7)
       AF_INET6            IPv6 Internet protocols          ipv6(7)
       AF_IPX              IPX - Novell protocols
       AF_NETLINK          Kernel user interface device     netlink(7)
       AF_X25              ITU-T X.25 / ISO-8208 protocol   x25(7)
       AF_AX25             Amateur radio AX.25 protocol
       AF_ATMPVC           Access to raw ATM PVCs
       AF_APPLETALK        AppleTalk                        ddp(7)
       AF_PACKET           Low level packet interface       packet(7)
       AF_ALG              Interface to kernel crypto API

この AF_PACKET(ないしは PF_PACKET)ってのが Ethernet フレームを直接叩く type protocol です。そうです。Linux は socket システムコールで Ethernet IF を直接読み書きできるのです。これを使えば Raspberry Pi でも Ethernet フレームを扱えるようになります。

MacOS での開発にするかラズパイにするか迷うところですが、最終的に Nerves のターゲットマシンにしたいですから、ここはラズパイでの検討を優先することにしました。

Elixir で socket システムコールを使う

脳がすっかり UNIX システムコール用になってます。改めて Elixir でのネットワーク通信の方法はと言うと、プロセス間のメッセージングでした。それも複数のプロセスで気持ちよくプログラミングするなら GenServer にはじまる一連の並行プログラミング手法です。Agent とか Task とかすっかりスワップアウトされてますね。Elixir では通信の部分をしっかり隠蔽してしまって、良く抽象化されたレベルのプログラミング環境を提供しています。

逆に生ネットワークプログラミングの面で言うと Elixir 自身はほぼ全く何も持っていません。ちょっとなにかしようとすると Erlang のお世話になるしかないです。例えば TCP や UDP でプログラミングしようとすると :gen_tcp, :gen_udp で Erlang のライブラリを呼び出す必要があります。私たちの大好きなソケットもそのようです。アルケミストのみなさん、Erlang の世界へようこそ。

Erlang の socket ライブラリ

Erlang には socket という名前のライブラリがあります。これは UNIX の socket システムコールをそのまま使う目的で、Erlang 関数でラップするのを目指しているのはほぼ間違いなさそうです。比較的新しくて OTP 22 から導入されたようです。

そこでもちろん「さあこれを使おう」となるところ、良くドキュメントを読んでみましょう。UNIX の socket そのままを持ってこようとしているので、ドキュメントもなかなかの量です。ただ見るべきところは最初です。

socket.jpeg

streamdgram に混じって raw ってのがありますね。でもこれしかないですね。嫌な予感がします。そうです、この raw で作れるソケットは IP や ICMP 用です。ちょうど MacOS とかの BSD 系のシステムコールに相当します。

では、linux のシステムコールの Ethernet を取り扱う PF_PACKET に対応するのはあるのでしょうか。それはきっと packet という名前になっていそうです。嫌な予感が的中です。そうなんです。Erlang の socket ライブラリは標準のままでは Ethernet フレームを扱えないということです。

Erlang の socket ライブラリを拡張する

ならば socket ライブラリを素朴に拡張すれば良いではないかという気がします。それをやってるのがこちらです。
socket ライブラリの拡張
先頭にEthernetブリッジのErlangプログラムがあります。2つのEthernet I/F を引数として、一方から来た Ethernet フレームを反対側に投げるというをやります。これをやるのに packet で socket 関数を呼んでいます。ここが標準のライブラリにないところです。シンプルですね。ちなみに、両方向の伝送処理をそれぞれ並行プロセスにしてます。Elixir になれてるとあまり驚きませんが、このあたりが綺麗に書けるのも Erlang/OTP の良さですね。

さて、この Erlang のプログラムの後ろにはパッチコードがあります。これは ERTS (Erlang Run Time System) に対するパッチです。これも最初の方を見ると packet が選べるようになってることが分かります。このパッチを当てれば望みの socket ライブラリができるのでしょうが、チラ見した感じでは現行バージョンの Erlang/OTP には当たらないように見えます。パッチの量もなかなかのものです。Erlang と Unix socket に精通してないと触れなさそうです。

Erlang gen_socket

上の作者の shun159 さんに教えてもらったのがこちらです。ドキュメントや例が少なくてお試しするのがチト辛い。せっかく教えてもらいはしたのですが、斜め読みしただけで終わりました。
https://github.com/travelping/gen_socket/blob/master/src/gen_socket.erl

Erlang Procket

さて、これまでの話の私の結論として今のところ一番使えそうなのがこちらの procket です。これの README.md には明示的に

  • generate and snoop packets using PF_PACKET sockets on Linux
  • generate and snoop packets using the BPF interface on BSDs like Mac OS X

と書いてあります。ドキュメントや例も多いので一番使いやすそうに思えました。
では早速 procket を使ってみましょう。例によって mix new してプロジェクトを作成してください。procket を使うのには mix.exs に以下の依存関係を記述してください。

  defp deps do
    [
      {:procket, github: "msantos/procket"}
    ]

  end

これを書いたら mix deps.get して、後は iex -S mix でとりあえず使えます。簡単なもんです。4

Procket で特権モード動作の準備をする

これで socket の TCP や UDP は procket で使えるようになりました。ただし RAW ソケット使うならもう一声あります。前に raw ソケットを使う場合は特権モードが必要と書きました。ですので procket でも IP, ICMP, Ethernet を使う場合には、特権モードでシステムコールを呼び出すようにしておかないとなりません。

素の Elixir で(procket はなしで)やってみましょう。

$ iex # ユーザモードで実行する
Erlang/OTP 22 [erts-10.5.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]

Interactive Elixir (1.9.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> {:ok, socket} = :socket.open(:inet, :raw, :icmp)
** (MatchError) no match of right hand side value: {:error, :eperm}
    (stdlib) erl_eval.erl:453: :erl_eval.expr/5
    (iex) lib/iex/evaluator.ex:257: IEx.Evaluator.handle_eval/5
    (iex) lib/iex/evaluator.ex:237: IEx.Evaluator.do_eval/3
    (iex) lib/iex/evaluator.ex:215: IEx.Evaluator.eval/3
    (iex) lib/iex/evaluator.ex:103: IEx.Evaluator.loop/1
    (iex) lib/iex/evaluator.ex:27: IEx.Evaluator.init/4
iex(1)> 

なんか怒られちゃいました。例によってエラーメッセージが親切ではないです。これ特権モードで動かすとこうなります。

$ sudo iex # 特権モードで動かす
Password:
Erlang/OTP 22 [erts-10.5.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]

Interactive Elixir (1.9.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> {:ok, socket} = :socket.open(:inet, :raw, :icmp)
{:ok, {:socket, #Reference<0.2568000987.817233924.138281>}}
iex(2)> 

と、ちゃんとソケットが出来ました。これは RAW ソケットと言っても ICMP ですが、それでも特権モードが必要です。いわんや生Ethernetをや。

そこで、プロケットではいくつかの方法で特権モードを扱います。ドキュメントには /etc/sudoers に記述する方法が最初にありますが、こんな簡単なことがどうも私の環境ではうまく行かなかったので、以下の「とあるファイルにのみ管理者実行権限を与える」方法をとりました。

$ sudo chown root deps/procket/priv/procket
$ sudo chmod u+s deps/procket/priv/procket
$ ls -l deps/procket/priv/procket
-rwsr-xr-x  1 root  staff  16460 11 29 09:09 deps/procket/priv/procket

Procket の Erlang 版エコーバックプログラムを Elixir 用に改造する

これで procket が raw ソケットを扱えるようになります。iex を起動するときにも sudo する必要はありません。procket にはいくつかの Erlang によるサンプルプログラムが掲載してあります。先頭が echo.erl というソケットを用いたエコーバックプログラムがありましたので、まずはこれを比較的そのまま Elixir プログラムにしてみました。

lib/echo.ex
require Logger

defmodule Echo do
  @port 54

  def start() do
    start(:tcp)
  end

  def start(:tcp) do
    start(@port, [{:protocol, :tcp}, {:family, :inet}, {:type, :stream}])
  end

  def start(:udp) do
    start(@port, [{:protocol, :udp}, {:family, :inet}, {:type, :dgram}])
  end

  def start(port, options) do
    proto = :proplists.get_value(:protocol, options, :tcp)
    family = :proplists.get_value(:family, options, :inet)
    {:ok, fd} = :procket.open(port, options)
    IO.puts("Listening on: #{port}, #{proto}")
    listen(proto, family, fd)
  end

  def listen(:tcp, family, fd) do
    {:ok, s} = :gen_tcp.listen(0, [:binary, family, {:fd, fd}])
    accept(s)
  end

  def listen(:udp, family, fd) do
    {:ok, _s} = :gen_udp.open(0, [:binary, family, {:fd, fd}])
    recv()
  end

  def accept(ls) do
    {:ok, _s} = :gen_tcp.accept(ls)
    spawn(fn() -> accept(ls) end)
    recv()
  end

  def recv() do
    receive do
      {:tcp, s, data} ->
#       :gen_tcp.send(s, data) # そのままエコーバックする場合
        :gen_tcp.send(s, "! #{String.upcase(data)}") # ちょっと加工
    recv()
      {:tcp_closed, s} ->
    :gen_tcp.close(s)
      {:udp, s, ip, port, data} ->
        :gen_udp.send(s, ip, port, data)
        recv()
      _ -> Logger.error("recv")
    end
  end
end

これを実行してみます。

$ iex -S mix
Erlang/OTP 22 [erts-10.5.3] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe] [dtrace]

Interactive Elixir (1.9.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> Echo.start
Listening on: 54, tcp

と出てくるので別のターミナルで以下をやります。

$ telnet localhost 54 # TCP ポート54 に接続
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
hello
! HELLO
world
! WORLD

helloworld が2回ずつ出てきます。これそれぞれ最初のが自分で打った入力で、続いて大文字になって出てくるのが Echo.start/0 関数がエコーバックしてきた結果です。セッションを終了するには telnet したターミナルでコントロールキーを押しながら ] を打ちます。

^]
telnet> quit
Connection closed.
$

すると Elixir 側でも

:ok
iex(2)>

と tcp セッションをクローズして終了します。

まとめ

  • Ethernet のフレームを扱うには BSD OS の BPF か linux の socket(2) を使う
  • Elixir で Ethernet フレームを扱うのに Erlang のライブラリを使った
  • Erlang の procket ライブラリが素性が良さそうで試してみた

え? 結局 RAW ソケットでプログラムはしてないじゃないかって? そこまで行かなかったんです。続きは乞うご期待。

明日のElixir Advent Calendar 2019 の記事は, @niku さんのescriptを動かすDockerfileのサンプルです。こちらもお楽しみに!

謝辞

これをやるに当たりたくさんの方にお世話になりました。

特にサッポロビーム@niku さんには erlang の外部ライブラリを Elixir で動かすのにガッツリお付き合い頂きました。大変助かりました。ありがとうございました。

また、Elixir.jp slack や Erlang & Elixir Fest のみなさんにお世話になりなりました。taiyo さん、seizans さん、Kinukawa Ryota さん、jj1bdx さん、(以上 slack 名)。ご助言ありがとうございました。 

また、これをやるにあたり自分の時間の確保にはもくもく会を利用させてもらいました。fukuoka.ex kokura.exでもくもく会を準備してくれたみなさんと一緒にもくもくしてくれたみなさんに感謝いたします。

参考文献

  • UNIXネットワークプログラミング 第2版 Vol.1, W.Richard Stevens著, 篠田陽一訳、ISBN4-8101-8612-1 (ソケットプログラミングのバイブルと思います。絶版のようです)
  • Procket
  • Erlang Socket
  • Ruby class Socket
  • Python socket

  1. 生Ethernetフレームが扱えるかは、該当する言語の socket ライブラリで socket オプションに PF_PACKET や AF_PACKET が使えるかを調べることでざっくり分かります。 

  2. 正確にはIPのプロトコル番号1として規定されています。INTERNET CONTROL MESSAGE PROTOCOL, RFC792 

  3. ごめんなさい。ここちゃんと調べきれてません。そんなことねーよ、という情報があればぜひ教えて下さい。 

  4. と、ここさらりと書いていますが、使おうと思ってからここに至るのに約10時間ほど煮溶かしてます。 

19
7
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
7