SORACOM Arcを特権なしで使えるツールを開発してみた

# はじめに

こちらは[SORACOM Advent Calendar 2021](https://qiita.com/advent-calendar/2021/soracom) 13日目の記事です。
昨日は@ozk009さんによる[「Androidスマホで簡易監視カメラをつくる」](https://qiita.com/ozk009/items/dc8a88b672df3c44d898)でした。
今はあまりコードを書かなくても、サービスの組み合わせでここまでできるようになっているのですね。とても勉強になります。

さて、私はSORACOM Arc関係の何かを書きます、と予告に書いていたので、SORACOM Arcの話です。

以前書いた記事[「SORACOM Arcを使ってAWS LambdaからSORACOMサービスを利用する」](https://qiita.com/1stship/items/b6a69ec3dcb9e32bddd9)にて、SORACOM Arcで利用しているVPN技術であるWireGuardはネットワークに関する特権(NET_ADMIN)が必要になるため、AWS Lambdaでは基本的には使えない、と書きました。

そして解決方法として、AWS LambdaをVPCの中に入れてSORACOM ArcのルーターとなるNATインスタンスを使う、という構成を提示したのですが、ぶっちゃけ不本意な構成なんですよね。NATインスタンスを動かすと、使っていない時にはコストがかからない、というLambdaの長所がなくなりますし、1つの仮想SIMに対して1つのNATインスタンスが必要になってしまうのも困ります。

![lambda-nat.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/21911eff-c829-283b-f406-6c83f85add28.png)

これを解決する方法として、WireGuardの通信を単なるアプリケーションの通信として実装する、という方法が考えられます。SORACOM Arcに対する通信をパケットキャプチャしてみると、単なるUDPの通信にすぎないことがわかります。であれば、WireGuardのハンドシェイクやら暗号化を実装してパケットの中身を作れれば、特権なしでも普通に使えるはずです。

その記事を書いた時は(アプリケーションでWireGuardのVPNを実装すればいける可能性はあるけど現実的ではない)ということで断念しましたが、WireGuardのクライアントはさまざまな言語で実装されており、参考にできるコードもあるので、勝算は十分ある、ということで今回は特権なしでSORACOM Arcを使えるツール開発にチャレンジしてみました。参考にしたのはSORACOM公式のツールである[soratun](https://github.com/soracom/soratun)、およびsoratunから利用している[wireguard-go](https://github.com/WireGuard/wireguard-go)です。

# 作ったもの：wireguard-oneshot

今回開発するツールの仕様は以下のものとしました。

- SORACOM Arc(WireGuard サーバー)に接続し、UDPのパケットを1回送信し、その結果を受信して終了する

通常のWireGuardはVPNなので、基本的には常時接続し、UDPに限らずTCPやICMPなどさまざまな通信を利用できるものです。常時接続はLambdaのようなイベント処理と相性が悪いですし、今回は仕組み上パケット処理を自分で実装しないといけないため、まずは簡単なプロトコルに絞りたかったためです。

通常はUDPのパケットを1つだけ飛ばせる通信にはあまり需要ないと思いますが、SORACOM ARCおよびSORACOMのアプリケーションサービスの組み合わせの場合は十分価値があります。ここにUDPパケットを飛ばすと、SORACOM Beamなどによるプロトコル変換やSORACOM Orbitによるデータフォーマット変換により、さまざまなクラウドに対する連携が可能となるためです。素晴らしいですね。

ということでできたのがこちらです。

**[wireguard-oneshot](https://github.com/1stship/wireguard-oneshot)**

簡単にいうと以下のような処理をしています。

- 入力を受け取る
- WireGuardのハンドシェイクして、送受信に必要な共通鍵を作成する
- 送信に必要なIP、UDPパケットを作り、ペイロードと一緒に暗号化してUDPで送信する
- UDPで受信して復号し、ペイロードを取り出す

コードの詳細な説明は今回はしません。wireguard-goから利用したコードが多く、自分でもあまり理解できていないところが多いためです。コードが理解できたらそのうちWireGuardの解説記事を書くかもしれません。(WireGuard自体もですが、IPヘッダなどを自分で作るのも面倒だった。。なんかいい方法があればよいのですが)

例外処理なども甘いので現段階で実用レベルではないのですが、とりあえず動くところまではできました。
さっそく試してみましょう！

# コマンドとしての利用

## コマンドラインツールのダウンロード

まずはコマンドラインから利用してみましょう。プロジェクトをcloneしてビルドしてもよいのですが、ビルド済みのバイナリを用意したのでこちらを使うのが簡単です。

こちらのページから環境に合うものを選んでダウンロードしてください。
https://github.com/1stship/wireguard-oneshot/releases/tag/v0.0.1

macOSだとこのようなエラーが発生するかもしれません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/0258b9ed-1749-e887-48b1-364ba58484d4.png)

この場合は、いったんcontrolを押しながら、アプリケーションを開いてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/569bd3e9-c135-ca51-bf2a-e18839d6450b.png)

このような確認が表示されますので、「開く」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/8877b2dd-6fb6-411a-53c0-4ceb9ed0bec3.png)

するとこのように実行された結果が表示され、以降普通に起動できるようになります。ついでにヘルプも出てきますので参考にしていただければと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/b0c12c61-481f-326c-e1d0-b654fd46fb49.png)

## バーチャルSIMの作成

次にバーチャルSIMを作成し、privateKey、publicKey、clientIpAddressを入手します。

1.SORACOMにログインする(説明略)
2.「+SIM登録」をクリックする

![スクリーンショット 2021-12-12 16.01.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/b952af5f-3ae1-0ea9-8edd-b7328e2812b6.png)


3.バーチャルSIMを登録タブを選択し、登録ボタンをクリックする

![スクリーンショット 2021-12-12 16.03.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/afad4e7e-fb3f-9e27-1aaf-0cf6d5611efd.png)

費用が発生するため注意してください。(初期費用55円、基本料金88円/月、税込、2021/12/13現在)

4.画面に表示される情報を控える

![スクリーンショット 2021-12-12 16.11.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/d3e53270-94dc-e174-248d-bf6951138b10.png)

PrivateKey、Address、PublicKey、Endpointの情報が必要になります。コピーしてテキストファイルに残しておくなどしておきましょう。

バーチャルSIMの作成は以上です。

## コマンドの実行1

コマンドを実行してみましょう。以下のコマンドで、Unified EndpointにTESTというデータを送信できます。{PrivateKey}、{PublicKey}、{Endpoint}、{Address}には先ほど取得した情報を入力します。ただし**Addressの/32の部分は入力しないようにしてください。(10.1.2.3/32であれば10.1.2.3と入力)**

```
./wireguard-oneshot -privateKey "{PrivateKey}" -publicKey "{PublicKey}" -endpoint "{Endpoint}" -clientIpAddress "{Address}(/32は除く)" -destinationIpAddress "100.127.69.42" -destinationPort 23080 -payload "TEST"
```

うまくいけば応答が返ってきて終了します。うまくいかない場合はエラーが発生して終了するか、待ち状態になります。待ち状態のタイムアウトはかけていないため、終了しない場合はCtrl + Cなどで終了させてください。
ただしこの時点では、グループが設定されていません、というエラーになるはずです。

![スクリーンショット 2021-12-12 16.18.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/afa3da71-9b56-9599-f869-6c8a00011459.png)

## SIMグループの設定

とりあえず通信できてるっぽいことが確認できたところで、SORACOM Harvestへのデータ保存を試してみましょう。非常に簡単に試すことができます。

1.SIM管理画面にて、新しいSIM管理画面となっていることを確認。なっていなければ新しいSIM管理画面へ移動
2.対象のSIMにチェックを入れ、[操作] - [所属グループ変更]をクリックする
![スクリーンショット 2021-12-12 16.31.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/4f3ec363-46a3-0f69-4f11-800be923c6c5.png)

3.新しい所属グループを選択し、新しいグループを作成をクリックする
![スクリーンショット 2021-12-12 16.34.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/f9934605-a514-4c2e-50e2-b7600e68d28e.png)

4.適当な名前を入力してグループ作成する
![スクリーンショット 2021-12-12 16.36.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/b2c1cf45-81a2-8223-0cc0-6efea1e119e0.png)

5.グループ変更する
![スクリーンショット 2021-12-12 16.36.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/2b26476b-1bc5-f020-3a90-83459f8dcdf0.png)

6.グループのリンクをクリックしグループ設定画面へ移動する
![スクリーンショット 2021-12-12 16.38.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/b944a513-4c53-2e8c-ee43-ae8920cf40cc.png)

7.SORACOM Harvest Dataの設定を開き、ONにして保存する
![スクリーンショット 2021-12-12 16.39.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/4401bb30-6d05-909c-bcb7-54f550e9b005.png)

料金について確認されるのでOKにします。

これでグループの設定及びHarvest Dataへの保存設定は終わりです。再度コマンドを実行してみましょう。

## コマンドの実行2

同じコマンドを実行すると、今度は201が返ってくるはずです。

```
./wireguard-oneshot -privateKey "{PrivateKey}" -publicKey "{PublicKey}" -endpoint "{Endpoint}" -clientIpAddress "{Address}(/32は除く)" -destinationIpAddress "100.127.69.42" -destinationPort 23080 -payload "TEST"
```

![スクリーンショット 2021-12-12 16.43.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/6605d406-8d25-2ae0-c48f-eb37e9e4abcd.png)

念の為保存されているかを確認してみましょう。

SIM管理画面にて、対象のSIMにチェックを入れ、[操作] - [データを確認]をクリックします
![スクリーンショット 2021-12-12 16.46.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/c4ae2e8c-e8f2-e667-fe15-eb33ef7ac009.png)

一時処理済みデータに「TEST」が入っていればOKです。当然TESTだけではなく、JSONなどのデータを投入することも可能です。
![スクリーンショット 2021-12-12 16.49.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/afa44f83-2757-47d0-898a-1a5349cc3620.png)

特権なしでコマンドラインからSORACOM Arc経由でSORACOM Harvestおよびさまざまなアプリケーションサービスにデータを送信できることがわかりました。

WireGuardやsoratunなどでVPN接続すると、ネットワークインタフェースが増えたりルート設定が変わったりなど、ネットワーク環境に大きな影響を与えてしまいますが、この方法だと環境に対する影響はありません。より手軽にSORACOM Arcが使えるのではないかと思います。

一方、VPN接続すれば既存のアプリケーションがそのまま使える、という利点は失われます。特に携帯回線を使うSORACOM Airとインターネット回線を使うSORACOM Arcで同じアプリケーションが使える、というのは大きなメリットなので、それが使えなくなるのはもったいないですね。適材適所で使っていきましょう。

# AWS Lambda + API Gatewayで利用してみる

次はAWS LambdaでSORACOM Arcを使ってみましょう。このプログラムであれば、特権が必要ないためLambdaでも使えるはずです。また、Webアプリなどに組み込めるようAPI Gatewayと連携してWebAPIとして呼び出せるようにします。

## デプロイパッケージの作成

まずAPI Gatewayからの入力を受け取る部分を作ります。コマンドラインと同じパラメータを受け取り、処理に回すようにします。

```go:cmd/arc-gateway/main.go
package main

import (
	"encoding/base64"
	"encoding/json"

	"github.com/1stship/wireguard-oneshot"
	"github.com/aws/aws-lambda-go/events"
	"github.com/aws/aws-lambda-go/lambda"
)

type ArcGateway struct {
    PrivateKey           string `json:"privateKey"`
    PublicKey            string `json:"publicKey"`
    Endpoint             string `json:"endpoint"`
	ClientIpAddress      string `json:"clientIpAddress"`
	DestinationIpAddress string `json:"destinationIpAddress"`
	DestinationPort      int    `json:"destinationPort"`
	Payload              string `json:"payload"`
	PayloadFormat        string `json:"payloadFormat"`
}

func main() {
	lambda.Start(handler)
}

func handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
	body := request.Body
	var bodyDecoded []byte
	if request.IsBase64Encoded {
		bodyDecoded, _ = base64.StdEncoding.DecodeString(body)
	} else {
		bodyDecoded = []byte(body)
	}
	
	var input ArcGateway
	err := json.Unmarshal(bodyDecoded, &input)
	if err != nil {
		return events.APIGatewayProxyResponse{
			Body:       string(err.Error()),
			StatusCode: 400,
		}, err
	}

	config := wireguard.Configuration {
		PrivateKey: input.PrivateKey,
		PublicKey: input.PublicKey,
		Endpoint: input.Endpoint,
		ClientIpAddress: input.ClientIpAddress,
	}

	var payload []byte
	if input.PayloadFormat == "base64" {
		payload, err = base64.StdEncoding.DecodeString(input.Payload)
	    if err != nil {
		    return events.APIGatewayProxyResponse{
				Body:       string(err.Error()),
				StatusCode: 400,
			}, err
	    }
	} else {
		payload = []byte(input.Payload)
	}

	receivedBuffer, err := wireguard.UdpOneShot(payload, input.DestinationIpAddress, input.DestinationPort, config)
	if err != nil {
		return events.APIGatewayProxyResponse{
			Body:       string(err.Error()),
			StatusCode: 400,
		}, err
	}

	for i := 0; i < len(receivedBuffer); i++ {
		if (receivedBuffer[i] == 0) {
			receivedBuffer = receivedBuffer[0:i]
			break
		}
	}

	return events.APIGatewayProxyResponse{
        Body:       string(receivedBuffer),
        StatusCode: 200,
    }, nil
}
```

これをGOOS=linux、GOARCH=amd64としてビルドします。ビルドしたバイナリをzipで圧縮するとAWS Lambda用のデプロイパッケージとなります。

また、パッケージ済みのZIPファイルが、[リリースページ](https://github.com/1stship/wireguard-oneshot/releases/tag/v0.0.1)の[arc-gateway_linux_amd64_v0.0.1.zip](https://github.com/1stship/wireguard-oneshot/releases/download/v0.0.1/arc-gateway_linux_amd64_v0.0.1.zip)にあるので、こちらをダウンロードして利用しても良いです。

## AWS Lambda関数の作成

AWS Lambdaの関数を作成しましょう。ランタイムはGo 1.xとします。

1.AWSにログインする(説明略)
2.AWS Lambdaのサービスページへ移動する(説明略)
3.関数の作成をクリックする
![スクリーンショット 2021-12-12 18.08.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/64ff7608-9f39-e247-a582-cd1a2fb46077.png)

4.関数名を入力し、ランタイムをGo 1.xに変更して、関数を作成する
![スクリーンショット 2021-12-12 18.09.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/b254b870-763b-a317-6182-fdb84a790c2d.png)
他の項目はデフォルトのままでよいです。

5.アップロード元として.zipファイルを選択
![スクリーンショット 2021-12-12 18.12.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/85e8db83-6d4d-b0de-a70d-159ae92b5711.png)

6.zipファイルを選択してアップロードし、保存する
![スクリーンショット 2021-12-12 18.16.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/4485c938-9d50-9f6a-7f90-41c45da2961e.png)

7.ランタイム設定を編集する
![スクリーンショット 2021-12-12 18.18.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/4871fc56-b08f-dc1e-aa9f-0f90603bb24b.png)

8.ハンドラを「arc-gateway」に変更する
![スクリーンショット 2021-12-12 18.19.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/37ac5e35-30be-aea7-eea5-6b6479eaffc0.png)
ハンドラはzipファイルの中の実行ファイル名を指定します。リリースページのZIPファイルはarc-gatewayという名前で実行ファイルを作成しているので、それを指定します。

これでAWS Lambdaの準備は完了です。最後にAPI Gatewayの設定をします。

## API Gatewayの設定

AWS Lambdaの関数をWeb APIから呼び出すのに、API Gatewayを利用します。以前のREST API設定に比べ、今はHTTP APIという比較的設定しやすい方法があるため、そちらを利用します。

1.API Gatewayのサービスページへ移動(説明略)
2.APIを作成する
![スクリーンショット 2021-12-12 18.24.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/77f0e6d5-e952-c20d-1145-d653cecb1be8.png)

3.HTTP APIを構築する
![スクリーンショット 2021-12-12 18.25.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/08e354a4-97a7-037d-ae4d-340b25590a0a.png)

4.AWS Lambdaを統合に追加する
![スクリーンショット 2021-12-12 18.27.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/fd080efa-9726-add7-8dbd-f865d2c34534.png)

5.POST /でLambdaが起動されるようルートを設定する
![スクリーンショット 2021-12-12 18.31.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/d7ea47c5-e0a2-31da-073b-951615790561.png)
メソッドやパスは特にプログラム内ではみていませんが、一番簡単なPOST / で受け取れるように設定しておきます。

6.ステージを設定する(デフォルトのまま次へ)
![スクリーンショット 2021-12-12 18.33.56.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/4fff404c-40ed-aafd-7d67-a97dc5fee077.png)

7.確認して作成する(特に問題なければそのまま作成する)
![スクリーンショット 2021-12-12 18.34.41.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/d53823f7-f19c-f37c-4a04-66ff97314546.png)

8.URLを確認する
![スクリーンショット 2021-12-12 18.35.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/33a3cfae-90c2-7465-9d92-6377ae13e189.png)
$defaultステージのURLを確認しておきます。このURLで呼び出すことができます。

これでAPI Gatewayの準備も完了です。呼び出してみましょう！

## WebAPIの呼び出し

コマンドラインの際のパラメータをそのままJSONにするだけです。

```
curl -H "Content-Type: application/json" -d '{"privateKey":"{PrivateKey}","publicKey":"{PublicKey}","endpoint":"{Endpoint}","clientIpAddress":"{Address}(/32は除く)","destinationIpAddress":"100.127.69.42","destinationPort":23080,"payload":"API Gateway"}' {API GatewayのURL}/
```

{PrivateKey}、{PublicKey}、{Endpoint}、{Address}は同じように入力、{API GatewayのURL}は先ほど取得したAPI GatwayのURLを指定し、末尾に/をつけましょう。

先ほどと同じく201応答が返ってきました。
![スクリーンショット 2021-12-12 18.44.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/58461b62-b6ab-133d-23ae-f03e9fd0b556.png)

データを確認すると、API Gatewayというデータが入っています。
![スクリーンショット 2021-12-12 18.47.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/f818e5e0-303b-e885-c8a8-85e5408360ca.png)

AWS Lambda + API Gatewayでの利用も問題ありません。当初の目的は達成です！
API GatewayにCORSの設定をすると、Webアプリから呼び出すこともできるようになります。

# LINE Notifyに通知する

Harvestにデータを入れるだけではなく、LINE Notifyへの通知なんかも試してみました。
以前SORACOMのユーザーグループと、LINE Developers Community合同でLINE通知のハンズオンを実施した時のテキストが公開されていましたので、こちらの手順通りやりました。

https://soracomug.github.io/soracom-arc-button-simulator-handson/#0
バーチャルSIMの発行や、SIMグループの作成などは終わっており、WireGuardのインストールなどは必要ないため、以下の4点を実施しました。

- 3. LINE Notify 用のトークン取得
- 6. SORACOM SIMグループ セットアップのSORACOM Air for セルラー設定
- 9. SORACOM Orbitセットアップ
- 10. SORACOM Beamセットアップ

こちらはデータをバイナリで送る必要があるため、payloadFormat = base64を追加してWebAPIに送信します。(データは[過去にSORACOM Peekでキャプチャしたデータ](https://qiita.com/1stship/items/0108b60a89893c97cd5b#lte-m%E3%83%9C%E3%82%BF%E3%83%B3)を流用しています)

```
curl -H "Content-Type: application/json" -d '{"privateKey":"{PrivateKey}","publicKey":"{PublicKey}","endpoint":"{Endpoint}","clientIpAddress":"{Address}(/32は除く)","destinationIpAddress":"100.127.69.42","destinationPort":23080,"payload":"TQEDUQ==","payloadFormat":"base64"}' {API GatewayのURL}/
```

![スクリーンショット 2021-12-12 19.38.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542185/ada672b9-51e7-71e3-ca59-ff262efab3e9.png)
こちらもちゃんと連携できてますね！

# 終わりに

今回は特権を使うことなしにSORACOM Arcを利用するツールを開発してみました。
例外処理やタイムアウト処理、接続の維持やデータの検証など、本来必要な処理をすっ飛ばしているので、実用的と言えるものではないですが、ちょっとしたログデータをSORACOMにアップロードするとか、メールやLINEへの送信などをちょっと足したい、というような場合には結構便利に使えるのではないかと思います。

また、実機がない状態でのバーチャルハンズオンに利用できますね。
SORACOMのユーザー会であるSORACOM UGでは、初心者歓迎のSORACOM UG ビギナーズや、LINE Developers Communityとの合同企画などでバーチャルハンズオンを実施しています。
IoTにご興味ある方は、直近では2021/12/18に企画している[「SORACOM UG ビギナーズ #6〜パソコンがあればできるIoT体験〜」](https://soracomug-tokyo.connpass.com/event/230616/)などにぜひご参加ください！

明日は@tacckさんです！よろしくお願いします！