totofugaのブログ

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

iptables勉強(2)

iptablesのtargetrの拡張

iptablesだいぶわかってきたので最小構成のtargetを書いてみた

構成ファイル

user空間用とkernel空間用の2種類のファイルを用意する必要がある

user空間用ファイル

iptablesコマンドを拡張するために /lib64/xtables/libに共有ライブラリを作成して配置する

名前はlibipt_<target名>.so

エントリポイントは_initという関数を作りxtables_targetを登録する

xtables.hはiptables-develパッケージに含まれている

#include <xtables.h>                                                                        
#include <stdio.h>                                                                          
                                                                                            
static void MYLOG_help() {                                                                  
    // printf("commandオプションがあればここに記述\n");  // iptables -j MYLOG --helpの時に表示される                                   
}                                                                                           
                                                                                            
static void MYLOG_print(const void *ip, const struct xt_entry_target *target, int numeric) {
    printf(" MYLOG");    // iptables -Lの時に表示される名前                                                                
}                                                                                           
                                                                                            
static struct xtables_target my_log_target = {                                              
    .name  = "MYLOG",                                                                       
    .version = XTABLES_VERSION,                                                             
    .family = NFPROTO_IPV4,                                                                 
    .help = MYLOG_help,                                                                     
    .print = MYLOG_print                                                                    
};                                                                                          
                                                                                            
void _init() {                                                                              
    xtables_register_target(&my_log_target);                                                
}                                                                                           

作成したら共有ライブラリとしてコンパイルして/lib64/xtables/libに配置

gcc -fpic -shared ./libipt_MYLOG.c -o libipt_MYLOG.so

kernel空間用ファイル

user空間用ファイルに対応するカーネルモジュールを作成する

今回はヒットしたらログを出すだけ。

ユーザ空間のファイルとの対応はxt_targetのnameで行なっている

#include <linux/module.h>                                                              
#include <linux/netfilter.h>                                                           
#include <linux/netfilter/x_tables.h>                                                  

MODULE_LICENSE("GPL");

static unsigned int log_mylog(struct sk_buff *skb, const struct xt_action_param *par) {
    printk(KERN_ALERT "run mylog\n");
    return XT_CONTINUE;                                                                
}                                                                                      
                                                                                       
static struct xt_target mylog_target __read_mostly = {                                 
    .name       = "MYLOG",                                                             
    .family     = NFPROTO_IPV4,                                                        
    .target     = log_mylog,                                                           
    .me     = THIS_MODULE,                                                             
};  

static int __init mylog_init(void) {                                                   
    return xt_register_target(&mylog_target);                                          
}   

static void __exit mylog_exit(void) {
    xt_unregister_target(&mylog_target);
}

module_init(mylog_init);
module_exit(mylog_exit);

作成したらカーネルモジュールをロード

insmod mylog.ko

iptables

user空間とkernel空間のファイルを作成すると、iptablesコマンドの-jオプションにて自分の作成したターゲットを指定できる

# iptables -A INPUT -j MYLOG
# iptables -L
Chain INPUT (policy ACCEPT)
target     prot opt source               destination         
MYLOG      all  --  anywhere             anywhere             MYLOG

Chain FORWARD (policy ACCEPT)
target     prot opt source               destination         

Chain OUTPUT (policy ACCEPT)
target     prot opt source               destination         

この状態でアクセスがあるとカーネルログにrun mylogが出力される

iptables勉強

ユーザ空間から IPTablesのチェインの取り出し。

とりあえずなんとなくとれるようになったので今日はここまで。

#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <string.h>
#include <net/if.h>
#include <getopt.h>
#include <stdlib.h>
#include <linux/netfilter_ipv4.h>
#include <linux/netfilter_ipv4/ip_tables.h>
#include <linux/netfilter/x_tables.h>

int show_ipt_entry(struct ipt_entry * ent, struct ipt_getinfo *info, struct ipt_get_entries *entries) {
    struct xt_entry_target *xt_target = ipt_get_target(ent);

    // user定義チェイン
    if ( strcmp(xt_target->u.user.name, "ERROR") == 0 ) {
        printf("user定義チェイン(%s)\n", (char*)xt_target->data);
        return 0;
    }

    // builtinチェイン
    int i;
    for ( i = 0; i < NF_IP_NUMHOOKS; ++i ) {
        if ( info->valid_hooks & (1 << i) ) { // 有効か確認
            if ( (struct ipt_entry*)((char*)entries->entrytable + info->hook_entry[i]) == ent ) {
                switch( i ) {
                    case NF_IP_PRE_ROUTING:
                        printf("PREROUTING\n");
                        break;
                    case NF_IP_POST_ROUTING:
                        printf("POSTROUTING\n");
                        break;
                    case NF_IP_LOCAL_IN:
                        printf("LOACLIN\n");
                        break;
                    case NF_IP_LOCAL_OUT:
                        printf("LOCALOUT\n");
                        break;
                    case NF_IP_FORWARD:
                        printf("FORWARD\n");
                        break;
                    default:
                        printf("unknown chain\n");
                        break;
                }
            }
        }
    }

    struct xt_standard_target *st = (struct xt_standard_target*)xt_target;
    printf("#### ipt_verdict %d ####\n", st->verdict);

    return 0;
}

