Программирование на C++ глазами хакера

         

Серверные функции


Вы уже знаете, что протокол TCP работает по технологии "клиент-сервер". Чтобы два компьютера смогли установить соединение, один из них должен запустить прослушивание на определенном порту. И только после этого клиент может присоединиться к серверу.

Давайте рассмотрим функции, необходимые для создания сервера. Первым делом следует связать сетевой локальный адрес с уже созданным сокетом. Для этого используется функция bind:

int bind ( SOCKET s, const struct sockaddr FAR* name, int namelen );

Давайте посмотрим на параметры этой функции:

предварительно созданный сокет;

указатель на структуру типа sockaddr;

размер структуры sockaddr, указанной в качестве второго параметра.

Структура sockaddr предназначена для хранения адреса, а в разных протоколах используется своя адресация. Поэтому и структура sockaddr может выглядеть по-разному. Для интернет-протоколов структура имеет имя sockaddr_in и выглядит следующим образом:

struct sockaddr_in { short sin_family; u_short sin_port; struct in_addr sin_addr; char sin_zero[8]; };

Рассмотрим параметры этой структуры:

sin_family — семейство протоколов. Этот параметр схож с первым параметром функции socket. Для интернет-протоколов указывается константа AF_INET;

sin_port — порт для идентификации программы поступающими данными;

sin_addr — структура SOCKADDR_IN, которая хранит IP-адрес;

sin_zero — используется для выравнивания адреса из параметра sin_addr. Это необходимо, чтобы размер структуры SOCKADDR_IN равнялся размеру SOCKADDR.

Сейчас я хочу подробнее остановиться на портах. Вы должны быть очень внимательны при выборе порта, потому что если он уже занят какой-либо программой, то вторая попытка закончится ошибкой. Вы должны знать, что некоторые порты зарезервированы для определенных (наиболее популярных) служб. Номера этих портов распределяются центром Internet Assigned Numbers Authority. Существует три категории портов:

0—1023 — управляются IANA и зарезервированы для стандартных служб. Не рекомендуется использовать порты из этого диапазона;




1024—49151 — зарезервированы IANA, но могут использоваться процессами и программами. Большинство из этих портов можно использовать;

49152—65535 — частные порты, никем не зарезервированы.

Если во время выполнения функции bind выяснится, что порт уже используется какой-либо службой, то функция вернет ошибку WSAEADDRINUSE.

Давайте рассмотрим пример кода, который создает сокет и привязывает к нему сетевой локальный адрес:

SOCKET s=socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);

struct sockaddr_in addr; addr.sin_family = AF_INET; addr.sin_port = htons(4888); addr.sin_addr.s_addr=htonl(INADDR_ANY);

bind(s, (SOCKADDR*)addr), sizeof(addr);

В данном примере создается сокет со следующими параметрами:

AF_INET — означает, что будет использоваться семейство интернет-протоколов;

SOCK_STREAM — указывает на протокол, устанавливающий соединение;

IPPROTO_TCP — используется протокол TCP.

Затем объявляется структура addr типа sockaddr_in. В параметре sin_family структуры также указывается семейство интернет-протоколов (AF_INET). В параметре sin_port указывается номер порта. Байты в номере должны следовать в определенном порядке, который несовместим с порядком байт в числовых переменных языка С, поэтому происходит преобразование с помощью функции htons.

В параметре sin_addr.s_addr указывается специальный адрес inaddr_any, который позволит в дальнейшем программе ожидать соединение на любом сетевом интерфейсе. Это значит, что если у вас две сетевые карты, соединенные с разными сетями, то программа будет ожидать соединения из обеих сетей. Есть еще один адрес, который можно указать, — INADDR_ANY. Позволяет рассылать широковещательные данные для всех компьютеров сети.

После того как локальный адрес и порт привязаны к сокету, можно приступить к прослушиванию порта в ожидании соединения со стороны клиента. Для этого служит функция listen, которая выглядит следующим образом:

int listen ( SOCKET s, int backlog );

Первый параметр — это все тот же сокет, который был создан и к которому привязан адрес. По этим данным функция определит, на каком порту нужно запустить прослушивание.



Второй параметр — это максимально допустимое число запросов, ожидающих обработки. Допустим, что вы указали здесь значение 3, а вам пришло 5 запросов на соединение от разных клиентов. Только первые три из них встанут в очередь, а остальные получат ошибку WSAECONNREFUSED, поэтому при написании клиента (в части соединения) обязательно должна быть проверка.

При вызове функции listen вы можете получить следующие основные ошибки:

WSAEINVAL — функция bind не была вызвана для данного сокета;

WSANOTINITIALISED — не загружена библиотека WinSock, т.е. не выполнена функция WSAStartup;

WSAENETDOWN — нарушена сетевая подсистема;

WSAEISCONN — сокет уже подключен.

Остальные ошибки возникают реже.

Когда клиент попадает в очередь на подключение к серверу, необходимо разрешить соединение с помощью функции accept. Она выглядит следующим образом:

SOCKET accept ( SOCKET s, struct sockaddr FAR* addr, int FAR* addrlen );

Во второй версии есть функция WSAAccept, у которой первые три параметра такие же, как и у функции accept. Функция WSAAccept выглядит следующим образом:

SOCKET WSAAccept ( SOCKET s, struct sockaddr FAR * addr, LPINT addrlen, LPCONDITIONPROC lpfnCondition, DWORD dwCallbackData );

Давайте рассмотрим общие параметры для этих функций :

предварительно созданный и запущенный на прослушивание сокет;

указатель на структуру типа sockaddr;

размер структуры sockaddr, указанной в качестве второго параметра.

После выполнения функции accept второй параметр (addr) будет содержать сведения об IP-адресе клиента, который произвел подключение. Эти данные можно использовать для контроля доступа к серверу по IP-адресу. Но вы должны помнить, что злоумышленнику не составляет труда подделать IP-адрес, поэтому такую защиту нельзя назвать достаточной, но она может усложнить взлом сервера.

Функция accept возвращает указатель на новый сокет, который можно использовать для общения с клиентом. Старая переменная типа SOCKET продолжает слушать порт в ожидании новых соединений, и ее использовать нет смысла. Таким образом, для каждого подключенного клиента будет свой SOCKET, благодаря чему вы сможете работать с любым из них.

Если вы вспомните пример с передачей данных с использованием MFC-объектов (см. разд. 4.5), то там применялся тот же метод. Как только клиент подключался к серверу, мы создавали новый сокет, через который и происходила работа с подключившимся клиентом. Именно этот сокет принимал данные, пришедшие по сети, и мог их отправлять обратно программе на стороне клиента.


Содержание раздела