dns-seederを読む
dns-seederとは
bitcoinのnodeを立てた時に、他のnodeのipを探すために利用するもの
dnsとして問い合わせがあると、他に起動しているactiveなnodeのIP一覧を返してくれる
リポジトリ
ソースの構造
main.cpp - メインDNSやCrawler, Seed問い合わせのthreadを起動する。
bitcoin.cpp - Bitcoinのp2p protocolの定義とやり取りをする部分を実装している。有効なノードかの確認にverack, 生存確認にping, 他のnodeを探すためにgetaddrを使っている
dns.c - DNS機能。問い合わせに対して、AレコードとかNSレコードとかをdynamicに応答返している。
db.cpp - データベース。見つけたnodeのIPとstatusとかを管理してる。DNS問い合わせがきた時はここからavailableなnodeのIP一覧を返す。
protocol.cpp - Bitcoin protocolのベースの処理を実装してるぽい。
netbase.cpp - まだ調べてない
seeder起動時に生成されるthread
Crawler用のThread
DNSの待ち受けと応答用のThread.
Dumper用。起動するとdnsseed.dumpというファイルが作られるのでそいつに対してデータを吐くもの。dumpのデータ内容はよくわからない。
ConsoleにStatusを表示するためのThread
他のSeederに問い合わせに行くためのThread. Seeder同士も情報やり取りしてる。
Threadを起動している箇所。DNSが4, Crawlerが96, Dump、Status, Seederがそれぞれ1ずつ。DNSとCrawlerのThread数は起動オプションから変更可能。
getaddrが発行されるまでの流れ
bitcoin.cppのline:88, GotVersion()でvaddrがnullではなかったらgetaddr messageが送信される。
bitcoin.cppのGotVersion()はbitcoin.cppのline:99, bool ProcessMessage(string strCommand, CDataStream& vRecv)の中で呼ばれる。条件としては、受け取ったmessageがversionで、その時渡されたparamsのうちnVersion < 209だった時、もしくは、verackメッセージを受け取った時。
bitcoin.cppのline:76, PushVersion()を呼ぶのはline:215, Run()の中、line:218。対象nodeとのconnectionが確率されたら呼ばれる。
bitcoin.cppのRun()が呼ばれるのは、line:279, bool TestNode(const CService &cip, int &ban, int &clientV, std::string &clientSV, int &blocks, vector<CAddress>* vAddr)のコンストラクタ中。
bitcoin.cppのTestNode(~~~)コンストラクタが呼ばれるのはmain.cppのline:166, extern "C" void* ThreadCrawler(void* data)の中のline:188。line:171のdb.GetMany(ips, 16, wait);で取得したipsに対してfor文でTestNodeが作られてbitcoin p2p messageが送られる。その際にgetaddrを発行するかどうかの条件はline:187のbool getaddr = res.ourLastSuccess + 5000 < now;で判定される。
db.GetManyはdb.cppのline:33, bool CAddrDb::Get_(CServiceResult &ip, int &wait) が本体。
nodeのstatus更新処理
known node(接続済みnode)に対して、再度p2p messageが発行する条件。
db.cppのline:50, if (time(NULL) - idToInfo[ret].ourLastTry < MIN_RETRY) return false;が判定条件。
MIN_RETRYはdb.hのline:13に次の様に定義されている。#define MIN_RETRY 1000
つまり、前に接続してから1000秒後に再度p2p messageが投げられるぽい。
idToInfoについて
db.hのline:206に定義がある。std::map<int, CAddrInfo> idToInfo; // map address id to address info (b,c,d,e)
多分peerIdからCAddrInfoを取り出すためのマップかな。
次の行には逆引き用のマップも定義されてた。std::map<CService, int> ipToId; // map ip to id (b,c,d,e)
CAddrInfo.ourLastTryについて
ということで、ourLastTryはCAddrInfoのfieldなので次はこれがどこで更新されているかについて調べてみる。
結論
CAddrDb::Add_で追加されたnodeは一旦unkId(多分unknownリスト的なやつ)に入れられる。
CAddrDb::Get_でunkIdとourId(これは一度訪問済みのnodeリストぽい)から適当にnodeのリストを返す。(ここどのid調べるかランダムになってるぽいけど、unkIdから優先的に調べていけばいい気もする。。。。 → 多分これは無意味なaddrを返しまくる攻撃への緩和のためかも。)
ourIdのnodeが選択された場合、time(NULL) - idToInfo[ret].ourLastTry < MIN_RETRYの判定がされる。なので、最後に調べてからMIN_RETRY = 1000秒以上たってないと候補にならない。
CAddrDb::Good_が呼ばれると、ourIdに入れられて、ourLastTryがnowに更新される。
CAddrDB::Bad_が呼ばれるとbanされてリストから消える。特にblacklist入りとかはしないので、CAddrDb::Add_で再び追加されれば再度有効なnodeかチェックされる。
Add_ -> unkId -> Good_ -> ourId -> (MIN_RETRY後) -> Good_ -> ....という流れみたい。
調査内容
db.cppのline:6, void CAddrInfo::Update(bool good)の中で初期化されているけどなぜか無意味に二回初期化してる。。。。line:9, ourLastTry = now - MIN_RETRY;とline:12, ourLastTry = now;。なぞ。。。line:12だけでいいやん。
db.cppのline:33, bool CAddrDb::Get_(CServiceResult &ip, int &wait)の中の、line:56, idToInfo[ret].ourLastTry = now;でもnowで初期化しているが、条件がline:54, if (idToInfo[ret].ignoreTill && idToInfo[ret].ignoreTill < now)となっており、ignoreTillの意味が不明。一旦無視しておく。
db.cppのline: 131, void CAddrDb::Add_(const CAddress &addr, bool force)の中でも初期化されている。その箇所はline:159, ai.ourLastTry = 0;で0にされている。そのあと、line::166, unkId.insert(id);となっているので、Add_で追加されるnode情報は最初はunkIdに記録される。
CAddrInfo::Updateは、db.cppのvoid CAddrDb::Good_(const CService &addr, int clientV, std::string clientSV, int blocks)の中のline:82, info.Update(true);とvoid CAddrDb::Bad_(const CService &addr, int ban)の中のline:97, info.Update(false);で呼ばれている。Bad_の処理を読む限りだとGood->Badになった場合はもう一回チェックされるが、known -> BadとBad -> Badの場合はbanされる模様。
Crawl対象の収集方法
1. ハードコードされた他のseederに対して問い合わせする。
2. 他のseederから帰ってきたIPに対してbitcoin protocolで問い合わせする。
1. verackなげて接続できるか確認する。
2. 接続に成功したらgetaddrを投げて、接続したnodeの知ってる他のノードを教えてもらう。
3. getaddrの結果をdbに保存する。
3. 一定時間経過後に、dbに保存していてまだ問い合わせしていないnodeに対して問い合わせする。この処理は2.と同じ。
という流れなので、seederを用いてnetworkを構築するためには常に稼働し続けるマスターノード的なものと、そのマスターノードのIP Listを返すマスターseeder的なものが必要
seederが返すip listのfilter条件
dbに保存されているipにはいくつかstatusが設定されている。
nslookup で問い合わせした時にはdbに記録されたipを全て無条件で返すわけではなく、filterされたものが返ってくる。
このfilterの条件を調査する。
CAddrDb::GetIPs_の処理
ipの一覧はこのメソッドを使って取得している。
場所はdb.cpp: line173
goodIdがあればその中から、requestedFlagsで絞り込んだipを返す。
goodIdが0の場合は、ourIdもしくはunkId(andではなくor)の中から同じくrequestedFlagsで絞り込んだipを返す。
goodIdはそのnodeに問い合わせて条件を満たしたnodeのリスト
unkIdはunknownなもの。他のseederやaddr messageで収集したipだけど、まだそのnodeに問い合わせしていないもの。
ourIdはnodeに接続確認中なもの。
seederが起動してすぐの時は、unkIdやourIdが存在するが、最終的にはgoodIdのみが返されるようになる。
nodeがGoodと判定される条件
db.h: line103あたりにあるbool IsGood() const {で判定している。
PortがdefaultPortであること。(defaultじゃなくてもいいと思うけど。。。)
(!(services & NODE_NETWORK)) return false;(これは何のチェックなのかわからない。。。)
nodeがglobal ipであること。(より正確にはroutable。つまり到達可能なIPであること)
nodeのclientVersionが70001より大きいこと。
nodeのblock heightが35000 or 50000より大きいこと。
testが3回以下の場合は、2/3のテストがsuccessしていること
あとは2H, 8H〜1Monthまでの間で信頼性(多分success率)がある一定以上であること。
good ipのうち半分しか返さない
db.cpp: line 203あたりに以下のコードがあって、収集したipのうち半分しか返さないようになっている。
code: half
if (max > goodIdFiltered.size() / 2)
max = goodIdFiltered.size() / 2;
何で半分しか返さないのかは謎。
ちなみに、maxの初期値は1000なので、最大500個返す感じになる。
収集しているipが2個しかない場合は常に1個しか返さない。
CAddrDb::GetIPs_で行なっているfilterling処理の予想