読者です 読者をやめる 読者になる 読者になる

totofugaのブログ

ネットワークとかc言語、perlの話。

linuxカーネルのIPフラグメント処理(1)

IPフラグメント処理

第一フラグメント便乗攻撃を調べるためlinuxのIPフラグメント処理を調べてみた。

長くなりそうなのでとりあえずinet_fragmentのメモ

ソースコード

  • net/ipv4/ip_fragment
  • net/ipv4/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

フラグが設定される。 呼び出し元で判断を行いたい場合はこのフラグを参照する。

参考にさててもらったサイト

RFC 791

ドメインレートリミット(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キャッシュについて

f:id:totofuga:20170321232719j:plain

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となっている。

パケット遅延

tcpのport 53 synパケットのみを3秒遅延したい等 特定のパケットのみに遅延を適用させる方法。

条件設定にはtcを使って行う方法とiptablesのMARK経由で行う方法がある。 今回は遅延以外にもdropやreject(impパケットを返す)も別途行いたかったので 条件をまとめられるようiptablesを使用した。

TCの設定

tcで下記のようなキューを作成する

  • キュー1 条件: MARK1 動作: 3秒遅延
  • キュー2 条件: MARK2 動作: 5秒遅延
  • キュー3 条件: MARK3 動作: 10秒遅延
  • キュー4 条件: デフォルト 動作: 遅延しない

prioキュー設定

まずprioキューを作成する

tc qdisc add dev eth0 root handle 1: prio bands 4 priomap 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3
bands

振り分けを行うクラスの数で今回は4個のクラスを作成する。

priomap

ipヘッダのTOSを見てどのクラスに振り分けるかの指定する設定だが、 今回パケットはTOSに関係なく全て4に送りたいので3を指定する。 (priomapのインデックスは0スタートなので3はクラス4の意味になる)

遅延用のキューを作成

prioキューの設定を行うと1:1 1:2 1:3 1:4の4つのクラスが出来てデフォルトでこれらは、 fifo qdiscと繋がっている。1:1 1:2 1:3の3つには遅延を行いたいのでnetemキューに繋ぎ直す

tc qdisc add dev eth0 parent 1:1 handle 10: netem delay 3s  
tc qdisc add dev eth0 parent 1:2 handle 20: netem delay 5s  
tc qdisc add dev eth0 parent 1:3 handle 30: netem delay 10s  
handle

netemで作成したクラス名を指定。 下記のように連結される

  • root=>1: => 1:1 => 10:
  • root=>1: => 1:2 => 20:
  • root=>1: => 1:3 => 30:
  • root=>1: => 1:4 => デフォルトのfifo disc
delay

遅延させる秒数で3秒、5秒、10秒を選択

遅延用のキューに振り分けるフィルターの作成

遅延用のキューが作成できたのでそれらに振り分けるための条件を作成する

tc filter add dev eth0 parent 1: prio 1 handle 1 fw flowid 1:1
tc filter add dev eth0 parent 1: prio 1 handle 2 fw flowid 1:2
tc filter add dev eth0 parent 1: prio 1 handle 3 fw flowid 1:3
prio

優先度の設定でフィルターは優先度の高いものを上から順に実行していくが、 今回はMARK指定のみで複数の条件が同時に当てはまることがないので全てに1を指定

handle

MARK番号の指定でMARK1, 2, 3を使用する (netemの時指定したhandleではクラス指定だったがこちらはMARK番号となることに注意)

iptablesの設定

TCの設定ができたのであとはiptablesで条件を指定してMARKをつけることにより簡単に遅延が完了

例えばDNSTCPフォールバックの3ウェイハンドシェイクのSYN,ACK応答のみ5秒遅延させたい場合は

iptables -A OUTPUT -p tcp --tcp-flags SYN,ACK SYN,ACK --sport 53 -j MARK --set-mark 2

テスト等で遅延させる秒数や条件が複数あり切り替えたい場合はユーザ定義チェインを作っておいて切り替えたり、 iptables-save&iptables-restoreでファイル経由にするほうがよい。

参考にさせていただいたサイト

論理ボリュームの縮小

縮小

アンマウント ↓ ファイルシステムの縮小 ↓ 論理ボリュームの縮小 ↓ マウント

という手順を取る必要がある

アンマウント

#lvdisplay
  --- Logical volume ---
  LV Name                /dev/VolGroup00/mylv
  VG Name                VolGroup00
  LV UUID                lK6lM3-lZ1X-0vtK-HaPd-xkMj-e403-zeTRHc
  LV Write Access        read/write
  LV Status              available
  # open                 1
  LV Size                2.91 GB
  Current LE             93
  Segments               1
  Allocation             inherit
  Read ahead sectors     auto
  - currently set to     256
  Block device           253:2

このボリュームを2GBに縮小する

umount

umount /mnt/test/

/mnt/testにマウントしていたので単純にumountでマウントを解除する

ファイルシステムの縮小

# resize2fs /dev/VolGroup00/mylv 2G
resize2fs 1.39 (29-May-2006)
Please run 'e2fsck -f /dev/VolGroup00/mylv' first.

resizeしようとするとe2fsckしてからにしてくれと言われるので まずチェックを行う

# e2fsck -f /dev/VolGroup00/mylv
# resize2fs /dev/VolGroup00/mylv 2G

ファイルシステム上は縮小されたので この状態でmountを行うと

/dev/mapper/VolGroup00-mylv
                      2.0G   69M  1.9G   4% /mnt/test

2Gになっているが

  Free  PE / Size       0 / 0 

ボリュームグループ上はサイズが増えていない

論理ボリュームの縮小

拡張を行うには lvextend 縮小を行うには lvreduce を使用する

# lvreduce -L 2G /dev/VolGroup00/mylv 
  WARNING: Reducing active logical volume to 2.00 GB
  THIS MAY DESTROY YOUR DATA (filesystem etc.)
Do you really want to reduce mylv? [y/n]: y
  Reducing logical volume mylv to 2.00 GB
  Logical volume mylv successfully resized

これでVolumeGroupの使用領域が解放されます。

# vgdisplay
Free  PE / Size       29 / 928.00 MB