「一般消費者が事業者の表示であることを判別することが困難である表示」の運用基準に基づく開示: この記事は記載の日付時点で株式会社ソラコムのエンジニアリングチームに所属する社員が執筆しました。ただし、個人としての投稿であり、株式会社ソラコムとしての正式な発言や見解ではありません。

はじめに

SORACOM Advent Calendar 2023 15 日目です。

昨日は @n_mikuni による ソラカメで撮影した風景から GPT-4V で富士山を探す でした。GPT-4V を活用した今時な内容でしたが、今回は SORACOM Arc と古から存在するテクノロジー FFI (Foreign Function Interface) に関するお話です。SORACOM Advent Calendar 2021soratun を改造して AWS Lambda から簡単に SORACOM Arc を使ってみました の続編になります。

SORACOM Arc とは

SORACOM Arc はソラコムが 2021 年 6 月にリリースした、SIM カードがなくてもソラコムのプラットフォームサービスをセキュアに利用できるサービスです。伝送路の安全性を WireGuard を使って確保しており、WireGuard を使える機器であれば iPhone や Android のようなスマートフォンから、Raspberry Pi を含む Linux、そして M5Stack で利用されている ESP32 でも動作します (参照: WireGuard for ESP32 の実装的なところ)。

物理的な SIM カードに代わってバーチャル SIM/Subscriber を発行し、バーチャル SIM/Subscriber に紐付いた WireGuard 認証情報を使って SORACOM プラットフォームと接続します。

ここから WireGuard という新しめの技術と FFI という古の仕組みがどのように関係してくるのかを説明します。

libsoratun とは

今回はこんなことをやりました。

  1. WireGuard の Go 実装である wireguard-go を利用し、バーチャル SIM/Subscriber の認証情報を利用して Unified Endpoint へデータを送るシンプルな関数を作りました。
  2. これを共有ライブラリ .so および静的ライブラリ .a として仕立てました。
  3. ソラコムが提供する SORACOM Arc のクライアントエージェントである soratun のライブラリ風実装ということで libsoratun と命名しました。

0x6b/libsoratun: The C library allows you to embed Soracom Arc connectivity into your own program, entirely from userspace.

libsoratun には以下のような特徴があります。

  • Linux カーネルに実装されている WireGuard やネットワークインターフェース(tun デバイス)を作成・操作する soratun と異なり、root ユーザー権限 (正確には NET_ADMIN capability) を必要としません。
  • C のライブラリですので Foreign Function Interface (FFI) を通してさまざまなプログラミング言語から利用できます。

ところで、冒頭から出てくる FFI とは何でしょうか。Wikipedia から引用します。

Foreign function interface(フォーリン・ファンクション・インターフェイス、FFI)とは、あるプログラミング言語から他のプログラミング言語で定義された関数などを利用するための機構。主に高水準言語から C/C++などの関数やメソッドを呼び出し、OS 固有の機能などを利用するために使用されることが多い。

一般的には「C 言語ではないプログラミング言語から C で実装された関数を呼び出す」技術として認知されているかもしれませんが、インターフェースの規定のみで実装言語は問いませんので以下のように使っています。

  1. Go 言語で実装した関数を共有・静的ライブラリとしてビルドし、外からは C の関数として見せる。
  2. いろいろなプログラミング言語が用意している C の関数を呼び出す機能 (= FFI) を使って 1 を呼び出す。

ここからライブラリのビルド方法と、いくつかのプログラミング言語からの使い方を紹介します。

使い方

Linux と macOS での動作を確認しています。Windows でも同様と思いますが確認していません。

Go をインストールする

ライブラリ本体 (.so ファイルおよび .a ファイル) を GitHub から配布する準備がまにあわなかったため、お手元でビルドが必要です。そのため、だいぶ面倒ですが Go のインストールから。Download and install - The Go Programming Language にしたがってください。

ソースコードを入手し libsoratun をビルドする

ソースコードを GitHub リポジトリから入手しビルドします。

$ git clone github.com/0x6b/libsoratun
$ cd libsoratun
$ make libs

lib/shared ディレクトリに libsoratun.so (共有ライブラリ)、lib/archive ディレクトリに libsoratun.a (静的ライブラリ)が生成されます。やや冗長なディレクトリ構成ですが、同じディレクトリに生成すると Rust のビルドスクリプトがうまく見つけてくれませんでした (詳細は調べていません)。

それぞれのディレクトリにはヘッダファイル libsoratun.h が生成されており、Unified Endpoint へのデータを送信する Send 関数が以下のシグネチャで定義されています。前述のとおり C の関数に見えますね。

extern char* Send(char* configJson, char* method, char* path, char* body);

バーチャル SIM/Subscriber をセットアップし、設定ファイルを作成する

公式ドキュメント Getting Started: SORACOM ユーザーコンソールでバーチャル SIM/Subscriber を作成して soratun で接続する を参照し、ステップ 1 (バーチャル SIM/Subscriber の作成) と ステップ 3 (soratun 設定ファイルの作成) を実施し、設定ファイル arc.json をゲットします。

いろいろなプログラミング言語から使う