void main(int argc, char *argv[]) {

    char table[256] = {};
    strcpy(table, "filter");
    int arg;
    while( (arg = getopt(argc, argv, "t:")) != -1 ) {
        switch(arg) {
            case 't':
                strcpy(table, optarg);
                break;
        }
    }

    int sock = socket(AF_INET, SOCK_RAW, IPPROTO_RAW);

    struct ipt_getinfo info;
    memset(&info, 0, sizeof(info));
    strcpy(info.name, table);


    int len = sizeof(info);
    if ( getsockopt(sock, IPPROTO_IP, IPT_SO_GET_INFO, &info, &len) == -1 ) {
        perror("IPT_SO_GET_INFO error\n");
        return;
    }

    printf("#### ipt_getinfo ####\n");
    printf("name : %s\n", info.name);
    printf("valid_hooks : %d\n", info.valid_hooks);
    printf("underflow: \n");
    printf("\tNF_INET_PRE_ROUTING : %d\n", info.underflow[NF_INET_PRE_ROUTING]);
    printf("\tNF_INET_LOCAL_IN: %d\n", info.underflow[NF_INET_LOCAL_IN]);
    printf("\tNF_INET_FORWARD: %d\n", info.underflow[NF_INET_FORWARD]);
    printf("\tNF_INET_LOCAL_OUT: %d\n", info.underflow[NF_INET_LOCAL_OUT]);
    printf("\tNF_INET_POST_ROUTING: %d\n", info.underflow[NF_INET_POST_ROUTING]);
    printf("num_entries: %d\n", info.num_entries);
    printf("size: %d\n", info.size);

    struct ipt_get_entries *entries;
    len = sizeof(*entries) + info.size;
    entries = calloc(1, len);
    strcpy(entries->name, info.name);
    entries->size = info.size;
    if ( getsockopt(sock, IPPROTO_IP, IPT_SO_GET_ENTRIES, entries, &len) == -1 ) {
        perror("IPT_SO_GET_ENTRIES error\n");
        return;
    }

    int i;
    for(i = 0; i < NF_INET_NUMHOOKS; ++i) {
        if ( info.underflow[i] == -1 ) {
            continue;
        }
        struct ipt_entry* ent = (struct ipt_entry*)((char*)entries->entrytable + info.underflow[i]);
        struct xt_entry_target *t = ipt_get_target(ent);
        if ( strcmp(t->u.user.name, "") != 0 ) {
            printf("error\n");
        }

        struct xt_standard_target *st = (struct xt_standard_target*)t;
    }

    IPT_ENTRY_ITERATE(entries->entrytable, entries->size, show_ipt_entry, &info, entries);

}

linux arpの内部データ

arpの統計情報が欲しかったのでnetlink経由で取得するものを作った NDTA_PARMSはrtattrが入れ子になっている。

#include <asm/types.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/neighbour.h>
#include <linux/rtnetlink.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <stdint.h>

static int seq;

