Как работать с сырыми сокетами (SOCK_RAW) | Часть 1
Представляем вашему вниманию гайд по работе с сырыми сокетами (SOCK_RAW). Здесь автоперевод. В конце статьи ссылка на первоисточник.
Статья за 2008 год. И вместе с тем, в качестве примера тут C++. А это, как известно, классика.
0x1. Вступление
=================
Цель этой статьи — объяснить часто неправильно понимаемую природу сырых сокетов. Движущей силой написания этого текста стало любопытство автора. чтобы изучить все тонкости этого мощного типа сокетов, также известного как SOCK_RAW. То, что здесь будет обсуждаться, * не * будет еще одним руководством о том, как изготовить собственные пакеты вручную. Эта тема много раз обсуждалась чрезмерно и в сети можно найти довольно много упоминаний о нем (микшер и т. д.). Какие Здесь будет обсуждаться то, что необработанные сокеты делают за кулисами.
Мы по этой причине собираюсь углубиться во внутреннее устройство сетевого стека. Предполагается, что читатель уже имеет некоторый опыт работы с сокетами и готов взглянуть на некоторый код ядра, поскольку реализация сырых сокетов фактически зависит от ОС. Мы будет охватывать реализации как FreeBSD 7.0, так и Linux 2.6. Большинство вещей покрыто для FreeBSD может также применяться к OpenBSD, NetBSD и даже MAC OS X.
0x2. Создание
=============
Перво-наперво. Создание. Как создается сырой сокет? Какие основные в хитросплетениях? Необработанный сокет создается путем вызова системного вызова socket (2) и определив тип сокета как SOCK_RAW следующим образом:
int fd = socket(AF_INET, SOCK_RAW, XXX);
где XXX — это * протокол int *, который, как мы обсудим далее, является главный источник путаницы и проблем, вникающий в сам факт того, что здесь могут применяться разные комбинации. Допустимые значения: IPPROTO_RAW, IPPROTO_ICMP, IPPOROTO_IGMP, IPPROTO_TCP, 0 (осторожно — см. Ниже), IPPROTO_UDP и т. Д. При другой комбинации возникает другое поведение. И это поведение критично для того, как ядро взаимодействует с приложение, создающее необработанный сокет.
Прежде чем переходить к конкретным комбинациям для каждой ОС, давайте сначала рассмотрим посмотрите на фактическое значение значения * протокола *. Чтобы все протоколы работали одновременно был использован конкретный общий подход к проектированию. Согласно ему, похожие протоколы сгруппированы в домены. Домен обычно определяется тем, что называется семейством протоколов или семейством адресов (последнее является самым последним практика) и ряд констант используются для различения между ними. В самые распространенные из них:
PF_INET / AF_INET --> Internet protocols (TCP, UDP etc)
PF_LOCAL, PF_UNIX / AF_LOCAL, AF_UNIX --> Unix local IPC protocol
PF_ROUTE / AF_ROUTE --> routing tables
Linux определяет эти константы в /usr/src/linux-2.6.*/include/linux/socket.h
/* Поддерживаемые семейства адресов. */
#define AF_UNSPEC 0
#define AF_UNIX 1 /* Unix domain sockets */
#define AF_LOCAL 1 /* POSIX name for AF_UNIX */
#define AF_INET 2 /* Internet IP Protocol */
/* ... */
/* Семейства протоколов, такие же, как семейства адресов. */
#define PF_UNSPEC AF_UNSPEC
#define PF_UNIX AF_UNIX
#define PF_LOCAL AF_LOCAL
#define PF_INET AF_INET
/* ... */
FreeBSD определяет указанные выше значения (почти такие же) в /usr/src/sys/sys/socket.h Как вы уже догадались, мы займемся Семья AF_INET. Семейство Интернета разбивает свои протоколы на протокол типы, каждый из которых может состоять более чем из одного протокол.
Linux определяет типы протоколов семейства Интернет в /usr/src/linux-2.6.*/include/linux/net.h
enum sock_type {
SOCK_STREAM = 1,
SOCK_DGRAM = 2,
SOCK_RAW = 3,
SOCK_RDM = 4,
SOCK_SEQPACKET = 5,
SOCK_DCCP = 6,
SOCK_PACKET = 10,
};
FreeBSD определяет типы AF_INET в
/usr/src/sys/sys/socket.h
/*
* Types
*/
#define SOCK_STREAM 1 /* stream socket */
#define SOCK_DGRAM 2 /* datagram socket */
#define SOCK_RAW 3 /* raw-protocol interface */
#if __BSD_VISIBLE
#define SOCK_RDM 4 /* reliably-delivered message */
#endif
#define SOCK_SEQPACKET 5 /* sequenced packet stream */
Если вы в прошлом занимались программированием сокетов, то вы, вероятно, признать некоторые из вышеперечисленных. Один из них должен быть вторым аргументом вызов socket (AF_INET, …, …). Третий аргумент — это значение IPPROTO_XXX, которое определяет фактический протокол над IP. Это важно понимать значение. Это значение / число — то, что IP-уровень будет записывать в Поле protocol_type в его заголовке для определения протокола верхнего уровня. Это Поле «Протокол», как вы видите в IP-заголовке ниже (RFC 791).
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|Version| IHL |Type of Service| Total Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Identification |Flags| Fragment Offset |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Time to Live | Protocol | Header Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Destination Address |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Это одно из самых важных полей, поскольку именно оно будет использоваться уровень IP на стороне получателя, чтобы понять, какой уровень находится над ним (для пример TCP или UDP) дейтаграмма должна быть доставлена.
Linux определяет эти протоколы в /usr/src/linux-2.6.*/include/linux/in.h
/* Standard well-defined IP protocols. */
enum {
IPPROTO_IP = 0, /* Dummy protocol for TCP */
IPPROTO_ICMP = 1, /* Internet Control Message Protocol */
IPPROTO_IGMP = 2, /* Internet Group Management Protocol */
IPPROTO_IPIP = 4, /* IPIP tunnels (older KA9Q tunnels use 94) */
IPPROTO_TCP = 6, /* Transmission Control Protocol */
IPPROTO_EGP = 8, /* Exterior Gateway Protocol */
IPPROTO_PUP = 12, /* PUP protocol */
IPPROTO_UDP = 17, /* User Datagram Protocol */
IPPROTO_IDP = 22, /* XNS IDP protocol */
IPPROTO_DCCP = 33, /* Datagram Congestion Control Protocol */
IPPROTO_RSVP = 46, /* RSVP protocol */
IPPROTO_GRE = 47, /* Cisco GRE tunnels (rfc 1701,1702) */
IPPROTO_IPV6 = 41, /* IPv6-in-IPv4 tunnelling */
IPPROTO_ESP = 50, /* Encapsulation Security Payload protocol */
IPPROTO_AH = 51, /* Authentication Header protocol */
IPPROTO_BEETPH = 94, /* IP option pseudo header for BEET */
IPPROTO_PIM = 103, /* Protocol Independent Multicast */
IPPROTO_COMP = 108, /* Compression Header protocol */
IPPROTO_SCTP = 132, /* Stream Control Transport Protocol */
IPPROTO_UDPLITE = 136, /* UDP-Lite (RFC 3828) */
IPPROTO_RAW = 255, /* Raw IP packets */
IPPROTO_MAX
};
FreeBSD определяет значения IPPROTO_XXX в
/usr/src/sys/netinet/in.h
Вот пример для IPPROTO_RAW:
#if __POSIX_VISIBLE >= 200112
#define IPPROTO_RAW 255 /* raw IP packet */
#define INET_ADDRSTRLEN 16
#endif
С таким количеством различных комбинаций лучше всего обсуждать вещи последовательно, чтобы начнем с часто используемого значения протокола * 0 *. Вы когда-нибудь задумывались, как Системный вызов socket (2) волшебным образом находит, какой протокол использовать, даже если он был называется как socket (…, …, 0)? Например, когда приложение вызывает его так:
socket(AF_INET, SOCK_STREAM, 0);
как ядро узнает, с каким протоколом связать сокет? Дело в том, что ядро не делает никаких предположений — ядро не играет в кости с пользовательским пространством (цитируя сумасшедшего физика и великого ума http://www.quotedb.com/quotes/878 в несколько ином контексте) — в большинстве случаи, которые есть (см. шифрование и энтропию для противоположной парадигмы)
Во FreeBSD все, что он делает, это ассоциирует первый протокол, найденный в связанный список доменов через функцию pffindproto (dom, type).
Давайте будем более конкретными и посмотрим код POC:
Сокет создается с помощью функции ядра socreate (), которая определяется как это: (исходный код из FreeBSD 7.0 /usr/src/sys/kern/uipc_socket.c)
/*
* socreate returns a socket with a ref count of 1. The socket should be
* closed with soclose().
*/
int
socreate(int dom, struct socket **aso, int type, int proto,
struct ucred *cred, struct thread *td)
{
struct protosw *prp;
struct socket *so;
int error;
if (proto)
prp = pffindproto(dom, proto, type);
else
prp = pffindtype(dom, type);
/* .... */
}
Секрет кроется в двух функциях pffindproto () и pffindtype (). Если proto == 0, тогда вызывается pffindtype, который менее строг, чем pffindproto (). Как видно из кода, pffindtype () не проверяет значение протокола в all и просто возвращает * первую * структуру protosw, которую он находит, выводя ее из только pr_type (тип протокола: SOCK_STREAM, SOCK_DGRAM, SOCK_RAW и т. д.) и семья / домен (AF_INET, AF_LOCAL и т. д.).
Каждая структура protosw (переключение протокола) представляет собой ассоциацию типа SOCK_XXX и Протокол IPPROTO_XXX. Все структуры protosw находятся внутри таблицы inetsw [] на который указывает запись inetdomain в связанном списке глобальных доменов. Графическое представление может немного прояснить ситуацию: домены:
domains:
---------
| | (domain linked list)
---------
|
|------------> isodomain: inetdomain:
--------- ---------
------- | | -----> -------| | -------> .....
| --------- | ---------
| |
| |
|---> isosw[]: |---> inetsw[]:
--------- ---------
| | | IP |
--------- ---------
| | | UDP |
--------- ---------
| | | TCP |
--------- ---------
| | |IP(raw)| (default entry)
--------- ---------
| | | ICMP |
--------- ---------
| IGMP |
---------
| ... |
---------
| ... |
---------
|IP(raw)| (wildcard entry)
---------
Примечание. Место IP (raw) в 4-м индексе (inetsw [3]) упоминается для исторические причины и более новые реализации стека FreeBSD отличаются в этом отношении. В частности, если в ядре определена поддержка SCTP, тогда IP (необработанный), ICMP и остальные перемещаются на 3 позиции вверх по массиву inetsw [] фактически становится inetsw [6], inetsw [7] и т. д. Конечно, здесь нет никаких значительная разница в исходных кодах ядра, поскольку inetsw [] никогда не используется индекс, но по имени. На протяжении всего текста мы будем использовать соглашение, относящееся к записи по умолчанию (inetsw [3]) как default_RAW и запись RAW с подстановочными знаками (последний член inetsw [] и в старые времена inetsw [6]) как wildcard_RAW для ясности.
Необработанная запись с подстановочными знаками (та, у которой .pr_protocol не присвоено значение и, следовательно, имеющий значение 0) определяется как последний член массива inetsw [] в /usr/src/sys/netinet/in_proto.h:
/* raw wildcard */
{
.pr_type = SOCK_RAW,
.pr_domain = &inetdomain,
.pr_flags = PR_ATOMIC|PR_ADDR,
.pr_input = rip_input,
.pr_ctloutput = rip_ctloutput,
.pr_init = rip_init,
.pr_usrreqs = &rip_usrreqs
},
}; /* end of inetsw[] */
Вернемся к поиску, который выглядит так:
pffindtype:
- найти соответствующий домен через значение * family *
- вернуть первую запись соответствующего protosw таблица, которая соответствует *type* значению
pffindproto:
- найти соответствующий домен через значение * family *
- вернуть первое совпадение пары * тип * — * протокол *
- если пара не найдена, а тип — SOCK_RAW, верните запись по умолчанию для сырого IP-адреса — default_RAW (см. ниже)
обе функции возвращают запись inetsw [] (то есть указатель protosw * на соответствующее смещение массива) /usr/src/sys/kernel/uipc_domain.c:
struct protosw *
pffindtype(int family, int type)
{
struct domain *dp;
struct protosw *pr;
for (dp = domains; dp; dp = dp->dom_next)
if (dp->dom_family == family)
goto found;
return (0);
found:
for (pr = dp->dom_protosw; pr < dp->dom_protoswNPROTOSW; pr++)
if (pr->pr_type && pr->pr_type == type)
return (pr);
return (0);
}
struct protosw *
pffindproto(int family, int protocol, int type)
{
struct domain *dp;
struct protosw *pr;
struct protosw *maybe = 0;
if (family == 0)
return (0);
for (dp = domains; dp; dp = dp->dom_next)
if (dp->dom_family == family)
goto found;
return (0);
found:
for (pr = dp->dom_protosw; pr < dp->dom_protoswNPROTOSW; pr++) {
if ((pr->pr_protocol == protocol) && (pr->pr_type == type))
return (pr);
if (type == SOCK_RAW && pr->pr_type == SOCK_RAW &&
pr->pr_protocol == 0 && maybe == (struct protosw *)0)
maybe = pr;
}
return (maybe);
}
Изучая приведенный выше код, мы замечаем еще одно важное значение SOCK_RAW. По сути, последние несколько строк pffindproto () используют SOCK_RAW как резервный протокол по умолчанию. Это означает, что если пользовательское приложение вызывает socket (2) вот так: сокет
socket(AF_INET, SOCK_RAW, 30);
где протокол со значением 30 не указан в ядре, то вместо в случае неудачи используется wildcard_RAW. Это потому, что это единственный protosw struct в массиве inetsw [], содержащем .pr_protocol равным 0 и pr_type равным SOCK_RAW.
То же самое и с таким вызовом:
socket(AF_INET, SOCK_RAW, IPPROTO_TCP);
Тип SOCK_RAW и IPPROTO_TCP не совпадают, поскольку SOCK_RAW имеет только записи для ICMP, IGMP и сырого IP. Однако это идеальный правильный вызов, поскольку ядро вернет подстановочный знак SOCK_RAW.
Это касается FreeBSD. Что касается Linux, возможно, у вас уже есть видно из фрагмента кода сверху (in.h), значение * 0 * на самом деле определяется как другой тип протокола для TCP. Эта практика имеет сильный эффект в переносе приложений между * BSD и Linux. Например, для * BSD это верный:
socket(AF_INET, SOCK_RAW, 0);
Обратите внимание, что при вводе значения 0 будет возвращена запись default_RAW, а не запись с подстановочным знаком, так как pffindtype () вернет первую структуру protosw с тип SOCK_RAW внутри таблицы inetsw [] и первый — default_RAW Вход.
В Linux вы получите ошибку EPROTONOSUPPORT. См. Код ядра ниже, чтобы понять, почему это происходит.
/usr/src/linux-2.6.*/net/ipv4/af_inet.c:
/* При запуске мы вставляем все элементы inetsw_array [] в * связанный список inetsw.
*/
static struct inet_protosw inetsw_array[] =
{
{
.type = SOCK_STREAM,
.protocol = IPPROTO_TCP,
.prot = &tcp_prot,
.ops = &inet_stream_ops,
.capability = -1,
.no_check = 0,
.flags = INET_PROTOSW_PERMANENT |
INET_PROTOSW_ICSK,
},
{
.type = SOCK_DGRAM,
.protocol = IPPROTO_UDP,
.prot = &udp_prot,
.ops = &inet_dgram_ops,
.capability = -1,
.no_check = UDP_CSUM_DEFAULT,
.flags = INET_PROTOSW_PERMANENT,
},
{
.type = SOCK_RAW,
.protocol = IPPROTO_IP, /* wild card */
.prot = &raw_prot,
.ops = &inet_sockraw_ops,
.capability = CAP_NET_RAW,
.no_check = UDP_CSUM_DEFAULT,
.flags = INET_PROTOSW_REUSE,
}
};
static int inet_create(struct net *net, struct socket *sock, int protocol)
{
/* ... */
/* Look for the requested type/protocol pair. */
answer = NULL;
lookup_protocol:
err = -ESOCKTNOSUPPORT;
rcu_read_lock();
list_for_each_rcu(p, &inetsw[sock->type]) {
answer = list_entry(p, struct inet_protosw, list);
/* Check the non-wild match. */
if (protocol == answer->protocol) {
if (protocol != IPPROTO_IP)
break;
} else {
/* Check for the two wild cases. */
if (IPPROTO_IP == protocol) {
protocol = answer->protocol;
break;
}
if (IPPROTO_IP == answer->protocol)
break;
}
err = -EPROTONOSUPPORT;
answer = NULL;
}
/* ... */
}
Помните сверху, что IPPROTO_IP = 0. Приведенный выше код можно разбить на следующие случаи:
1) socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
protocol = 6
answer = inet_protosw[0]
protocol = answer->protocol : first "if" TRUE
OK
2) socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
protocol = 17
answer = inet_protosw[1]
protocol = answer->protocol : first "if" TRUE
OK
3) socket(AF_INET, SOCK_STREAM, 0);
protocol = 0
answer = inet_protosw[0]
if (protocol == answer->protocol) : FALSE
check else :
/* Check for the two wild cases. */
if (IPPROTO_IP == protocol) {
protocol = answer->protocol;
break;
}
: TRUE
note that protocol value 0 is substituted with the
real value of IPPROTO_TCP in line:
protocol = answer->protocol;
OK
4) socket(AF_INET, SOCK_DGRAM, 0);
protocol = 0
answer = inet_protosw[1]
if (protocol == answer->protocol) : FALSE
check else :
/* Check for the two wild cases. */
if (IPPROTO_IP == protocol) {
protocol = answer->protocol;
break;
}
: TRUE
note that protocol value 0 is substituted with the
real value of IPPROTO_UDP in line:
protocol = answer->protocol;
OK
5) socket(AF_INET, SOCK_RAW, 0);
protocol = 0
answer = inet_protosw[2]
protocol == IPPROTO_IP so : if (protocol != IPPROTO_IP) is FALSE
not OK -> EPROTONOSUPPORT
6) socket(AF_INET, SOCK_STREAM, 9); (where 9 can be any protocol
except IPPROTO_TCP)
protocol = 9
answer = inet_protosw[0]
if (protocol == answer->protocol) : FALSE
check else :
/* Check for the two wild cases. */
if (IPPROTO_IP == protocol) {
protocol = answer->protocol;
break;
}
if (IPPROTO_IP == answer->protocol)
break;
both are a FALSE
not OK -> EPROTONOSUPPORT
7) socket(AF_INET, SOCK_DGRAM, 9); (where 9 can be any protocol except
IPPROTO_UDP)
same as above
not OK -> EPROTONOSUPPORT
8) socket(AF_INET, SOCK_RAW, 9); (where 9 can be *any* protocol except 0)
protocol = 9
answer = inet_protosw[2]
if (protocol == answer->protocol) : FALSE
check else :
/* Check for the two wild cases. */
if (IPPROTO_IP == protocol) {
protocol = answer->protocol;
break;
}
: FALSE
if (IPPROTO_IP == answer->protocol)
break;
: TRUE
OK
Случай 8 демонстрирует, как Linux использует SOCK_RAW в качестве резервного протокола, как и мы видели выше с FreeBSD.
Это не единственные различия между двумя системами. Мы обсудим больше из них дальше.
Продолжение следует….
Источник https://sock-raw.org/papers/sock_raw (автоперевод)