ここからは、それぞれのプログラミング言語が用意している C の関数を呼び出す機能 (= FFI) を使って、C の関数に見える Go の関数を呼び出し Unified Endpoint へデータを送っていきます。

ソースコードは examples ディレクトリにあります。

Python

Python では標準ライブラリに含まれる ctypes モジュールを使って C の共有ライブラリを呼び出せます。 ctypes.cdll.LoadLibrary を使って共有ライブラリをロードし、ctypes.c_char_p で文字列を char* に変換して渡します。

import ctypes
import sys

soratun = ctypes.cdll.LoadLibrary("../../lib/shared/libsoratun.so")

with open(sys.argv[1], "r") as file:
    config = ctypes.c_char_p(file.read().encode("utf-8"))

method = ctypes.c_char_p(b"POST")
path = ctypes.c_char_p(b"/")
body = ctypes.c_char_p(" ".join(sys.argv[2:]).encode("utf-8"))

soratun.Send(config, method, path, body)

以下のように実行できます。

$ cd examples/python
$ python3 main.py /path/to/arc.json '{"message": "hey"}'

Python 3.12.0 で動作を確認しました。

Node.js

Node.js では ffi-napi モジュールを使用しますので npm コマンドによるインストールが必要です。

$ cd examples/nodejs
$ npm install

ffi.Library で共有ライブラリのロードと Send 関数のシグネチャを定義します。Python と異なり char* への変換はよしなにやってくれました。

const { Library } = require("ffi-napi");
const { readFileSync } = require("fs");

const soratun = Library("../../lib/shared/libsoratun", {
  Send: ["string", ["string", "string", "string", "string"]]
});

const config = readFileSync(process.argv[2], "utf8");
soratun.Send(config, "POST", "/", process.argv[3]);

以下のように実行できます。

$ node src/index.js /path/to/arc.json '{"message": "hey"}'

Node.js v18.19.0 で動作を確認しました。

Rust

Rust も Python と同様標準ライブラリに std::ffi として FFI をサポートするモジュールが用意されています。ライブラリはビルド時にリンクされ (build.rs 参照)、std::ffi::CStrstd::ffi::CString を通して C の関数とやりとりします。リポジトリ上のサンプルからコマンドライン引数の処理等の部分を除くエッセンスは以下のようなかたちになります。言語の違いはあれいずれも似たような流れですね。

use std::ffi::CStr;
use std::ffi::CString;
// ...
use crate::soratun::Send;
// ...
mod soratun;

fn main() -> Result<(), Box<dyn Error>> {
    // ...
    let config = CString::new(read_to_string(config)?)?.into_raw();
    let method = CString::new(method)?.into_raw();
    let path = CString::new(path)?.into_raw();
    let body = CString::new(body)?.into_raw();

    let result = unsafe {
        let r = Send(config, method, path, body);
        CStr::from_ptr(r).to_str()?
    };
    // ...
}

実行は cargo run です。

$ cd examples/rust
$ cargo run -- --config /path/to/arc.json '{"message": "hey"}'

Rust 1.74.1 で動作を確認しました。

AWS Lambda

さて、ようやく冒頭に紹介した soratun を改造して AWS Lambda から簡単に SORACOM Arc を使ってみました の続きのお話です。2021 年には Lambda 関数から SORACOM プラットフォームへデータを送信するために結構な手間をかけました。

  1. soratun を SORACOM プラットフォームへのプロキシサーバーとして動作するよう改造 (soraproxy と命名)
  2. soraproxy を Lambda extension として動作するよう AWS のサンプルを元にラッパースクリプトを作成し、Lambda レイヤーとして定義
  3. Lambda extension を経由して Lambda 関数から SORACOM プラットフォームへデータを送信

今回紹介した libsoratun を使うと 2 と 3 のパッケージと Lambda 関数の実装が簡単になります。

  1. 以下のような Lambda 関数を作成します。上の Python と同じ要領です。

    import ctypes
    import json
    import sys
    
    soratun = ctypes.cdll.LoadLibrary("libsoratun.so")
    
    with open("arc.json", "r") as file:
        config = ctypes.c_char_p(file.read().encode("utf-8"))
    
    def lambda_handler(event, context):
        method = ctypes.c_char_p(b"POST")
        path = ctypes.c_char_p(b"/")
        body = ctypes.c_char_p("hello from lambda".encode("utf-8"))
        soratun.Send(config, method, path, body)
        return {
            'statusCode': 200,
            'body': "Successfully sent to the unified endpoint"
        }
    
  2. libsoratun.soarc.json を上記のファイルと同じディレクトリに配置し ZIP アーカイブにまとめます。

  3. Lambda 関数としてアップロードします。

Lambda 関数を実行すると以下のようなログが出力されます。

