自前のlocaltunnelを構築してみる

3 min read読了の目安(約3400字

localtunnel の紹介

みなさん、開発用にローカルサーバを HTTPS サイトとして外部露出したくなったことはありませんか?

この手の要件でよく使われるツールとして ngrok というものがあるのですが、ローカルサーバをグローバルに正当な証明書がついた HTTPS サイトとして露出させるというツールとしては便利ですが、お金を払わないとサブドメインが固定できない、なんか一定時間ごとに切れる(?)という難点があります。

そういう場合は、 localtunnel が便利です。無料で利用できて、サブドメインを固定できます。

CLI で立ち上げることができるほか、NodeJS 上で自分でプログラムを書いて立ち上げることもできます。

const localtunnel = require("localtunnel");

(async () => {
  const tunnel = await localtunnel({
    // 希望するサブドメイン(すでに取られていたらランダム文字列)
    subdomain: "myfavoritesubdomain",
    // リバースプロキシするポート
    port: 3000,
  });
  console.log(`Open ${tunnel.url}.`);
})();

Node.js で関数として立ち上げられるのは、他ツールとの連携という意味合いでかなり便利です。

localtunnel の問題点

ただ、こんな便利な localtunnel ですが、サーバが海の向こうにあるせいか、利用者数がちょっと多いかもしれないせいか、重かったり応答が遅かったりするのが結構難点です。

また、自分の関しないところでダウンタイムに突入するのもやや難があります。

自分専用の localtunnel を立ててみる

そういうことで、自分たちだけのための localtunnel 立ててみます。

localtunnel は サーバ用の NodeJS プロジェクト が公開されているので、それを用いましょう。

AWS で立てるとして、下記のような構成が考えられます。

  • パブリック IP を持つ EC2 インスタンスを立てる
    • セキュリティグループ上、 :1000 番以上のポートをすべて受け付けられるようにする。
      • クライアントからトンネルを貼る際、サーバの 1000 番以上のポートがランダムで利用されます。
  • HTTPS 終端させる。以下の 2 つのオプションが選べます。
    • ALB (+ ACM) で HTTPS 終端させる
      • HTTP 側には HTTPS のアドレスにリダイレクトするルールを定義しておきましょう。
      • Cognito や OIDC などの認証を ALB 側にアタッチしてもいいかもしれません。
    • EC2 上で Let’s Encypt でワイルドカード証明書を取得して Nginx まで HTTPS 終端し、リバースプロキシする
      • Let's Encrypt でワイルドカード証明書を取得する場合、DNS 上で所有証明をする必要があります。通常、自動化は容易にできないので certbot-dns-route53 などを利用するとよさそうです。
        • EC2 インスタンスにインスタンスプロファイルで Route53 関連のロールを与えればツールが自動的に DNS を編集してくれるようです。
      • Nginx の設定例自体は別プロジェクトにて用意されています。
  • 下記の DNS レコードを定義します。
    • mysubdomain.example.com => EC2 インスタンス
    • *.mysubdomain.example.com => ALB or Let's Encrypt の場合は EC2 インスタンスの IP アドレス
  • EC2 インスタンス上でサーバ用の NodeJS プロジェクトを立ち上げる。
    • Docker イメージ もありますが、ネットワークモードはホストモードにする必要があります。
    • 上記例だと --domain mysubdomain.example.com というオプションが必要になります。

私は ALB + EC2 (+Terraform) で立ち上げましたが、certbot で DNS チャレンジを自動化できるのであれば、その構成の方が安価かつ引っかかりどころが少ないように感じました。

また上記のように自前のサーバを立ち上げた後、クライアント側からは、下記のように host オプションをつけて繋げることができます。

const localtunnel = require("localtunnel");

(async () => {
  const tunnel = await localtunnel({
    host: "http://mysubdomain.example.com", // or https://mysubdomain.example.com
    subdomain: "myfavoritesubdomain",
    port: 3000,
  });
  console.log(`Open ${tunnel.url}.`);
})();

実際に自分専用の localtunnel を運用してみての課題

他人のただ乗りを禁止したくなってくる

上記で自前サーバを立ち上げられるのですが、誰でもトンネルを登録できるようになっているので、誰かに見つかったが最後、踏み台にされる可能性があります。

サブドメインが解放されない

localtunnel のクライアントが意図せず終了すると、特にサブドメインが解放されずサーバに登録されっぱなしになります。たとえば、 hoge.example.com を希望しても、前の登録がサーバ上で解放されずに残っていると another-hoge.example.com みたいなドメインが勝手に割り当てられてしまいます。

これへの対処は下記が考えられます。

  • 一定時間ごとに localtunnel サーバを再起動して、強制解放させる。
  • localtunnel サーバを改造して、サブドメインの取得を後勝ちにする。

私は後者を選びました。 ClientManager.js の一行書き換えれば済みます。

        if (clients[id]) {
-            id = hri.random();
+            this.removeClient(id);
        }

特に難しいソースでもないので、他にも追加したい機能(認証機能など)を追加するのもよさそうです。

まとめ

localtunnel を使えば簡単に HTTPS トンネルサーバを利用・自前構築できます。