linuxカーネルのpid管理(取得周り)
カーネルのpid管理と取得周りを調べてみた
PID名前空間
Linuxではpid_namespace(詳しくはman 7参照)に対応しているため、 1つのプロセスに対して複数のプロセスIDを複数持てるようになっている。
pid_namespaceは階層構造になっていて、 子のプロセスIDは同一プロセスでもそれぞれの名前空間で別のプロセスidとなる
親で作成されたのプロセスIDは子からは見えないようになっている
したがってプロセスIDは作成された階層の深度分プロセスIDを持つようになっている
カーネルでの表現
カーネル内部ではPIDはpidとupidという構造で表される
enum pid_type { PIDTYPE_PID, PIDTYPE_PGID, PIDTYPE_SID, PIDTYPE_MAX }; struct upid { int nr; // 実際のID struct pid_namespace *ns; // 属しているネームスペース struct hlist_node pid_chain; }; struct pid { atomic_t count; unsigned int level; // このプロセスが作成された時のname_spaceの深度 struct hlist_head tasks[PIDTYPE_MAX]; // pid構造体のプロセス、プロセスグループ、セッションに属するタスク struct rcu_head rcu; struct upid numbers[1]; // 可変長でlevel分だけupidを持つ };
pid構造体のIDはネームスペース毎にネームスペース毎に持つため、所属している深度分のupidを持つようになっていて、upidに実際の番号を持っている。
pid->tasksメンバ
tasksメンバはそのpidのプロセス、プロセスグループ、セッションに属するtask_structの一覧を管理する
pid構造体に属するPIDTYPE_PIDに結びつくプロセスは1つしかないので、 1つまでしか入らないことが保証されている。
IDからpidへの変換処理
IDからpidへの変換処理はよく行われるので、 pid_hashというIDとネームスペースのポインタをキーにしたハッシュを使い処理を高速化している
関数としてはネームスペースを渡すバージョンと、現在実行中のタスクのネームスペースを使うものの二種類がある。
extern struct pid *find_pid_ns(int nr, struct pid_namespace *ns); extern struct pid *find_vpid(int nr);
pidからidへの変換処理
ネームスペースのlevelからupidを探し出してnrを返す関数
こちらもネームスペースを渡すバージョンと、現在実行中のタスクのネームスペースを使うものの二種類がある。
pid_t pid_nr_ns(struct pid *pid, struct pid_namespace *ns); pid_t pid_vnr(struct pid *pid);
現在実行中のプロセスのネームスペース取得方法
// 呼び出し元 task_active_pid_ns(current->group_leader); // currentはスレッドに対応するのでプロセスのtask_structで呼び出し struct pid_namespace *task_active_pid_ns(struct task_struct *tsk) { return ns_of_pid(task_pid(tsk)); // task_pidでpid構造体を取得してpid構造体からネームスペースを取得する } static inline struct pid *task_pid(struct task_struct *task) { return task->pids[PIDTYPE_PID].pid; // カレントプロセスのPID構造体を取得 } static inline struct pid_namespace *ns_of_pid(struct pid *pid) { struct pid_namespace *ns = NULL; if (pid) ns = pid->numbers[pid->level].ns; // カレントプロセスの最大深度 = カレントプロセスが作成された名前空間となる。 return ns; }
LinuxとFreeBSDの共有ライブラリについて
共有ライブラリについて
Linuxでは共有ライブラリとして.soファイルが使用される。 .soは実行時にリンクする以外でも、実行中にdlopenを使用して読み込むことができる。
実行中の共有ライブラリ読み込みの仕組み
共有ライブラリは.textセクションを共有する為、 静的ライブラリのように外部ライブラリのアドレスを relセクションを作ってリンク時に書き換えということができない。
.textセクションの関数呼び出し先を共有しない書き込み可能な.got(global offset table)という領域に分けて、 そのポインタを参照することにより対処している。
共有ライブラリの遅延ロード
dlopenにはRTLD_LEZYというフラグがある。 RTLD_LEZYは関数の実行時まで参照先のシンボルを解決しないというフラグだが、 それを実現するために.pltという領域を使用している。
.pltには
0x00007fffeb62f568 <+0>: jmpq *0x2022ea(%rip) # 0x7fffeb831858 0x00007fffeb62f56e <+6>: pushq $0x22 0x00007fffeb62f573 <+11>: jmpq 0x7fffeb62f338
のような関数ごとにjmpを2回するようなコードがある。 0x7fffeb831858はGOTのアドレスを指していて、この場合一度も実行していないと0x00007fffeb62f56e(pushdのところ)のアドレスが入っている。
したがって1回目の実行では最初のjmpは何もしないで次の行に進みシンボルをを解決したい関数のID(0x22)を入れて、シンボル解決するコードにジャンプする。 シンボル解決コードでは0x7fffeb831858の中を本来jmpしたいアドレスに書き換えてから、本来jmpしたいアドレスにjmpする。 2回目の呼び出しでは、0x7fffeb831858のGOTが書き換えられているので本来呼びたい関数に直接ジャンプできるという仕組みになっている
LinuxとFreeBSDの挙動の違い
一度解決したリンク先がdlcloseによって参照先がなくなった場合の挙動にlinuxとBSDでは微妙に違うところがある。
first.so
#include <stdio.h> void do_action() { printf("action for first_test\n"); }
second.so
#include <stdio.h> void do_action() { printf("action for second_test\n"); }
test.so
void do_test() { do_action(); }
main.c
void main(){ void (*do_test)(); void *first = dlopen("./libfirst.so", RTLD_LAZY | RTLD_GLOBAL); void *h = dlopen("./libtest.so", RTLD_LAZY | RTLD_LOCAL); *(void **)(&do_test) = dlsym(h, "do_test"); (*do_test)(); dlclose(first); void *second = dlopen("./libsecond.so", RTLD_LAZY | RTLD_GLOBAL); (*do_test)(); }
main => do_test => do_actionの順番に呼ばれているプログラムである。
上記実行するとFreeBSDではセグフォで落ちる どうなっているのかというとdo_testを読んだ時点で、do_actionの.gotがfirst.soのシンボルとして解決され、 その後, first.soをdlcloseにてプロセスから削除している。
次に呼ばれたdo_testでは削除されたアドレスにシンボル解決されたdo_actionを呼ぶので落ちるのである。
Linuxの出力は
action for first_test action for first_test
とfirstが二回出力される。
Linuxではシンボル解決された段階で参照先(first.so)の参照カウントが上がるようで、 dl_closeしても、プロセス空間から削除されないようだ。
まとめ
dl_openで読み込む対象を動的に切り替えるのはうまくいかない。 Linuxではエラーにはならないが最初に読み込まれたもののみが実行される。 FreeBSDではセグフォで落ちる
linuxカーネルのIPフラグメント処理(1)
IPフラグメント処理
第一フラグメント便乗攻撃を調べるためlinuxのIPフラグメント処理を調べてみた。
長くなりそうなのでとりあえずinet_fragmentのメモ
ソースコード
inet_fragmentはプロトコルに依存しない処理を実装して、 ip_fragmentはipプロトコルに関わる処理を実装している
inet_fragment
ネットワーク層のプロトコルに依存しないフラグメント処理をしている。 主にやっていることは下記の3つ
- フラグメント可されたパケットの管理
- タイムアウト処理
- リミット処理
procによるパラメータの設定
ipv4の場合、タイムアウト処理とリミット処理の閾値は下記proc経由で設定可能
- /proc/sys/net/ipv4/ipfrag_time (フラグメント可されたパケットを最初に受信してから有効な秒数)
- /proc/sys/net/ipv4/ipfrag_high_thresh (メモリ使用量がこの値を超えるとフラグメント可されたパケットを受信できなくなる)
- /proc/sys/net/ipv4/ipfrag_low_thresh (メモリ使用量がこの値を超えると値を下回るまで削除が始まる)
カーネル内部でパラメータの格納はnetns_fragsで行われる
struct netns_frags { struct percpu_counter mem; // 現在使用しているメモリ量 int timeout; // フラグメントのタイムアウト int high_thresh; // 削除を開始するメモリ上限 int low_thresh; // 処理を中止するメモリ上限 }
この値はネームスペース毎に保持してあり、プロセスからアクセスするには
task_struct->nsproxy->net_ns->ipv4->frags
となる
フラグメント可されたパケットの管理
フラグメント可されたパケットはinet_fragsのハッシュで管理されており、値はinet_frag_queueとなる プロトコルに関わるところは抽象化されている 主なパラメータは下記の通り
struct inet_frag_queue { struct timer_list timer; // タイムアウトの設定 struct hlist_node list; // ハッシュリスト struct sk_buff *fragments; // 同一パケット内のフラグメント可されたパケットのリスト struct sk_buff *fragments_tail; // fragmentsの最後の要素 int len; // 現在格納されているパケットの最終位置 int meat; // 現在格納されてるパケットのバイト数 (最後のパケットが格納されていてlen == meatになると全て格納されたことになる) __u8 flags; // 下で説明 struct netns_frags *net; // 対象名前空間の総計と制限値へのポインタ }; struct inet_frags { struct inet_frag_bucket hash[INETFRAGS_HASHSZ]; unsigned int (*hashfn)(const struct inet_frag_queue *); // プロトコル毎のハッシュ値の計算 (必須) bool (*match)(const struct inet_frag_queue *q, const void *arg); // プロトコル毎の値比較(必須) void (*destructor)(struct inet_frag_queue *); // プロトコル固有の情報削除用フック(任意) void (*skb_free)(struct sk_buff *); // sk_buff削除時のフック(任意) void (*frag_expire)(unsigned long data); // タイムアウトまたはメモリ上限が超えて削除される時のフック(必須) struct kmem_cache *frags_cachep; // inet_frag_initで設定される名前がfrags_cache_nameでサイズがqsizeのスラブキャッシュ const char *frags_cache_name; // スラブキャッシュに使用される名前(必須) int qsize; // プロトコル固有データとinet_frag_queueデータの合計サイズ(必須) }
構造体自体は使用する側で確保して、必須となっているパラメータの設定を行い、
inet_frag_init
を呼ぶことにより使用可能となる。 inet_frag_init内ではhashの初期化とfrag_cache_name、qsizeからスラブハッシュを構築する
タイムアウト処理
IPフラグメントは各セグメント毎にタイムアウトが設定されるため、 inet_frag_queueにタイマーを持っている。
作成と同時にタイマーが設定され、 タイムアウト時の呼び出しはinet_fragsに設定されたfrag_expire関数が呼ばれる
inet_frag_create inet_frag_alloc setup_timer(&q->timer, f->frag_expire, (unsigned long)q); inet_flag_intern mod_timer(&qp->timer, jiffies + nf->timeout)
リミット処理
制限値を超えた場合は、カーネルのワークキューを使用してメモリの解放処理が行われている。
トリガーとなるのはソフトリミットを超えた場合で、ハードリミットを超えると処理を中断する。 ワーカースレッドが実行する関数は
inet_frag_worker
で、ソフトリミットを超えなくなるまでハッシュの要素を削除する。
expireが呼ばれる時のタイムアウトとリミット処理判断
expireはタイムアウトとリミット処理の両方で、リミットを超えて削除される場合は
INET_FRAG_EVICTED
フラグが設定される。 呼び出し元で判断を行いたい場合はこのフラグを参照する。
参考にさててもらったサイト
ドメインレートリミット(unbound)
ドメインレートリミットとは
/services/cache/infra.cに実装されている
例えば 以下のようなレコードが返ってくる権威サーバーがあったとする
QD: www.hoge.co.jp. IN A NS: hoge.co.jp. IN NS ns1.hoge.co.jp. hoge.co.jp. IN NS ns2.hoge.co.jp.
このような場合に次にns1.hoge.co.jpまたはns2.hoge.co.jpに問い合わせを行うが、 この問い合わせ数を制限するものである。
水攻め攻撃が行われ
a.hoge.co.jp b.hoge.co.jp c.hoge.co.jp d.hoge.co.jp
のように名前を変えたアクサスをされると、毎回キャッシュに存在しない問い合わせになるので アクセスがあるたびにhoge.co.jpに問い合わせを行ってしまう。
これを防ぐために攻撃を受けているドメインの権威サーバへのアクセスを N秒以内にM回までに制限する。
Nはunboundでは固定で2秒となっている
設定項目
設定項目は下記の6つ
ratelimit: num
ratelimitサイズで、これが0の場合はドメインratelimit無効になる。
ratelimit-size: size
Hashのキーのサイズ(バイト指定)
ratelimit-slabs: num
Hashのスラブサイズ。マルチスレッドの時のアクセス効率を良くするため、ハッシュ自体を何個に分けるかを指定。
ratelimit-factor: num
ratelimitを超えた場合にどうするかを決める。 0だと必ずFAILを返すようになるここに数値を設定するとratelimitを超えた場合には1/数値だけ 成功するようになる。
ratelimit-for-domain: domain num
指定されたドメインのratelimit値を別な値に置き換える 沢山の子を持つTLDドメインや攻撃を受けているドメインのrate-limitを特別扱いしたい場合等に使える
ratelimit-below-domain: domain num
指定されたドメインのサブドメインすべてのratelimit値を別な値に置き換える 指定されたドメイン自体のrate-limitは置き換わらない。
テスト
テストがしやすいようにconfの項目の値を下記のように設定しておく。
log-queries: yes ratelimit: 3 do-ip4: yes do-ip6: no verbosity: 2 harden-referral-path: no
プライミングとncad.co.jpのキャッシュを終わらせるために一度www.ncad.co.jpに問い合わせてから次のようなスクリプトを実行する
dig @127.0.0.1 a.ncad.co.jp & dig @127.0.0.1 b.ncad.co.jp & dig @127.0.0.1 c.ncad.co.jp & dig @127.0.0.1 d.ncad.co.jp & dig @127.0.0.1 e.ncad.co.jp &
実行結果
May 16 08:53:03 unbound[11879:1] info: 127.0.0.1 b.ncad.co.jp. A IN May 16 08:53:03 unbound[11879:1] info: resolving b.ncad.co.jp. A IN May 16 08:53:03 unbound[11879:1] info: 127.0.0.1 a.ncad.co.jp. A IN May 16 08:53:03 unbound[11879:1] info: resolving a.ncad.co.jp. A IN May 16 08:53:03 unbound[11879:1] info: 127.0.0.1 c.ncad.co.jp. A IN May 16 08:53:03 unbound[11879:1] info: resolving c.ncad.co.jp. A IN May 16 08:53:03 unbound[11879:1] notice: ratelimit exceeded ncad.co.jp. 3 May 16 08:53:03 unbound[11879:1] info: response for b.ncad.co.jp. A IN May 16 08:53:03 unbound[11879:1] info: reply from <ncad.co.jp.> 182.171.76.42#53 May 16 08:53:03 unbound[11879:1] info: query response was NXDOMAIN ANSWER May 16 08:53:03 unbound[11879:1] info: 127.0.0.1 d.ncad.co.jp. A IN May 16 08:53:03 unbound[11879:1] info: resolving d.ncad.co.jp. A IN May 16 08:53:03 unbound[11879:1] info: 127.0.0.1 e.ncad.co.jp. A IN May 16 08:53:03 unbound[11879:1] info: resolving e.ncad.co.jp. A IN May 16 08:53:03 unbound[11879:1] info: response for e.ncad.co.jp. A IN May 16 08:53:03 unbound[11879:1] info: reply from <ncad.co.jp.> 182.171.76.42#53 May 16 08:53:03 unbound[11879:1] info: query response was NXDOMAIN ANSWER
3件でratelimitがかかる為、query responseの行が2行しか無いのがわかる。
ratelimitは最初に超えた時しかログが出ないのでragtelimit exceededの行は1行のみである。
内部構造
キャッシュを管理しているデータ
// infra.h struct infra_cache { ... struct slabhash* domain_rate; // keyはrate_key dataはrate_data 対象ドメインに秒間何回のアクセスがあったかを格納する rbtree_type domain_limits; // aclと同じくdomainのparentを辿れる構造。for-domainとbelow-domainの情報を格納 ... }; struct rate_key { struct lruhash_entry entry; uint8_t *name; // ドメイン名のバイナリ表現(圧縮無し) size_t name_len; // nameのlen }; struct rate_data { int qps[RATE_WINDOW]; // 対象の秒数に何回アクセスがあったかをカウントする time_t timestamp[RATE_WINDOW]; // qpsに対応する時刻(秒単位) }; // infra.c int infra_dp_ratelimit // configで指定されたratelimitの値を格納 0 だと無効
関数
// infra.c // 指定された現在時間からRATE_WINDOW以内のアクセスがratelimitを超えていないか調べる int infra_ratelimit_exceeded(struct infra_cache* infra, uint8_t* name, size_t namelen, time_t timenow) // 指定されたドメインに対するratelimitをインクリメントする インクリメントされた結果ratelimitを超えているかを返す int infra_ratelimit_inc(struct infra_cache* infra, uint8_t* name, size_t namelen, time_t timenow)
Unboundのtarget_fetch_policy
target-fetch-policyについて
マニュアルを見ても「ターゲット アドレスを日和見的に取ってくる」 となってよくわからなかったのでソースから調べてみた。
結果
設定値
デフォルトは”3 2 1 0 0" 数字の並びは左から深度に対応している 深度は委任レコードを返された時に外部名だった場合外部名をたどる回数となる 外部名探索中にさらに外部名が見つかった場合には深度2となり左から2番目の数値が参照される。
数字はいくつの外部名を同時に探すかに対応する。 Unboundでは複数の外部名があった場合同時に問い合わせを行い、レスポンスが速いものが使用される。
0と1の違い
外部名しか存在しない場合は0と1に違いはない 外部名と内部名が混在していた場合、0では内部名のアドレスに対して問い合わせるのと同時に 外部名に対しても1つ問い合わせを行う
-1
-1が指定されていた場合にはすべての外部名の探索が行われる
深度より多い問い合わせ
深度 -1以上の問い合わせは行わないようになっている 例えば”3 1”と指定された場合始めの外部名では3つに対して問い合わせを行うが さらに外部名に出会った時には処理を終了する。
何故-1なのかはわからなかった。
ソースコード
コンフィグから値の設定
iter_init iter_apply_cfg read_fetch_policy
max_dependency_depthとtarget_fetch_policyに使用している max_dependency_depthには設定した個数-1が入る
使用場所
processQueryTargets query_for_targets
processQueryTargetsの中はだいたい以下のようになっている
// 現在の深度の問い合わせ個数取得 tf_policy = ie->target_fetch_policy[iq->depth]; if(iq->caps_fallback) { // 0x20エンコードの時は強制的に全ての外部名をたどる query_for_targets(qstate, iq, ie, id, -1, &extra) .... } else if(tf_policy != 0) { // それ以外の場合には問い合わせ個数分取得 query_for_targets(qstate, iq, ie, id, tf_policy, &extra); .... } // すでにアドレスが分かっている問い合わせ先を取得(内部名やキャッシュされている場合ここで見つけられる) target = iter_server_selection(...) if ( !target ) { // 問い合わせ先がなければ一つだけ問い合わせる query_for_targets(qstate, iq, ie, id, 1, &extra); }
実験
unboundに付属しているtestboundで試すことができる
server: target-fetch-policy: "2 0" do-ip6: no stub-zone: name: "." stub-addr: 0.0.0.1 CONFIG_END SCENARIO_BEGIN my test RANGE_BEGIN 0 100 ADDRESS 0.0.0.1 ENTRY_BEGIN MATCH opcode qtype qname ADJUST copy_id REPLY QR NOERROR SECTION QUESTION . IN NS SECTION ANSWER . IN NS A.ROOTSERVERS.NET. SECTION ADDITIONAL A.ROOTSERVERS.NET. IN A 0.0.0.1 ENTRY_END ENTRY_BEGIN MATCH opcode qtype qname ADJUST copy_id REPLY QR NOERROR SECTION QUESTION test.jp. IN A SECTION AUTHORITY jp. IN NS b.jp jp. IN NS a.jp jp. IN NS c.jp jp. IN NS d.jp SECTION ADDITIONAL d.jp IN A 1.1.1.1 ENTRY_END RANGE_END STEP 1 QUERY ENTRY_BEGIN REPLY RD SECTION QUESTION test.jp. IN A ENTRY_END STEP 10 NOTHING SCENARIO_END
実行結果の一部
[0] unbound[43053:0] info: pending msg;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 0 ;; flags: cd ; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1 ;; QUESTION SECTION: b.jp. IN A ;; ANSWER SECTION: ;; AUTHORITY SECTION: ;; ADDITIONAL SECTION: ; EDNS: version: 0; flags: do ; udp: 4096 ;; MSG SIZE rcvd: 33 [0] unbound[43053:0] debug: pending to 1.1.1.1 port 53 [0] unbound[43053:0] info: pending msg;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 0 ;; flags: cd ; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1 ;; QUESTION SECTION: a.jp. IN A ;; ANSWER SECTION: ;; AUTHORITY SECTION: ;; ADDITIONAL SECTION: ; EDNS: version: 0; flags: do ; udp: 4096 ;; MSG SIZE rcvd: 33 [0] unbound[43053:0] debug: pending to 1.1.1.1 port 53 [0] unbound[43053:0] info: pending msg;; ->>HEADER<<- opcode: QUERY, rcode: NOERROR, id: 0 ;; flags: ; QUERY: 1, ANSWER: 0, AUTHORITY: 0, ADDITIONAL: 1 ;; QUESTION SECTION: test.jp. IN A ;; ANSWER SECTION: ;; AUTHORITY SECTION: ;; ADDITIONAL SECTION: ; EDNS: version: 0; flags: do ; udp: 4096 ;; MSG SIZE rcvd: 36
設定が深度1の設定が2なので2件の外部名問い合わせと内部名のアドレス(1.1.1.1)に問い合わせているのが確認できる。
Unboundのmsgキャッシュについて
msgキャッシュとは
ユーザからのリクエストをkeyとしてレスポンスを保存するためのハッシュ
keyがquery_infoでvalueがreply_infoとなる。 ハッシュ構造とキーをまとめて管理できるように msgreply_entryという構造体にまとめられている。
reply_infoとub_packed_rrset_key
reply_infoが参照しているub_packed_rrset_keyは rrsetキャッシュ内にあるものを指している為、 rrsetキャッシュの容量がいっぱいになった場合に削除される可能性がある。
削除されるとポインタの参照先が無効領域になってしまい、 セグフォになってしまう為、一度確保したub_packed_rrset_keyは削除せずに 領域を再利用する構造になっている(util/alloc.hを参照)
領域が解放されるとub_packed_rrset_keyとrrset_refのidが違うものになる為解放されたことがわかる。 idを確認する為にはub_packed_rrset_keyのロックを取得してからidの確認を行う。 ロックはハッシュで使用しているものと同じものを使用する。
unboundのCNAME処理
処理内容
権威からCNAMEが返ってきた場合、 キャッシュサーバーでは別名を引きに行かなくてはいけない。
仮に下記のような登録があった場合、キャッシュサーバーにて cname.jp.への問い合わせとanswer.jpへの問い合わせを行い、 それらのレコードを合成して結果を返す必要がある
[cname.jp] cname.jp. CNAME IN answer.jp.
[answer.jp] answer.jp. A IN 1.1.1.1
[クライアントに返す必要があるレスポンス] cname.jp. CNAME IN answer.jp. answer.jp. A IN 1.1.1.1
権威からのCNAME受信
process_response(権威サーバーからのメッセージをparse) processQueryResponse(レスポンス処理) iter_handle iter_dns_store(rrsetのみをキャッシュ) handle_name_response iter_add_prepend_answer(クライアントへのレスポンスで必要なのでストアしておく) iter_qstateのqchase入れ替えしてqstateを初期化 next_state(iq, INIT_REQUEST_STATE)
CNAMEで返ってきたrrsetを iter_qstateのan_prepend_listに追加しておき、 qchaseをCNAMEから取り出した内容で置き換えて (元の問い合わせはmodule_qstateのqinfoに入っている) 再度リクエスト処理を行う。
INIT_REQUEST_STATEからリクエストを開始するので、 キャッシュがあれば通常通りキャッシュが使用される。
クライアントへのレスポンス
iter_handle processFinishded iter_prepend(an_prepend_listにストアしていたものを取り出し、dns_msgを作り直す) iter_dns_store(msgとrrset両方キャッシュする)
CNAME先の問い合わせとan_prepend_listにストアしていたものを 合成してレスポンスを作成する
合成したものをキャッシュにストアしておく。
まとめ
CNAMEでのリクエストがあった場合には Aレコードをストアしておき問い合わせ名を変えて再度リクエスト。 クライアントに返せる状態になってからAレコードを取り出し合成する。 合成した情報をキャッシュに入れて次回のリクエストに備えている。
おまけ
CNAMEの無限ループを抑えるため、MAX_RESTART_COUNT以上CNAMEを辿らないようになっている MAX_RESTART_COUNTは8となっている。