Туннелирование - взгляд изнутри

В данной статье будут рассмотрены способы туннелирования протоколов прикладного уровня через HTTP, SOCKS4 и SOCKS5 proxy servers. По ходу будут приводиться выдержки из RFC 2616 и 1928, а также примеры кода. Итак, все по порядку.

tunneling via HTTP Proxy.

В RFC 2616 (HTTP 1.1) описаны следующие методы: OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT.
Пролистав ниже по спецификации, читаем следующее о методе CONNECT: "Спецификация резервирует имя метода CONNECT для использования с прокси, которые могут динамически переключаться на работу в качестве туннеля (например, туннелирование SSL)". Далее идет ссылка на документ (draft-luotonen-web-proxy-tunneling-XX), некоторые выдержки из которого будут приводиться ниже. Этот документ описывает расширение протокола HTTP 1.x (не только 1.1, но и 1.0) для организации туннелирования TCP-based протоколов через веб-прокси. Теперь собственно о том, как это работает.
1. Клиент соединяется с HTTP Proxy, поддерживающим метод CONNECT.
2. Клиент посылает серверу строку вида "CONNECT target_host:target_port HTTP/1.x\r\nUser-Agent: User Agent\r\n"
3. В ответ на эту строку сервер может либо потребовать авторизацию, либо вернуть состояние удачного соединения либо ошибку.
Для первого случая это:

HTTP/1.x 407 Proxy auth required
Proxy-agent: Proxy Agent
Proxy-authenticate:...
<Empty line>

Тогда необходимо послать серверу следующую строку:

CONNECT target_host:target_port HTTP/1.x
User-agent: User Agent
Proxy-authorization: basic password_in_base64_encoding
<Empty line>

В случае удачно установленного соединения сервер вернет следующее:

HTTP/1.0 200 Connection established
Proxy-agent: Proxy Agent
<Empty line>

В общем диалог может принять следующий вид (если нужна авторизация):

CLIENT -> SERVER SERVER -> CLIENT

CONNECT target_host:target_port HTTP/1.x
User-agent: User Agent
<Empty line>
HTTP/1.x 407 Proxy auth required
Proxy-agent: Proxy Agent
Proxy-authenticate: ...
<Empty line>
CONNECT target_host:target_port HTTP/1.x
User-agent: User Agent
Proxy-authorization: ...
<Empty line>
HTTP/1.x 200 Connection established
Proxy-agent: Netscape-Proxy/1.1
<Empty line>
<Data tunneling to both directions begins>

(выдержка из draft-luotonen-web-proxy-tunneling-XX)
Открытый тунель может использоваться не только как клиент-прокси канал, но и как прокси-прокси канал. Отсюда следует, что можно сделать цепочку из нескольких прокси.
Немного кода:

int len;
char buff[BUFF_SIZE];
bzero (buff,sizeof(buff));
sprintf(buff, "CONNECT %s:%d HTTP/1.0\r\nUser-Agent: ProxyChains 1.8\r\n", inet_ntoa( * (struct in_addr *) &ip), ntohs(port));
len = strlen (buff);
if(len!=send(sock,buff,len,0))
return SOCKET_ERROR;
//читаем, что нам вернул сервер

bzero(buff,sizeof(buff));
len=0 ;
while(len<BUFF_SIZE)
{
if(1==read_n_bytes(sock,buff+len,1))
len++;
else
return SOCKET_ERROR;
if ( len > 4 &&
buff[len-1]=='\n' &&
buff[len-2]=='\r' &&
buff[len-3]=='\n' &&
buff[len-4]=='\r' ) break;
}

// если ответ не 200, значит либо порт закрыт, либо хост мертв
if ( (len==BUFF_SIZE) ||
! ( buff[9] =='2' &&
buff[10]=='0' &&
buff[11]=='0' )) return BLOCKED;
return SUCCESS;

Далее, если все в порядке (туннель готов), можно интерпретировать полученный туннель как обычное соединение с target_host:target_port.

tunneling via SOCKS Proxy.

немного о назначении SOCKS протоколов

RFC 1928: "Использование сетевых файрволлов и систем, эффективно скрывающих организацию внутренней сетевой структуры от внешней сети, такой как Интернет, становится все более популярным. Эти файрволлы обычно работают как шлюзы прикладного уровня между сетями, предлагая обычно telnet-, FTP-, и SMTP-доступ. С появлением более сложных протоколов прикладного уровня, предназначенных для облегчения глобального информационного взаимодействия, появилась потребность в обеспечении общей основы для прозрачной и безопасной работы через файрволл для этих протоколов. Существует также необходимость в строгой аутентификации при работе через файрволл, в некоторой степени похожей на используемые сейчас методы. Это требование обусловлено тем, что отношения типа клиент-сервер появляются между сетями различных организаций, и эти отношения должны быть управляемыми и, зачастую, строго аутентифицированы. Описываемый протокол разработан, чтобы обеспечить основу для удобного и безопасного использования сервиса сетевых файрволлов для приложений типа клиент-сервер, работающих по протоколам TCP и UDP. Протокол представляет собой "уровень-прокладку" между прикладным уровнем и транспортным уровнем, и, как таковой, не обеспечивает сервиса шлюзов сетевого уровня, такого, как пересылка пакетов ICMP. "