void msg_receive(int sock) {
    char buf[4096]; 

    int n = read(sock, buf, sizeof(buf));
    if ( n <= 0 ) {
        perror("read error\n");
        exit(1);
    }
    struct nlmsghdr *nh = (struct nlmsghdr *)buf;
    // printf("read size: %d\n", n);
    if ( nh->nlmsg_seq != seq ) {
        printf("seq error\n");
        exit(1);
    }
    if ( nh->nlmsg_type == NLMSG_ERROR ) {
        printf("error\n");
        exit(1);
    } else if ( nh->nlmsg_type == RTM_NEWNEIGHTBL ) {
        NLMSG_ALIGN(nh->nlmsg_len);
        struct ndtmsg *ndtm = NLMSG_DATA(nh);
        if ( ndtm->ndtm_family != AF_INET ) {
            printf("ndgm_family is not AF_INET\n");
            exit(1);
        }

        struct rtattr *rta;
        int len = nh->nlmsg_len - NLMSG_LENGTH(sizeof(struct ndtmsg));
        for (rta = (struct rtattr*)((char *)ndtm + sizeof(struct ndtmsg)); RTA_OK(rta, len); rta = RTA_NEXT(rta, len)) {
            switch(rta->rta_type) {
                case NDTA_NAME:
                    printf("NAME: %s\n", (char*)RTA_DATA(rta));
                    break;
                case NDTA_THRESH1:
                    printf("THRESH1 %d\n", *(uint32_t*)RTA_DATA(rta));
                    break;
                case NDTA_THRESH2:
                    printf("THRESH2 %d\n", *(uint32_t*)RTA_DATA(rta));
                    break;
                case NDTA_THRESH3:
                    printf("THRESH3 %d\n", *(uint32_t*)RTA_DATA(rta));
                    break;
                case NDTA_CONFIG:
                     {
                        struct ndt_config *config = (struct ndt_config*)RTA_DATA(rta);
                        printf("CONFIG:\n");
                        printf("\tkey_len: %d\n", config->ndtc_key_len);
                        printf("\tentry_size: %d\n", config->ndtc_entry_size);
                        printf("\tentries: %d\n", config->ndtc_entries);
                        printf("\tlast_flush: %d\n", config->ndtc_last_flush);
                        printf("\tlast_rand: %d\n", config->ndtc_last_rand);
                        printf("\thash_rnd: %d\n", config->ndtc_hash_rnd);
                        printf("\thash_mask: %d\n", config->ndtc_hash_mask);
                        printf("\thash_chain_gc: %d\n", config->ndtc_hash_chain_gc);
                        printf("\tproxy_qlen: %d\n", config->ndtc_proxy_qlen);
                    }
                    break;
                case NDTA_PARMS:
                    {
                        printf("PARAMS:\n");
                        struct rtattr *parm_rta;
                        int parm_len = rta->rta_len;
                        for (parm_rta = rta + 1; RTA_OK(parm_rta, parm_len); parm_rta = RTA_NEXT(parm_rta, parm_len)) {
                            switch(parm_rta->rta_type) {
                                case NDTPA_IFINDEX:
                                    printf("\tifindex: %d\n", *(uint32_t*)RTA_DATA(parm_rta));
                                    break;
                                case NDTPA_REFCNT:
                                    printf("\trefcnt: %d\n", *(uint32_t*)RTA_DATA(parm_rta));
                                    break;
                                case NDTPA_REACHABLE_TIME:
                                    printf("\treachable_time: %lld\n", *(uint64_t*)RTA_DATA(parm_rta));
                                    break;
                                case NDTPA_BASE_REACHABLE_TIME:
                                    printf("\tbase_reachable_time: %lld\n", *(uint64_t*)RTA_DATA(parm_rta));
                                    break;
                                case NDTPA_RETRANS_TIME:
                                    printf("\tretrans_time: %lld\n", *(uint64_t*)RTA_DATA(parm_rta));
                                    break;
                                case NDTPA_GC_STALETIME:
                                    printf("\tgc_staletime: %lld\n", *(uint64_t*)RTA_DATA(parm_rta));
                                    break;
                                case NDTPA_DELAY_PROBE_TIME:
                                    printf("\tdelay_probe_time: %lld\n", *(uint64_t*)RTA_DATA(parm_rta));
                                    break;
                                case NDTPA_QUEUE_LEN:
                                    printf("\tqueue_len: %d\n", *(uint32_t*)RTA_DATA(parm_rta));
                                    break;
                                case NDTPA_APP_PROBES:
                                    printf("\tapp_probes: %d\n", *(uint32_t*)RTA_DATA(parm_rta));
                                    break;
                                case NDTPA_UCAST_PROBES:
                                    printf("\tucast_probes: %d\n", *(uint32_t*)RTA_DATA(parm_rta));
                                    break;
                                case NDTPA_MCAST_PROBES:
                                    printf("\tmcast_probes: %d\n", *(uint32_t*)RTA_DATA(parm_rta));
                                    break;
                                case NDTPA_ANYCAST_DELAY:
                                    printf("\tanycast_delay: %lld\n", *(uint64_t*)RTA_DATA(parm_rta));
                                    break;
                                case NDTPA_PROXY_DELAY:
                                    printf("\tproxy_delay: %d\n", *(uint64_t*)RTA_DATA(parm_rta));
                                    break;
                                case NDTPA_PROXY_QLEN:
                                    printf("\tproxy_qlen: %d\n", *(uint32_t*)RTA_DATA(parm_rta));
                                    break;
                                case NDTPA_LOCKTIME:
                                    printf("\tlocktime: %d\n", *(uint64_t*)RTA_DATA(parm_rta));
                                    break;
                            }
                        }
                    }
                    break;
                case NDTA_STATS:
                    {
                        struct ndt_stats *stats = (struct ndt_stats*)RTA_DATA(rta);
                        printf("STATS:\n");
                        printf("\tallocs: %d\n", stats->ndts_allocs);
                        printf("\tdestorys: %d\n", stats->ndts_destroys);
                        printf("\tgrow: %d\n", stats->ndts_hash_grows);
                        printf("\tres_failed: %d\n", stats->ndts_res_failed);
                        printf("\tlookups %d\n", stats->ndts_lookups);
                        printf("\thits: %d\n", stats->ndts_hits);
                        printf("\trcv_probes_mcast: %d\n", stats->ndts_rcv_probes_mcast);
                        printf("\trcv_probes_ucast: %d\n", stats->ndts_rcv_probes_ucast);
                        printf("\tperiodic_gc_runs: %d\n", stats->ndts_periodic_gc_runs);
                        printf("\tforced_gc_runs: %d\n", stats->ndts_forced_gc_runs);
                    }
                    break;
                case NDTA_GC_INTERVAL:
                    printf("GC_INTERVAL %lld\n", *(uint64_t*)RTA_DATA(rta));
                    break;
            }
        }
    }
}

