🚃

Tailscale Funnelで自宅サーバーのRailsアプリケーションを独自ドメインで公開する方法

2024/03/18に公開

自宅サーバーで独自ドメインのWebサービスを公開するためには、ポートを開けたり、固定IPアドレスを用意するかDynamic DNSを使うか、みたいな手間が必要でした。VPNサービスであるTailscaleには、Tailscaleのネットワーク(Tailnet)上の任意のTCPポートをインターネットに向けて公開できるTailscale Funnelという機能があります。これを活用して、自宅サーバー上のRailsアプリケーションを安全かつかんたんに公開できないか? 試してみたので、その方法をご紹介します。

この記事について

説明すること

  • TailscaleからのアクセスをRailsでどのように扱うかのポイント

説明しないこと

  • Tailscaleについて
  • CloudFrontについて
  • Railsアプリケーションのデプロイについて
  • ステップ・バイ・ステップで動くものを作る

参考

構成

以下に、Tailscale Funel + 独自ドメインの構成で自宅サーバーに到達するまでの流れを示します。

ポイント

  • 自宅サーバーでTailscale Funnelを実行する
  • Tailscale Funnelリレーに独自ドメインは設定できないため、CloudFrontを前段に置く
    • Route53からCloudFrontディストリビューションを指定する
    • 独自ドメインのサーバ証明書はACMで発行して使う
  • Tailscle FunnelはSNIによって接続先ノードを決定するため、CloudFrontからのオリジンリクエストの Host は独自ドメインでなく machinename.yourtailnetname.ts.net である必要がある
    • Railsにリクエストのホスト名を伝えるため X-Forwarded-Host が必要
    • Tailscale Funnel が X-Forwarded-Hostmachinename.yourtailnetname.ts.net を設定する
      • CloudFrontのビューワーリクエストイベントで Host ヘッダの値を Viewer-Host に転記しておき、Funnel通過後、Rackミドルウェアで X-Forwarded-Host に追記する

自宅サーバーでTailscale Funnelを実行する

ACL

Tailscale ACL で Funnel を使用可能なノードを設定する必要があります。ここでは yuichi@test.host が管理するノードで利用できるようにします。

{
  "groups": {
    "group:funnel":  ["yuichi@test.host"]
  },
  "nodeAttrs": [
    {
      "target": ["group:funnel"],
      "attr":   ["funnel"]
    }
  ]
}

Funnelを実行

Funnelを起動し、リレーからの接続をtcpポートへ送るよう指示します。(今回、Railsアプリケーションの前にTraefikがいるので、80番です。Railsアプリケーションへ直接であれば 3000番が普通かと思います)
--bg のオプションを付けるとバックグラウンドで起動します。 tailscale up したときに自動復帰します。

$ tailscale funnel --bg http://127.0.0.1:80
Available on the internet:

https://my-server.tailabcde.ts.net/
|-- proxy http://127.0.0.1:80

これで https://my-server.tailabcde.ts.net/http://127.0.0.1:80 に届くようになりました。

CloudFrontを設定する

CloudFrontでの設定のポイントは次の3つです。

  1. FunnelのURL(https://my-server.tailabcde.ts.net/)をオリジンに指定する
  2. オリジンへのリクエストには、Host ヘッダーを含めない
  3. オリジンへのリクエストの前に実行するように、ビューワーリクエストHost ヘッダーの値を独自の Viewer-Host ヘッダーの値に転記する

CDKで書くと以下のようになります。

const viewerHostFunction = new cloudfront.Function(this, 'ViewerHostFunction', {
	code: cloudfront.FunctionCode.fromFile({
	    filePath: 'src/lambda/viewer_host/index.js'
	}),
    runtime: cloudfront.FunctionRuntime.JS_2_0
});

const distribution = new cloudfront.Distribution(this, 'Distribution', {
    // 省略
    defaultBehavior: {
	    origin: new cloudfront_origins.HttpOrigin("my-server.tailabcde.ts.net"),
        // 省略
        viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        protocolPolicy: cloudfront.OriginProtocolPolicy.MATCH_VIEWER,
        originRequestPolicy: cloudfront.OriginRequestPolicy.ALL_VIEWER_EXCEPT_HOST_HEADER,
        functionAssociations: [
            {
                function: viewerHostFunction,
                eventType: cloudfront.FunctionEventType.VIEWER_REQUEST
            }
        ]
    }
    // 省略
});
function handler(event) {
    var request = event.request;

    request.headers["viewer-host"] = request.headers.host;

    return request;
}

Railsアプリケーションを設定する

Rackミドルウェアで X-Forwarded-Host に追記

# CloudFront ViewerRequest イベントでセットした VIEWR_HOST ヘッダーを使って、HTTP_X_FORWARDED_HOST ヘッダーを書き換える
# Tailscale Funnel では SNI を利用する関係で Host ヘッダーは xxxxxxx.xxxxxxxx.ts.net でなくてはならないので、CloudFront から Host ヘッダーを転送できない
# CloudFront ViewerRequest で HTTP_X_FORWARDED_HOST を設定していても、経路中で上書きされ  xxxxxxx.xxxxxxxx.ts.net になってしまったので、ここで末尾に追加する
# rackでは末尾の値が優先される為
# https://github.com/rack/rack/blob/ae7d6a171963a70918b4e43525408c571a3f28fe/lib/rack/request.rb#L402
class ProxyViewerHost
  def initialize(app)
    @app = app
  end

  def call(env)
    return @app.call(env) unless env["HTTP_VIEWER_HOST"]

    env["HTTP_X_FORWARDED_HOST"] = [ env["HTTP_X_FORWARDED_HOST"].presence, env["HTTP_VIEWER_HOST"].presence ].compact.join(", ")

    @app.call(env)
  end
end
require "middleware/proxy_viewer_host"
config.middleware.insert 0, ProxyViewerHost

実際の成果物

Comparing 952a3b2328f2fe7490d62ab43e0355747980f070...febf3ec04d0866a6eda9a8ab6b56511006945368 · takeyuwebinc/rails-takeyuwebinc (github.com)

まとめ

この記事では、TailscaleのFunnel機能を利用して、自宅サーバー上のRailsアプリケーションをインターネットに公開する方法について解説しました。Tailscale Funnelを使用することで、ポートの開放や固定IPアドレスの準備、Dynamic DNSの利用などの手間を省くことができます。また、CloudFrontと組み合わせることで、独自ドメインでの提供が可能になります。

重要なポイントは、Tailscale Funnelリレーに独自ドメインを設定できないため、CloudFrontなどを前段に置くこと、そしてSNIによって接続先ノードを決定するため、CloudFrontからのオリジンリクエストの Hostmachinename.yourtailnetname.ts.net である必要があることです。

最後に、Railsアプリケーションの設定を行い、Rackミドルウェアで X-Forwarded-Host に独自ドメインのホスト名を追記することで、アプリケーションにホスト名を知らせることができます。

この方法を利用すれば、自宅サーバーを活用して、自宅サーバーを直接インターネットに出さずに、Railsアプリケーションを公開することができます。興味のある方は、ぜひ試してみてください。

タケユー・ウェブ株式会社

Discussion