SOCKS 4 tunneling

SOCKS-запрос формируется следующим образом:

0    1     2            4            8
+----+-----+------------+------------+
|VER | CMD | DST.PORT   | DST.ADDR   |
+----+-----+------------+------------+

Где:
VER — версия протокола: 4;
CMD — CONNECT: 1, другие значения нас не интерисуют;
DST.ADDR — требуемый адрес;
DST.PORT — требуемый порт (в сетевом порядке октетов).
Если в первых двух байтах ответа сервера содержится 0 и отличное от 90 значение — значит установленно соединение с DST.ADDR:DST.PORT. Иначе — ошибка. Если нет ошибки, в исходный сокет можно писать туннелируемые данные.
Исходный код:

int len;
char buff[BUFF_SIZE];
bzero (buff,sizeof(buff));
memset(buff,0,sizeof(buff));
buff[0]=4; // socks version
buff[1]=1; // connect command
memcpy(&buff[2],&port,2); // dest port
memcpy(&buff[4],&ip,4); // dest host
len=strlen(user)+1; // username
if(len>1)
strcpy(&buff[8],user);
if((len+8)!=write_n_bytes(sock,buff,(8+len)))
return SOCKET_ERROR;

if(8!=read_n_bytes(sock,buff,8))
return SOCKET_ERROR;

if (buff[0]!=0||buff[1]!=90)
return BLOCKED;

return SUCCESS;

SOCKS 5 tunneling

Этот протокол имеет более сложную структуру по сравнению с предыдущей версией (4).
Клиент соединяется с сервером и посылает сообщение с номером версии и выбором соответствующего метода аутентификации:

+----+--------------+-------------+
|VER | NMETHODS     | METHODS     |
+----+--------------+-------------+
| 1  | 1            | 1 to 255    |
+----+--------------+-------------+

Поле NMETHODS содержит число октетов в идентификаторах методов авторизации в поле METHODS.
Серевер выбирает один из предложенных методов, перечисленных в METHODS, и послылает ответ о выбранном методе:

+----+-----------+
|VER | METHOD    |
+----+-----------+
| 1  | 1         |
+----+-----------+

Если выбранный метод в METHOD равен FF, то ни один из предложенных клиентом методов не применим и клиент должен закрыть соединение.
Следующие значения определены для поля METHOD:
· 00 — аутентификация не требуется;
· 02 — USERNAME/PASSWORD (см. RFC1929).
Остальные возможные значения поля METHOD здесь не рассматриваются .
Затем клиент и сервер начинают аутентификацию согласно выбранному методу.
После того, как аутентификация выполнена, клиент посылает детали запроса. Если выбранный метод аутентификации требует особое формирование пакетов с целью проверки целостности и/или конфедициальности, запросы должны инкапсулироваться в пакет, формат которого определяется выбранным методом.
SOCKS-запрос формируется следующим образом:

+----+-------+-------+--------+------------+------------+
|VER | CMD   | RSV   | ATYP   | DST.ADDR   | DST.PORT   |
+----+-------+-------+--------+------------+------------+
| 1  | 1     | X'00' | 1      | Variable   | 2          |
+----+-------+-------+--------+------------+------------+

Где:
VER — версия протокола: 05;
CMD — CONNECT: X'01'
RSV — зарезервировано;
ATYP — тип адреса, следующего вида:
IP v4 адрес: 01;
имя домена: 03;
IP v6 адрес: 04;
DST.ADDR — требуемый адрес;
DST.PORT — требуемый порт (в сетевом порядке октетов).
SOCKS-запрос посылается клиентом как только он установил соединение с SOCKS-сервером и выполнил аутентификацию. Сервер обрабатывает запрос и посылает ответ в следующей форме:

+----+-----+--------+------+--------------+-------------+
|VER | REP | RSV    | ATYP | BND.ADDR     | BND.PORT    |
+----+-----+--------+------+--------------+-------------+
| 1  | 1   | X'00'  | 1    | Variable     | 2           |
+----+-----+--------+------+--------------+-------------+

