[小ネタ] Amazon Route 53で長いTXTレコードを作成する際に気を付けたいこと

2024.06.12

しばたです。

Route 53で長い文字列のTXTレコードを作成する際の仕様について勘違いしてたことがあり、本記事で恥を晒すとともに簡単にまとめてみました。

Route 53で長いTXTレコードを作成するには

Route 53で長いTXTレコードを作成する方法自体は以下のre:Postやドキュメントにある通り「255文字以内で二重引用符(")で囲って分割」すればOKです。
大抵の場合はスペース区切りで各文字列を分けていると思います。

そして、マネジメントコンソールのUIでは「複数のレコードを登録する場合は改行区切り」にする必要があります。
簡単な例として、1レコード目の値が長い文字列、2レコード目に短い文字列を登録したい時は下図の様にする必要があります。

(今回はテスト用なので1レコード目の文字列は短くしてあります。実際には255文字を超える場合に分割することになるでしょう。)

digコマンドで登録結果を確認するとこんな感じで意図通り2レコード返されます。

# 意図通りの登録がされた場合 : 2レコード
$ dig +noall +ans longvalue.example.shibata.tech TXT
longvalue.example.shibata.tech. 0 IN    TXT     "First long value part1" "long value part2" "long value part3"
longvalue.example.shibata.tech. 0 IN    TXT     "Second value"

私は本記事を書くまで長い文字列を改行区切りにすると誤解しており、次の様な登録ができると勘違いしていました。

(実データは長い文字列になるので間違いに気づきにくい...)

こうしてしまうと4レコードとして登録されてしまうのでdigコマンドの結果も意図した通りになりません。

# 意図とは異なる登録がされた場合 : 4レコード
$ dig +noall +ans longvalue.example.shibata.tech TXT +ans
longvalue.example.shibata.tech. 0 IN    TXT     "long value part3"
longvalue.example.shibata.tech. 0 IN    TXT     "First long value part1"
longvalue.example.shibata.tech. 0 IN    TXT     "Second value"
longvalue.example.shibata.tech. 0 IN    TXT     "long value part2"

余談 : Route 53ではスペース区切りは必須でない

これは記事を書いている最中に気が付いたのですが、Route 53において長い文字列を設定する際に区切り文字は必須ではありませんでした。
下図の様に二重引用符で分割されていれば連結した形でも構いません。

(スペース区切りにしないでも登録できてしまう...)

これでもdigコマンドの結果は期待通りです。

# 期待通りの2レコードになる
$ dig +noall +ans longvalue.example.shibata.tech TXT
longvalue.example.shibata.tech. 0 IN    TXT     "First long value part1" "long value part2" "long value part3"
longvalue.example.shibata.tech. 0 IN    TXT     "Second value"

ただし、これはあくまでRoute 53のUIの話であり他のDNSにおいては区切り文字が必須になり得ますのでご注意ください。

例えばCloudflare DNSの設定UIでは区切り文字が無い場合は二重引用符がエスケープされた一つの文字列として扱われてしまいます。

上図の様な登録をすると

"First long value part1\"\"long value part2\"\"long value part3"

という1つの文字列として扱われてしまいます。(加えて255文字単位で自動分割される)
意図した内容にするにはスペース区切りにする必要があります。

AWS CLIで操作する場合

AWS CLIでレコード登録する際はaws route53 change-resource-record-setsコマンドを使いますが、長いTXTレコードを登録する場合はResourceRecordsプロパティに前述のルールを適用してやればOKです。

こちらだとそれぞれの値をValueプロパティで指定する形になるので改行の扱いを間違えることは無いですね。

# AWS CLIで長い文字列のTXTレコードを登録する例
#  * 各レコードをValueで指定するので間違えにくい
#  * パラメーターをJSONで指定するので、値に使う二重引用符は要エスケープ
params=$(cat <<EOM
{
  "Comment": "Test long record",
  "Changes": [
    {
      "Action": "UPSERT",
      "ResourceRecordSet": {
        "Name": "longvalue.yourdomain.example",
        "Type": "TXT",
        "TTL": 60,
        "ResourceRecords": [
          {
            "Value": "\"First long value part1\" \"long value part2\" \"long value part3\""
          },
          {
            "Value": "\"Second value\""
          }
        ]
      }
    }
  ]
}
EOM
)
aws route53 change-resource-record-sets --hosted-zone-id Z0000000000000 --change-batch "$params"

JSONでパラメーター指定する都合Value内部の二重引用符はエスケープする必要があります。
また、AWS CLI(およびAPI)で登録する際は二重引用符の指定を忘れるとエラーとなりますのでご注意ください。

簡単なまとめ

ここまでの内容を改めてまとめておきます。

  • Route 53のTXTレコードで255文字以上登録するときは二重引用符(")で分割する
  • 分割に際しスペース区切りにしなくても大丈夫だがスペース区切りにしておいた方が安全
    • ドキュメントでもスペース区切りにしている
  • 改行は各レコードの区切り文字なので分割した文字列の途中で使ってはいけない
    • レコードを分ける時だけ改行を入れる

たったこれだけの話なんですが、実際にマネジメントコンソールで長い文字列を扱うと画面の見にくさも相まって気づきにくいです。

おまけ1 : パケットキャプチャしてみる

最後におまけとしてTXTレコードのクエリをパケットキャプチャしてみます。

一番最初に紹介したレコードを登録した状態で以下のdigコマンドを実行し、そのパケットをWiresharkで取得してみました。

# パケットキャプチャしたコマンド
#  * DNSサーバーに8.8.8.8指定してますが、とりあえず使えそうなやつを選んだ以上の意味はありません
dig +noall +ans longvalue.example.shibata.tech TXT @8.8.8.8

コマンドの実行結果はこんな感じで、

$ dig +noall +ans longvalue.example.shibata.tech TXT @8.8.8.8
longvalue.example.shibata.tech. 60 IN   TXT     "First long value part1" "long value part2" "long value part3"
longvalue.example.shibata.tech. 60 IN   TXT     "Second value"

パケットキャプチャの結果(レスポンスのみ)はこんな感じになります。

Answerセクションに2つのResource Record(RR)がセットされ、最初のRRのRDATAに3つのパートに分かれたテキストがセットされているのが見て取れます。

次のRRに2レコード目のテキストが入っています。

DNSパケットのフォーマットとしてTXTレコードのRDATA(TXT-DATA)は以下の様に1つ以上の文字列(<character-string>)から構成され、

    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    /                   TXT-DATA                    /
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

where:

TXT-DATA        One or more <character-string>s.

TXT RRs are used to hold descriptive text.  The semantics of the text
depends on the domain where it is found.

この<character-string>

<character-string> is a single
length octet followed by that number of characters.  <character-string>
is treated as binary information, and can be up to 256 characters in
length (including the length octet).

と、最初のオクテットを文字列長とした最長256文字 *1のバイト列で表現するルールとなっています。

TXTレコードの255文字制限はこの仕様に由来しており、実際にパケットを見てみると分かりやすいですね。

なお、この255文字単位に分かれた文字列の扱いについては使用者次第であり、現実にはSPF、DKIM、DMARCでの使用がほとんどになると思いますが、三者ともそのまま連結して扱う様になっています。

  • SPF : RFC 7208
    • 3.3. Multiple Strings in a Single DNS Record
  • DKIM : RFC 6376
    • 3.6.2.2. Resource Record Types for Key Storage
  • DMARC : RFC 7489
    • 6.1. DMARC Policy Record

連結時にスペースを入れたりすることは無いので特にSPFを扱う場合は気を付けると良いでしょう。

おまけ2 : 最長文字列長について

パケットフォーマットを見るとRDATAの長さを表現するRDLENGTHが16bitで定義されているので、理論上は

  • 255 文字 * 256 回 = 65280 文字

まで指定可能なはずです。

とはいえ実際のサービスでそこまで許可しているものは存在せず、Route 53においては「最長4000文字 *2」までに制限されています。
他社サービスにおいても

  • Google Cloud DNS : 最長1000文字
    • 緩和申請は可能な模様
  • Azure DNS : 最長4000文字
    • Microsoft National Cloudsでは最長1024文字の場合あり
  • Cloudflare DNS : 最長2048文字
    • 加えて同名のレコードの場合、全レコードで8192文字まで

といった感じで結構まちまちです。
UDPパケットサイズの問題もあるので4000文字くらいが現実的な上限なのかな?と思うものの実際のところよくわかりませんでした...

終わりに

以上となります。

単純な話ではあるのですが、調べてみるとかなりの時間を要しました。
特にRFCを追うのに非常に苦労し、正しいRFCを参照できてるかあまり自信がありません...
DNSは本当に難しいですね。

ただTXTレコードを登録する実運用においては本記事の内容で特段困ることはないと思います。
この記事が誰かの役に立てば幸いです。

脚注

  1. 最初の文字列長オクテット含めて256文字
  2. 実際に試したところ、二重引用符、区切り文字を含めて4000文字まででした。テキストだけだと最長3968文字になります。