START RequestId: a932c113-3e88-4bea-adb1-6b58adee2040 Version: $LATEST
DEBUG: (libsoratun/tunnel) 2023/12/14 13:38:09 Soracom Arc connection configuration:
[Interface]
Address = 10.180.169.243/32
PrivateKey = <secret>
[Peer]
PublicKey = bea25b2e59cc6f9ee4f55aecde3a9dc0d6982864358d45065324f67b1f05f167
AllowedIPs = 100.127.0.0/16, 192.168.2.0/24
Endpoint = 35.75.26.187:11010
DEBUG: (libsoratun/tunnel) 2023/12/14 13:38:09 UAPI: Updating private key
DEBUG: (libsoratun/tunnel) 2023/12/14 13:38:09 Routine: decryption worker 1 - started
DEBUG: (libsoratun/tunnel) 2023/12/14 13:38:09 Routine: encryption worker 1 - started
DEBUG: (libsoratun/tunnel) 2023/12/14 13:38:09 Routine: decryption worker 2 - started
DEBUG: (libsoratun/tunnel) 2023/12/14 13:38:09 Routine: handshake worker 1 - started
DEBUG: (libsoratun/tunnel) 2023/12/14 13:38:09 Routine: encryption worker 2 - started
DEBUG: (libsoratun/tunnel) 2023/12/14 13:38:09 Routine: handshake worker 2 - started
DEBUG: (libsoratun/tunnel) 2023/12/14 13:38:09 Routine: TUN reader - started
DEBUG: (libsoratun/tunnel) 2023/12/14 13:38:09 peer(vqJb…F8Wc) - UAPI: Created
DEBUG: (libsoratun/tunnel) 2023/12/14 13:38:09 peer(vqJb…F8Wc) - UAPI: Updating endpoint
DEBUG: (libsoratun/tunnel) 2023/12/14 13:38:09 peer(vqJb…F8Wc) - UAPI: Adding allowedip
DEBUG: (libsoratun/tunnel) 2023/12/14 13:38:09 peer(vqJb…F8Wc) - UAPI: Adding allowedip
DEBUG: (libsoratun/tunnel) 2023/12/14 13:38:09 UDP bind has been updated
DEBUG: (libsoratun/tunnel) 2023/12/14 13:38:09 peer(vqJb…F8Wc) - Starting
DEBUG: (libsoratun/tunnel) 2023/12/14 13:38:09 Interface state was Down, requested Up, now Up
DEBUG: (libsoratun/client) 2023/12/14 13:38:09 Soracom Unified Endpoint URL: http://100.127.69.42:80
DEBUG: (libsoratun/client) 2023/12/14 13:38:09 User-Agent: libsoratun/0c04f39
DEBUG: (libsoratun/client) 2023/12/14 13:38:09 Sent HTTP request:
POST / HTTP/1.1
Host: 100.127.69.42:80
hello from lambda
DEBUG: (libsoratun/tunnel) 2023/12/14 13:38:09 Routine: event worker - started
DEBUG: (libsoratun/tunnel) 2023/12/14 13:38:09 Interface up requested
DEBUG: (libsoratun/tunnel) 2023/12/14 13:38:09 Routine: receive incoming v4 - started
DEBUG: (libsoratun/tunnel) 2023/12/14 13:38:09 peer(vqJb…F8Wc) - Sending handshake initiation
DEBUG: (libsoratun/tunnel) 2023/12/14 13:38:09 peer(vqJb…F8Wc) - Routine: sequential sender - started
DEBUG: (libsoratun/tunnel) 2023/12/14 13:38:09 peer(vqJb…F8Wc) - Routine: sequential receiver - started
DEBUG: (libsoratun/tunnel) 2023/12/14 13:38:09 peer(vqJb…F8Wc) - Received handshake response
DEBUG: (libsoratun/client) 2023/12/14 13:38:09 Received HTTP response:
HTTP/1.1 201 Created
Connection: close
Content-Length: 0
Date: Thu, 14 Dec 2023 13:38:09 GMT
END RequestId: a932c113-3e88-4bea-adb1-6b58adee2040
REPORT RequestId: a932c113-3e88-4bea-adb1-6b58adee2040	Duration: 150.61 ms	Billed Duration: 151 ms	Memory Size: 128 MB	Max Memory Used: 55 MB

AWS Lambda の Python 3.11 ランタイムで動作を確認しました。

libsoratun.so は 8.5 MB と大きめのため、Lambda レイヤー としてもいいかもしれません。また、arc.json を ZIP アーカイブに含めず、AWS Secrets Manager に認証情報を保管し設定ファイルを実行時に生成する方法もよさそうです。今回は説明のためにわかりやすさを優先しました。

まとめ

今回は SORACOM Arc への接続を C のライブラリとしてパッケージした libsoratun を紹介しました。

今回は試していませんが、Ruby には Ruby-FFI gem があり、Perl には FFI::Platypus モジュール、PHP は標準で FFI をサポートしています。プラットフォーム毎にビルドしなければならないという手間はあるものの、一度作ってしまえば使い慣れたスクリプト言語から root 権限不要で SORACOM プラットフォームへデータが送信できますのでテストなどの際に便利にお使いいただけると思います。

2023 年 12 月現在、SORACOM Arc は 1 アカウントあたりバーチャル SIM/Subscriber 1 契約分の基本使用料 (月額)、1 GB 分のデータ通信が無料で利用できます。また、1 アカウントあたり 1 回に限り初期費用が無料です。ぜひお試しください。