Где:
VER —версия протокола: 05;
REP код ответа:
00 — успешный;
01 — ошибка SOCKS-сервера;
02 — соединение запрещено набором правил;
03 — сеть недоступна;
04 — хост недоступен;
05 — отказ в соединении;
06 — истечение TTL;
07 — команда не поддерживается;
08 — тип адреса не поддерживается;
от 09 до X'FF' — не определены.
RSV — зарезервирован;
ATYP — тип последующего адреса:
IP v4 адрес: 01;
имя домена: 03;
IP v6 адрес: 04;
BND.ADDR — выданный сервером адрес;
BND.PORT — выданный сервером порт (в сетевом порядке октетов).
Если нет ошибки, в исходный сокет можно писать туннелируемые данные.
Значения зарезервированных (RSV) полей должны быть установлены в 00.
В ответ на CONNECT, BND.PORT содержит номер порта, который сервер назначает для соединения с указанным хостом, а BND.ADDR содержит связанный IP-адрес. Выданный BND.ADDR зачастую отличается от IP-адреса, который клиент использует для доступа к SOCKS-северу, так как такие серверы часто имеют несколько IP-адресов. Ожидается, что сервер будет использовать DST.ADDR и DST.PORT и адрес клиента при обработке запроса CONNECT.
Приведем немного исходного кода:

int len;
char buff[BUFF_SIZE];
bzero (buff,sizeof(buff));
memset(buff,0,sizeof(buff));
if(user)
{
buff[0]=5; //version
buff[1]=2; //nomber of methods
buff[2]=0; // no auth method
buff[3]=2; /// auth method -> username / password
if(4!=write_n_bytes(sock,buff,4))
return SOCKET_ERROR;
}
else
{
buff[0]=5; //version
buff[1]=1; //nomber of methods
buff[2]=0; // no auth method
if(3!=write_n_bytes(sock,buff,3))
return SOCKET_ERROR;
}
memset(buff,0,sizeof(buff));
if(2!=read_n_bytes(sock,buff,2))
return SOCKET_ERROR;
if (buff[0]!=5||(buff[1]!=0&&buff[1]!=2))
{
if((buff[0]==5)&&(buff[1]==0xFF))
return BLOCKED;
else
return SOCKET_ERROR;
}
if (buff[1]==2)
{
// authentication
char in[2];
char out[515]; char* cur=out;
int c;
*cur++=1; // version
c=strlen(user);
*cur++=c;
strncpy(cur,user,c);
cur+=c;
c=strlen(pass);
*cur++=c;
strncpy(cur,pass,c);
cur+=c;
if((cur-out)!=write_n_bytes(sock,out,cur-out))
return SOCKET_ERROR;
if(2!=read_n_bytes(sock,in,2))
return SOCKET_ERROR;
if(in[0]!=1||in[1]!=0)
{
if(in[0]!=1)
return SOCKET_ERROR;
else
return BLOCKED;
}
}
buff[0]=5; // version
buff[1]=1; // connect
buff[2]=0; // reserved
buff[3]=1; // ip v4
memcpy(&buff[4],&ip,4); // dest host
memcpy(&buff[8],&port,2); // dest port
if(10!=write_n_bytes(sock,buff,10))
return SOCKET_ERROR;
if(4!=read_n_bytes(sock,buff,4))
return SOCKET_ERROR;
if (buff[0]!=5||buff[1]!=0)
return SOCKET_ERROR;
switch (buff[3])
{
case 1: len=4; break;
case 4: len=16; break;
case 3: len=0;
if(1!=read_n_bytes(sock,(char*)&len,1))
return SOCKET_ERROR;
break;
default:
return SOCKET_ERROR;
}
if((len+2)!=read_n_bytes(sock,buff,(len+2)))
return SOCKET_ERROR;
return SUCCESS;

заключение

Есть куча причин, почему вам может потребоваться туннелирование. Одна из них — анонимность. Имея цепочку из нескольких анонимных прокси, можно гарантировать большие проблемы при попытке обнаружения источника соединения.
Кстати уже есть готовый продукт: Proxychains. Взять его можно тут http://proxychains.sourceforge.net. Рекомендую изучить его. В качестве параметров он принимает имя запускаемого приложения и целевой хост. Затем это приложение запускается в контексте процесса proxychains . В результате proxychains перехватывает вызов connect и заменяет его своим. Приложение получает дескриптор сокета и даже не подозревает о существовании цепочки прокси. Proxychains поддерживает три типа цепочек: строгая (следует прямо по цепочке и в случае невозможности достижения целевого хоста возвращает ошибку), динамическая (если встречает мертвый прокси, пропускает его и следует дальше по цепочке) и рандомная (это я думаю понятно).
Цепочки прокси-серверов берутся из /etc/proxychains.conf.

pt4h



Сетевые решения. Статья была опубликована в номере 04 за 2003 год в рубрике технологии

©1999-2024 Сетевые решения