نوشته‌های من

درباره برنامه‌نویسی، بازی، کتاب و هر چیزی که دوست داشته باشم

نوشته‌های من

درباره برنامه‌نویسی، بازی، کتاب و هر چیزی که دوست داشته باشم

نوشته‌های من
طبقه بندی موضوعی

چگونه یک فایروال بنویسیم؟

پنجشنبه, ۱۹ دی ۱۳۹۲، ۰۱:۴۳ ق.ظ

مطلبی که در اینجا قصد دارم بنویسم، نتیجه جستجوی من در ارتباط با نحوه نوشتن یک فایروال در لینوکس است.

چگونه می‌توان در لینوکس بسته‌های شبکه را به صورت برخط دریافت کرد؟ چگونه می‌توان آن‌ها را بلاک کرد و یا تغییر داد؟ برنامه‌ای مانند iptables چگونه عمل می‌کند؟ دیگر فایروال‌های تحت لینوکس چگونه کار می‌کنند؟

این‌ها سوال‌هایی در ذهنم بودند که برای آن‌ها دنبال جواب بودم و البته راه‌های رسیدن به یک فایروال زیاد است و چیزی که اینجا بیان می‌شود تنها یکی از آن‌هاست و لزوما بهترین راه نیست. لازم به ذکر است که منظور از فایروال، فایروال نسل یک است و به طور خلاصه منظور فایروالی است که فقط بسته‌ها را فیلتر می‌کند و State-full نیست و بسیاری از ویژگی‌های یک فایروال نسل جدید را ندارد. برای مطالعه نسل‌های مختلف فایروال به صفحه فایروال در ویکی‌پدیا[^] و یا مراجع دیگر مراجعه نمایید.

توجه نمایید که آنچه در اینجا آمده است بر روی کرنل لینوکس 3.13.0-rc4+ و 3.5.0.40 تست شده است و ممکن است برای نسخه‌های دیگر نیاز به مقداری تغییر داشته باشد.

نت فیلتر (NetFilter) چیست؟

همانطور که از اسمش بر‌می‌آید، برای فیلتر کردن بسته‌های شبکه است و در واقع یک زیر سیستم از کرنل لینوکس است برای فیلتر کردن بسته‌ها، که امکان تعریف قلاب‌(hook)ها‌یی در مسیر حرکت بسته‌های شبکه را فراهم می‌سازد. به کمک این قلاب‌ها، در نقاط مختلف مسیر حرکت بسته‌ها در کرنل لینوکس، می‌توانید آن‌ها را دریافت کنید و در صورت نیاز بررسی کنید و یا تغییر بدهید سپس آن‌ها را به مسیر برگردانید و یا از ادامه مسیر حذفشان کنید.

استفاده از این قلاب‌ها تنها در فضای کرنل لینوکس(Kernel Space) مانند «مــاژول‌هــای کرنل لــینوکــس»(Kernel Modules) امکان پذیر است و نمی‌توانید از آن‌ها در برنامه‌هایی که در کرنل لینوکس اجرا نمی‌شوند(User Space Applications) استفاده کنید.

نمونه‌ای از این ماژول‌ها که از قابلیت قلاب‌های نت‌فیلتر استفاده می‌کنند مجموعه ماژول‌های iptables, ip6tables, ebtables, iparptable هستند که به عنوان فایروال در اکثر لینوکس‌ها استفاده می‌شود.

مسیر حرکت بسته‌ها

برای اینکه بتوانیم قلاب‌های نت‌فیلتر را در نقطه مناسبی ایجاد کنیم، بایستی ابتدا مسیر حرکت بسته‌های شبکه را در لینوکس بشناسیم. در نمودار زیر مسیر حرکت بسته‌ها و همچنین نقاطی که می‌توان قلاب ایجاد کرد قابل مشاهده است.

Packets in linux

بسته‌های شبکه در نمودار بالا می‌توانند مربوط به سه مسیر باشند.

مسیر اول، مربوط به بسته‌هایی است که وارد این سیستم شده و مرتبط به همین سیستم است. این‌گونه بسته‌ها پس از ورود توسط مسیر یاب به پشته شبکه در لینوکس ارسال می‌شود و توسط این پشته به دست لایه‌های بالاتر می‌رسد. این بسته‌ها از نقاط ۱ و ۲ در نمودار بالا عبور می‌کنند.