void main() {
    srand(time(NULL));
    seq = rand();

    int sock = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE);
    if ( sock == -1 ) {
        perror("sock error\n");
        exit(1);
    }

    struct {
        struct nlmsghdr nh;
        struct rtgenmsg rt;
    } h;

    struct sockaddr_nl nl;
    memset(&nl, 0, sizeof(nl));
    nl.nl_family = AF_NETLINK;

    if ( bind(sock, (struct sockaddr*)&nl, sizeof(nl)) == -1 ) {
        perror("bind error\n");
        exit(1);
    }

    memset(&h, 0, sizeof(h));
    h.nh.nlmsg_type  = RTM_GETNEIGHTBL;
    h.nh.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_DUMP;
    h.nh.nlmsg_len   = NLMSG_LENGTH(sizeof(h.rt));
    h.nh.nlmsg_seq   = seq;
    h.rt.rtgen_family = AF_INET;

    int n = write(sock, &h, sizeof(h));
    if ( n <= 0 ) {
        perror("write error\n");
        exit(1);
    }
    msg_receive(sock);
}

出力はこんな感じ

NAME: arp_cache
GC_INTERVAL 10000
THRESH1 128
THRESH2 512
THRESH3 1024
CONFIG:
        key_len: 4
        entry_size: 260
        entries: 3
        last_flush: 27997298
        last_rand: 63783
        hash_rnd: 398501669
        hash_mask: 7
        hash_chain_gc: 0
        proxy_qlen: 0
STATS:
        allocs: 49
        destorys: 46
        grow: 0
        res_failed: 25
        lookups 61661
        hits: 48360
        rcv_probes_mcast: 0
        rcv_probes_ucast: 0
        periodic_gc_runs: 2274
        forced_gc_runs: 0
PARAMS:
        refcnt: 1
        queue_len: 3
        proxy_qlen: 64
        app_probes: 0
        ucast_probes: 3
        mcast_probes: 3
        reachable_time: 18336
        base_reachable_time: 20000
        gc_staletime: 60000
        delay_probe_time: 5000
        retrans_time: 1000
        anycast_delay: 1000
        proxy_delay: 800
        locktime: 1000

linuxカーネルのpid管理(取得周り)

カーネルのpid管理と取得周りを調べてみた

PID名前空間

Linuxではpid_namespace(詳しくはman 7参照)に対応しているため、 1つのプロセスに対して複数のプロセスIDを複数持てるようになっている。

pid_namespaceは階層構造になっていて、 子のプロセスIDは同一プロセスでもそれぞれの名前空間で別のプロセスidとなる f:id:totofuga:20170624091642p:plain

親で作成されたのプロセスIDは子からは見えないようになっている f:id:totofuga:20170624091957p:plain

したがってプロセス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の一覧を管理する f:id:totofuga:20170624102843p:plain

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が書き換えられているので本来呼びたい関数に直接ジャンプできるという仕組みになっている

f:id:totofuga:20170622105542p:plain

f:id:totofuga:20170622105623p:plain

LinuxFreeBSDの挙動の違い

一度解決したリンク先がdlcloseによって参照先がなくなった場合の挙動にlinuxBSDでは微妙に違うところがある。

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のメモ

ソースコード

  • 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)