Как работать с сырыми сокетами (SOCK_RAW) | Часть 3
Продолжение (3-я часть) статьи на тему «сырых сокетов» на языке С++
0x5. необработанный вывод
=============== Проанализировав, как обрабатываются входящие сырые дейтаграммы, мы готовы к переходу подробнее о механизме необработанного вывода. Что будем проверять вот какие значения IP-заголовка записываются по умолчанию каждым ядром и за какие из них мы обязаны сами заполнить. Это конечно применяется только в том случае, если включена опция IP_HDRINCL (установленная нами или ядро по умолчанию [Linux - IPPROTO_RAW]).
а. Linux
******** Основным обработчиком вывода необработанного сокета является raw_sendmsg (), который определен в /usr/src/linux.2.6*/net/ipv4/raw.c: static int raw_sendmsg (struct kiocb * iocb, struct sock * sk, struct msghdr * msg, size_t len) { struct inet_sock * inet = inet_sk (sk); struct ipcm_cookie ipc; struct rtable * rt = NULL; / * ... * / если (inet-> hdrincl) err = raw_send_hdrinc (sk, msg-> msg_iov, len, rt, msg-> msg_flags); / * ... * / } Если установлен IP_HDRINCL, то вызывается raw_send_hdrinc () (определено в тот же файл): static int raw_send_hdrinc (struct sock * sk, void * from, size_t length, структура rtable * rt, беззнаковые целочисленные флаги) { / * ... * / if (length> rt-> u.dst.dev-> mtu) { ip_local_error (sk, EMSGSIZE, rt-> rt_dst, inet-> dport, rt-> u.dst.dev-> mtu); возврат -EMSGSIZE; } / * Мы не изменяем недопустимый заголовок * / iphlen = iph-> ihl * 4; if (iphlen> = sizeof (* iph) && iphlen <= length) { если (! iph-> saddr) iph-> saddr = rt-> rt_src; iph-> проверка = 0; iph-> tot_len = htons (длина); если (! iph-> id) ip_select_ident (iph, & rt-> u.dst, NULL); iph-> check = ip_fast_csum ((unsigned char *) iph, iph-> ihl); } если (iph-> протокол == IPPROTO_ICMP) icmp_out_count (((struct icmphdr *) skb_transport_header (skb)) -> тип); err = NF_HOOK (PF_INET, NF_IP_LOCAL_OUT, skb, NULL, rt-> u.dst.dev, dst_output); / * ... * / } Код здесь довольно очевиден. Если приложение не определило IP адрес источника, затем он заполняется. То же самое касается IP-идентификации. номер. Всегда заполняются два поля: контрольная сумма IP. (надеюсь для нас - это избавит нас от хлопот) и общая длина, iph-> tot_len дейтаграммы (здесь нет коррупции). Вышесказанное подтверждено от man 7 raw: + ------------------------------------------------- - + | Поля заголовка IP, измененные при отправке IP_HDRINCL | + ---------------------- + -------------------------- - + | Контрольная сумма IP | Всегда заполняется. + ---------------------- + -------------------------- - + | Исходный адрес | Заполняется, когда ноль. | + ---------------------- + -------------------------- - + | Идентификатор пакета | Заполняется, когда ноль. | + ---------------------- + -------------------------- - + | Общая длина | Всегда заполняется. | + ---------------------- + -------------------------- - + Обратите внимание, что в конце вызывается функция dst_output () и уровень IP вообще не участвует. В результате это имеет один отрицательный эффект (хотя в производительность лучше): при необходимости не будет фрагментации IP. Этот означает, что необработанный пакет больше, чем MTU интерфейса, вероятно, быть отброшенным. Вместо этого ip_local_error (), который выполняет общую очистку sk_buff, вызывается и возвращается ошибка EMSGSIZE. С другой стороны, нормальный сырой фрагментация сокета происходит, когда мы не включаем собственный IP-заголовок.
б. FreeBSD
********** Необработанный вывод во FreeBSD обрабатывается rip_output (). Цепочка функций ядра вызываются до того, как мы дойдем до rip_output (). Первый sosend_generic (), который определен в /usr/src/sys/kern/uipc_socket.c обращается к структуре pr_usrreqs (пользовательский запрос) и вызывает такой вызов: * so-> so_proto-> pr_usrreqs-> pru_send (...) где so - тип struct socket * so. so_proto - указатель на struct protosw * и указывает на протокол разъем. pr_usrreqs - это структура, члены которой являются указателями на функции которые используются протоколом для обслуживания запросов, поступающих из сокета слой (обычно выдается системными вызовами на уровне приложения). Для протокола RAW pr_usrreqs называется rip_usrreqs и определяется в /usr/src/sys/kern/raw_ip.c непосредственно перед концом файла: struct pr_usrreqs rip_usrreqs = { .pru_abort = rip_abort, .pru_attach = rip_attach, .pru_bind = rip_bind, .pru_connect = rip_connect, .pru_control = in_control, .pru_detach = rip_detach, .pru_disconnect = rip_disconnect, .pru_peeraddr = in_getpeeraddr, .pru_send = rip_send, .pru_shutdown = rip_shutdown, .pru_sockaddr = in_getsockaddr, .pru_sosetlabel = in_pcbsosetlabel, }; Это означает, что вызов * so-> so_proto-> pr_usrreqs-> pru_send (...) на необработанном socket, вызовет rip_send (), который определен в raw_ip.c, и вызовет rip_output (), которым мы сейчас займемся. статический int rip_send (struct socket * so, int flags, struct mbuf * m, struct sockaddr * nam, struct mbuf * control, struct thread * td) { struct inpcb * inp; u_long dst; / * ... * / return rip_output (m, so, dst); } rip_output () либо частично создаст заголовок IP перед его передачей на уровень IP, или, если мы установили опцию IP_HDRINCL, отметьте пакет как не подлежащие изменению путем установки флага IP_RAWOUTPUT. В конце концов, он звонит ip_output () и покоится с миром (рип) на данный момент. / * * Сгенерировать IP-заголовок и передать пакет в ip_output. * Пользователь может настроить параметры при помощи контрольного вызова. * / int rip_output (struct mbuf * m, struct socket * so, u_long dst) { struct ip * ip; int error; struct inpcb * inp = sotoinpcb (так); int flags = ((so-> so_options & SO_DONTROUTE)? IP_ROUTETOIF: 0) | IP_ALLOWBROADCAST; / * * Если пользователь передал нам полный IP-пакет, используйте его. * В противном случае выделите mbuf для заголовка и заполните его. * / if ((inp-> inp_flags & INP_HDRINCL) == 0) { if (m-> m_pkthdr.len + sizeof (struct ip)> IP_MAXPACKET) { m_freem (м); возврат (EMSGSIZE); } M_PREPEND (m, sizeof (struct ip), M_DONTWAIT); если (m == NULL) возврат (ENOBUFS); INP_LOCK (дюйм); ip = mtod (m, struct ip *); ip-> ip_tos = inp-> inp_ip_tos; если (inp-> inp_flags & INP_DONTFRAG) ip-> ip_off = IP_DF; еще ip-> ip_off = 0; ip-> ip_p = inp-> inp_ip_p; / * записываем указанное значение протокола XXX в сокете (AF_INET, SOCK_RAW, XXX) * / ip-> ip_len = m-> m_pkthdr.len; если (заключен в тюрьму (inp-> inp_socket-> so_cred)) ip-> ip_src.s_addr = htonl (тюрьма_getip (inp-> inp_socket-> so_cred)); еще ip-> ip_src = inp-> inp_laddr; ip-> ip_dst.s_addr = dst; ip-> ip_ttl = inp-> inp_ip_ttl; } еще { / * проверки * / / * ... * / / * не разрешать и параметры, указанные пользователем, и параметры setsockopt, и не позволяйте размерам пакетов, которые приведут к сбою * / если (((ip-> ip_hl! = (sizeof (* ip) >> 2)) && inp-> inp_options) || (ip-> ip_len> m-> m_pkthdr.len) || (ip-> ip_len <(ip-> ip_hl << 2))) { INP_UNLOCK (дюйм); m_freem (м); вернуть EINVAL; } если (ip-> ip_id == 0) ip-> ip_id = ip_newid (); / * XXX запрещает ip_output перезаписывать поля заголовка * / флаги | = IP_RAWOUTPUT; ipstat.ips_rawout ++; } / * ... * / error = ip_output (m, inp-> inp_options, NULL, flags, inp-> inp_moptions, inp); INP_UNLOCK (дюйм); ошибка возврата; } Что касается случая, когда ядро добавляет свой собственный заголовок, это стоит наших время упомянуть, что полю протокола IP-заголовка присвоено значение упоминается в сокете (AF_INET, SOCK_RAW, XXX) и * не * IPPROTO_RAW, даже если запись default_RAW фактически является его обработчиком. Нас интересует случай, показанный под условием else (case где пользователь передал полный заголовок IP-пакета). Ну в былые времена пакет большой длины может потенциально привести к сбою некоторых вещей, так как проверок ip_len (который является общей длиной дейтаграммы) не было. Еще одна интересная вещь здесь заключается в том, что ядро будет использовать случайное значение для установите ip_id (идентификационный номер - используется при фрагментации), если он был слева со значением 0. Следующее - флаг IP_RAWOUTPUT, о котором мы говорили. чуть раньше. Посмотрим, как это повлияет на ip_output () который определен в одноименном файле .c в /usr/src/sys/netinet/ip_output.c: int ip_output (struct mbuf * m, struct mbuf * opt, struct route * ro, int flags, struct ip_moptions * imo, struct inpcb * inp) { / * ... * / if ((flags & (IP_FORWARDING | IP_RAWOUTPUT)) == 0) { ip-> ip_v = IPVERSION; ip-> ip_hl = hlen >> 2; ip-> ip_id = ip_newid (); ipstat.ips_localout ++; } еще { hlen = ip-> ip_hl << 2; } / * ... * / / * * Если достаточно мало для интерфейса, или интерфейс примет * забота о фрагментации для нас, мы можем просто отправить напрямую. * / / * ... * / ip-> ip_sum = 0; если (sw_csum & CSUM_DELAY_IP) ip-> ip_sum = in_cksum (м, хлен); / * ... * / / * * Если адрес источника еще не указан, используйте адрес * выходного интерфейса. * / если (ip-> ip_src.s_addr == INADDR_ANY) { / * Интерфейс может не иметь адресов. * / if (ia! = NULL) { ip-> ip_src = IA_SIN (ia) -> sin_addr; } } / * ... * / } Как мы можем легко увидеть, если пакет отмечен флагом IP_RAWOUTPUT тогда (почти) ядро не обрабатывает поля. * Только * контрольная сумма IP всегда рассчитывается для нас. Кроме того, если в приложении нет указан любой исходный IP-адрес, тогда адрес исходящего интерфейса использовал. Как правило, пользователь имеет полный контроль над заголовком IP. После того, как ip_output () завершает обработку пакета, он передается более функция низкого уровня if_output (), внутреннее устройство которой выходит за рамки этого текст. В отличие от Linux, сырые пакеты во FreeBSD всегда будут (даже если мы включить наш собственный IP-заголовок, который) при необходимости будет фрагментирован, давая пользователю больше гибкости при создании больших пакетов.
Продолжение следует…
Источник https://sock-raw.org/papers/sock_raw (автоперевод)