مسیر دوم، مربوط به بسته‌هایی است که توسط این سیستم تولید شده و به بیرون ارسال می‌شوند. این‌گونه یسته‌ها توسط پشته شبکه به مسیریاب ارسال شده و از یکی از خروجی‌ها به بیرون ارسال می‌گردد. این بسته‌ها از نقاط ۵ و ۴ عبور می‌کنند.

مسیر سوم، مربوط به بسته‌هایی است که مقصدشان این سیستم نیست و این سیستم برای آن‌ها نقش مسیر یاب را بازی می‌کند. این بسته‌ها پس از ورود، به مسیریاب ارسال شده و سپس از یکی از خروجی‌ها به بیرون ارسال می‌گردد. این بسته‌ها از نقاط ۱، ۳ و ۴ عبور می‌کنند.


نقاط دریافت بسته

بنابراین قلاب ۱، یعنی NF_INET_PRE_ROUTING، نقطه‌ای است که تمامی بسته‌های ورودی را در برمی‌گیرد، چه بسته‌هایی که مقصد نهایی‌شان این سیستم است و چه آن‌هایی که نیست.

قلاب ۲، یعنی NF_INET_LOCAL_IN، نقطه‌ای است که تمامی بسته‌های ورودی را که مقصد نهایی‌شان این سیستم است در بر می‌گیرد.

قلاب ۳، یعنی NF_INET_FORWARD، نقطه‌ای است که تمامی بسته‌های ورودی را که مقصد نهایی‌شان این سیستم نیست در بر می‌گیرد.

قلاب ۴، یعنی NF_INET_POST_ROUTING، نقطه‌ای است که تمامی بسته‌های خروجی را در بر می‌گیرد. چه آن‌هایی که مبداءشان این سیستم باشد و چه آن هایی که مبدا‌ءشان این سیستم نباشد.

و در نهایت قلاب ۵، یعنی NF_INET_LOCAL_OUT، نقطه‌ای است که تمامی بسته‌های خروجی را که از پشته شبکه این سیستم خارج می‌شوند را در بر می‌گیرد.

تعریف این نقاط را می‌توانید در فایل linux/netfilter.h[^]  مشاهده کنید.


تصمیم قلاب

هر قلابی که تعریف و ثبت می‌کنید باید نسبت به ادامه حرکت بسته تصمیم گیری کند. این تصمیم گیری توسط مقدار برگشتی از تابع قلاب مشخص می‌شود که می‌توانند یکی از مقادیر زیر باشند. این مقادیر در فایل linux/netfilter.h[^] تعریف شده‌اند.

  • NF_DROP
  • NF_ACCEPT
  • NF_STOLEN
  • NF_QUEUE
  • NF_REPEAT

    ارسال NF_DROP به عنوان مقدار برگشتی قلاب، باعث حذف بسته از ادامه مسیر و آزاد شدن منابع در اختیارش، می‌شود. 

    ارسال NF_ACCEPT به عنوان مقدار برگشتی، باعث ادامه مسیر دادن بسته می‌شود.

    ارسال NF_STOLEN به عنوان مقدار برگستی، باعث حذف بسته از ادامه مسیر می‌شود ولی همچنان این بسته در اختیار تابع قلاب قرار دارد و منابعش آزاد نمی‌شود.

    ارسال NF_QUEUE باعث می‌شود بسته برای استفاده برنامه‌های فضای کاربری (User Space) درون صف قرار بگیرد.

    و در نهایت ارسال NF_QUEUE باعث فراخوانی مجدد این تابع قلاب می‌شود.


    اولویت قلاب

    هر تابع قلابی که ثبت می‌شود نیازمند تعیین یک اولویت است چرا که زیرسیستم نت‌فیلتر توابع قلاب مربوط به یک نقطه را بر اساس اولویتشان فرا‌می‌خواند. کمترین مقدار اولویت دارای بالاترین میزان اولویت است. در اینجا از مقدار تعریف شده NF_IP_PRI_FIRST استفاده می‌شود که با اولیت‌ترین مقدار است. این مقدار در فایل linux/netfilter_ipv4.h[^] تعریف شده است.

    تابع قلاب

    تابع  قلاب بایستی مشابه نمونه زیر باشد، که در فایل linux/netfilter.h[^]  تعریف شده است:
    typedef unsigned int nf_hookfn(unsigned int hooknum,
                                   struct sk_buff *skb,
                                   const struct net_device *in,
                                   const struct net_device *out,
                                   int (*okfn)(struct sk_buff *));

    زمانی‌که این تابع توسط زیر‌سیستم نت‌فیلتر فرا‌خوانده می‌شود، hooknum بیانگر نقطه‌ای است که قلاب در آن ثبت شده است، skb اشاره‌گر به ساختاری است که حاوی اطلاعات بسته است، in بیان‌گر واسط شبکه ورودی و out بیان‌گر واسط شبکه خروجی است و آخرین پارامتر اشاره‌گر به تابعی است که توسط خود نت‌فیلتر بعد از اینکه کار تمامی قلاب‌ها به پایان رسید فراخوانده می‌شود و معمولا توابع قلاب آن را فراخوانی نمی‌‌کنند چرا که باعث می‌شود بقیه قلاب‌ها نتوانند کار خود را انجام دهند.

    ثبت قلاب

    اکنون که نقاط ثبت قلاب، اولویت قلاب و مقدار تصمیم قلاب را شناختیم، نوبت به نحوه ثبت قلاب در زیرسیستم نت‌فیلتر است. این عمل با استفاده از تابع nf_register_hook استفاده می‌کنیم. پارامتر ورودی این تابع یک متغیر از نوع ساختار nf_hook_ops است. تمامی اطلاعات مربوط به قلاب در این متغیر تنظیم شده و سپس توسط آن تابع در زیرسیستم نت‌فیلتر ثبت می‌گردد. 

    ساختار nf_hook_ops به صورت زیر در فایل linux/netfilter.h[^] تعریف شده است.

    struct nf_hook_ops {
            struct list_head list;
    
            /* User fills in from here down. */
            nf_hookfn *hook;
            struct module *owner;
            u_int8_t pf;
            unsigned int hooknum;
            /* Hooks are ordered in ascending priority. */
            int priority;
    };
    
    مقادیری که بایستی در این ساختار تعیین شود عبارتند از:
    • hook که اشاره‌گر به تابع قلاب است .
    • owner بیان‌گر ماژولی است که این تابع در آن تعریف و ثبت می‌شود. معمولا از ماکرو THIS_MODULE برای این قسمت استفاده می‌شود.
    • pf نوع پروتکل بسته‌هایی را که تابع می‌خواهد دریافت کند معین می‌کند و در اینجا از مقدار  NFPROTO_IPV4 استفاده می‌کنیم تا بسته‌های IPv4 را دریافت کنیم. بقیه پروتکل‌ها را می‌توانید در فایل linux/socket.h[^] مشاهده کنید.
    • hooknum بیان کننده نقطه‌ای است که می‌خواهید قلاب را در آن‌جا ثبت کنید. بایستی یکی از مقادیری باشد که در بالا عنوان شد. در اینجا از مقدار NF_INET_PRE_ROUTING استفاده می‌کنیم تا تمامی بسته‌های ورودی را دریافت کنیم.
    • priority نیز برای مشخص کردن اولویت این قلاب است که در اینجا از مقدار ماکرو  NF_IP_PRI_FIRST  استفاده می‌کنیم.

    جمع بندی

    در نهایت با استفاده از تمامی اطلاعاتی که در بالا ذکر شد می‌توان یک ماژول برای کرنل لینوکس به صورت زیر نوشت. این ماژول در هنگام راه‌اندازی با استفاده از تابع ذکر شده، قلاب را ثبت می‌کند. همچنین این ماژول در زمان خذف شدن، تابع قلاب را با استفاده از تابع nf_unregister_hook از نت‌فیلتر حذف می‌کند.

    توجه کنید که نوشتن ماژول‌های کرنل لینوکس بسیار حساس است و با کوچکترین اشتباه در برنامه، کرنل لینوکس کرش می‌کند و سیستم ریبوت می‌شود.

    مثال یک

    در این مثال ساده سعی شده است تا ساختار یک ماژول کرنل لینوکس به همراه یک تابع قلاب آورده شود. این تابع قلاب تمامی بسته‌ها را دور می‌اندازد!
    #include <linux/module.h>
    #include <linux/kernel.h>
    #include <linux/netdevice.h>
    #include <linux/etherdevice.h>
    #include <net/ip.h>
    #include <linux/tcp.h>
    #include <linux/udp.h>
    
    unsigned int nf_pre_route_hook( unsigned int hooknum, struct sk_buff *skb,
            const struct net_device *in, const struct net_device *out,
            int(*okfn)( struct sk_buff * ) );
    
    static struct nf_hook_ops myhook_ops __read_mostly = {
        .pf = NFPROTO_IPV4,
        .priority = NF_IP_PRI_FIRST,
        .owner = THIS_MODULE,
        .hooknum = NF_INET_PRE_ROUTING,
        .hookfn = (nf_hookfn *)nf_pre_route_hook,
    };
    
    static int __init my_firewall_init(void)
    {
        return nf_register_hook(&myhook_ops);
    }
    
    static void __exit my_firewall_exit(void)
    {
        nf_unregister_hook(&myhook_ops);
    }
    
    unsigned int nf_pre_route_hook( unsigned int hooknum, struct sk_buff *skb,
            const struct net_device *in, const struct net_device *out,
            int(*okfn)( struct sk_buff * ) )
    {
        return NF_DROP;
    }
    
    module_init(my_firewall_init);
    module_exit(my_firewall_exit);
    

    مثال دو

    در این مثال فقظ تابع قلاب آورده شده است. این تابع در عمل یک قالب برای شما خواهد بود تا بتوانید بسته به نیازتان آن را تکمیل نمایید. این تابع فقط اطلاعات مربوط به هر بسته را لاگ می‌زند.
    unsigned int nf_pre_route_hook( unsigned int hooknum, struct sk_buff *skb,
            const struct net_device *in, const struct net_device *out,
            int(*okfn)( struct sk_buff * ) )
    {
        struct ethhdr *eth = eth_hdr(skb); /*  ‌(Ethernet) دریافت یک اشاره‌گر به ساختار  */
        u_int16_t etype;
    
        printk( "%s: Received a packet %p, device in = %s\n", __func__,
                    skb, in ? in->name : "<NONE>" );
    
        /* آیا این بسته مالتی‌کست است؟ */
        if( is_multicast_ether_addr(eth->h_dest) )
        {
            /* کاری که می‌خواهید انجام دهید */
            printk( "Packet is multicast\n" );
        }
    
        /* آیا این بسته برادکست است؟ */
        if( is_broadcast_ether_addr(eth->h_dest) )
        {
            /* کاری که می‌خواهید انجام دهید */
            printk( "Packet is broadcast\n");
        }
    
        /* EtherType دریافت */
        etype = ntohs( eth->h_proto );
    
        /* آیا بسته حاوی بسته آی‌پی است؟ */
        if( etype == ETH_P_IP )
        {
            struct iphdr *ip = NULL;
            struct udphdr *udp = NULL;
            struct tcphdr *tcp = NULL;
    
            ip = ip_hdr(skb);
            if (ip == NULL)
            {
                return NF_ACCEPT;
            }
    
            /* است؟ UDP آیا پرونکل */
            if (ip->protocol == IPPROTO_UDP)
            {
                udp = (struct udphdr *)(skb_network_header(skb) + ip_hdrlen(skb));
    
                /* کاری که می‌خواهید انجام دهید */
                printk( "UDP packet\n");
            }
            /*  است؟ TCP آیا پروتکل */
            else if (ip->protocol == IPPROTO_TCP)
            {
                tcp = (struct tcphdr *)(skb_network_header(skb) + ip_hdrlen(skb));
    
                /* کاری که می‌خواهید انجام دهید */
                printk( "TCP packet\n");
            }
        }
    
        return NF_ACCEPT;
    }
    

    دانلود

    سورس کدهای این ماژول‌ها را می‌توانید از پروژه گیت‌هاب به آدرس github.com/zaghaghi/firewall-kernel-module[^] دریافت کنید.


    منابع



    • حامد ذقاقی

    نظرات  (۱)

    سلام مطالبی که گذاشته اید عالی هستن
    اگر اطلاعات بیشتری راجب نوشتن فایروال دارید لطفا برام ایمیل کنید.
    خیلی خیلی ضروری
    ممنون
    پاسخ:
    سلام،
    فایروالی که قصد نوشتنش را دارید در چه پهنای باندی قرار است کار کنه؟
    ارسال نظر آزاد است، اما اگر قبلا در بیان ثبت نام کرده اید می توانید ابتدا وارد شوید.
    تجدید کد امنیتی