Реализовать протокол socks5 с помощью nodejs

Node.js

Источник этой статьиshenyifengtk.github.io/Если перепечатано, укажите источник

Представляем носки5

socks5s — это сетевой протокол передачи, который в основном используется для промежуточной передачи данных между клиентом и внешним сетевым сервером. Когда клиент за брандмауэром хочет получить доступ к внешнему серверу, он подключается к прокси-серверу SOCKS. Этот прокси-сервер контролирует право клиента на доступ к внешней сети.Если это разрешено, он отправляет запрос клиента на внешний сервер.

参考图
Согласно модели OSI, SOCKS — это протокол сеансового уровня, расположенный между уровнем представления и транспортным уровнем, то есть socks — это протокол поверх TCP.

по сравнению с HTTP-прокси

Прокси-серверы HTTP могут проксировать только HTTP-запросы.Такие протоколы, как TCP и HTTPS, очень слабы и имеют определенные ограничения. SOCKS работает на более низком уровне, чем HTTP-прокси: SOCKS использует протокол рукопожатия для информирования прокси-программы о соединениях, которые его клиенты пытаются установить SOCKS, а затем делает это настолько прозрачно, насколько это возможно, тогда как обычные прокси-серверы могут интерпретировать и > перезаписывать заголовки (например, , Используйте другой базовый протокол, например FTP, однако прокси-серверы HTTP просто пересылают HTTP-запросы на нужный HTTP-сервер). Хотя прокси-серверы HTTP имеют разные шаблоны использования, метод CONNECT позволяет Допускается переадресация TCP-соединений, однако прокси-серверы SOCKS также могут пересылать трафик UDP и обратные прокси-серверы, тогда как прокси-серверы HTTP не могут. Прокси-серверы HTTP обычно лучше знают протокол HTTP и выполняют фильтрацию более высокого уровня (хотя обычно используются только для GET и метод POST, а не для метода CONNECT).

Официальный протокол RFC

Выберите метод аутентификации

Вообще говоря, процесс соединения socks, сначала клиент отправляет пакет данных прокси-серверу socks

Var NMETHODS METHODS
1 1 0-255

Единицы в таблице представляют количество цифр

  • VarУказывает версию SOCK, должно быть 5;
  • NMETHODSвыражатьMETHODSдлина секции
  • METHODSУказывает список методов аутентификации, поддерживаемых клиентом, каждый метод занимает 1 байт. Текущее определение
    • 0x00 Аутентификация не требуется
    • 0x01 GSSAPI
    • 0x02 Аутентификация по имени пользователя и паролю
    • 0x03 — 0x7F, выделенный IANA (зарезервировано)
    • 0x80 - 0xFE зарезервировано для закрытых методов
    • 0xFF Нет приемлемого метода

Сервер ответит клиенту

VER METHOD
1 1
  • VarУказывает версию SOCK, должно быть 5;
  • METHODЯвляется ли метод выбора на стороне сервера, значение этого вышеMETHODSодин из списка. Если клиент поддерживает 0x00, 0x01, 0x02, эти три метода. Сервер выберет только один метод аутентификации и вернет его клиенту.Если он вернет 0xFF, это означает, что метод аутентификации не выбран, и клиенту необходимо закрыть соединение. Сначала мы используем простой Nodejs для реализации рукопожатия sock-соединения.Просмотрите дейтаграмму, отправленную клиентом.
const net = require('net');
let server = net.createServer(sock =>{
sock.once('data', (data)=>{
console.log(data);
});
});
server.listen(8888,'localhost');

Используйте инструмент curl для подключения nodejs

curl -x socks5://localhost:8888 https://www.baidu.com

консольный вывод

<Buffer 05 02 00 01>

Использовать аутентификацию по паролю учетной записи

Когда сервер выбирает метод аутентификации учетной записи и пароля 0x02, клиент начинает отправлять номер учетной записи и пароль Формат пакета данных следующий: (в байтах)

VER ULEN UNAME PLEN PASSWD
1 1 1 to 255 1 1 to 255
  • VER — это версия SOCKS.
  • Длина имени пользователя ULEN
  • Строка учетной записи UNAME
  • Длина пароля PLEN
  • Строка пароля PASSWD

Видно, что пароль учетной записипередача открытого текста, очень небезопасно. После завершения проверки на стороне сервера он ответит следующими данными():

VER STATUS
1 1
  • СТАТУС 0x00 означает успех, 0x01 означает отказ

запрос пакета

После завершения аутентификации клиент может отправить информацию запроса. Клиент начинает инкапсулировать информацию запроса Формат запроса SOCKS5 (в байтах):

VER CMD RSV ATYP DST.ADDR DST.PORT
1 1 0x00 1 динамичный 2
  • VER — это версия SOCKS, здесь должно быть 0x05;
  • CMD — это код команды для SOCK
    • 0x01 означает запрос CONNECT

      • Запрос CONNECT может открыть двусторонний канал связи между клиентом и запрошенным ресурсом. Его можно использовать для создания туннелей. Например,**CONNECT** Может использоваться для доступа к принятымSSL (HTTPS) сайт протокола. Клиент просит прокси-сервер туннелировать TCP-соединение с хостом назначения. После этого сервер установит соединение с хостом назначения вместо клиента. После установления соединения прокси-сервер отправляет или получает поток TCP-сообщений клиенту.
    • 0x02 означает запрос BIND

      Метод Bind используется, когда целевому хосту необходимо активно подключаться к клиенту (протокол ftp).

      Когда CMD в пакете данных, полученном сервером, равен X'02', сервер использует метод Bind для прокси. При использовании метода Bind в качестве прокси сервер должен отвечать клиенту не более двух раз на пакеты данных.

      Сервер использует TCP-протокол для подключения к соответствующему (DST.ADDR, DST.PORT), и в случае сбоя возвращает неудачный пакет и закрывает сеанс. В случае успеха прослушайте (BND.ADDR, BND.PORT), чтобы принять запрос запрашивающего хоста, а затем верните первый пакет данных, который используется клиентом для отправки данных с указанием адреса клиента подключения целевого хоста и сумки порта.

      После того, как целевому узлу удастся или не удастся подключиться к адресу и порту, указанным сервером, ответьте на второй пакет. В это время (BND.ADDR, BND.PORT) должны быть адрес и порт соединения между целевым хостом и сервером.

    • 0x03 означает переадресацию UDP

  • RSV 0x00, зарезервировано
  • тип АТИП
    • 0x01 IPv4-адрес, часть DST.ADDR длиной 4 байта
    • 0x03 Доменное имя, первый байт части DST.ADDR — это длина доменного имени, а остальная часть DST.ADDR — доменное имя без конца \0.
    • 0x04 IPv6-адрес длиной 16 байт.
  • DST.ADDR адрес назначения
  • Порт назначения DST.PORT в сетевом порядке байтов образец данных

<Buffer 05 01 00 01 0e d7 b1 26 01 bb>

Сервер инкапсулирует данные в соответствии с клиентом, запрашивает удаленный сервер и отвечает клиенту в следующем фиксированном формате.

VER REP RSV ATYP BND.ADDR BND.PORT
1 1 0x00 1 динамичный 2
  • VER — это версия SOCKS, здесь должно быть 0x05;
  • Поле ответа REP
    • 0x00 означает успех
    • 0x01 Нормальное соединение с сервером SOCKS не удалось
    • 0x02 Соединение не разрешено существующими правилами
    • 0x03 сеть недоступна
    • 0x04 хост недоступен
    • 0x05 В соединении отказано
    • 0x06 Тайм-аут TTL
    • 0x07 Неподдерживаемая команда
    • 0x08 Неподдерживаемый тип адреса
    • 0x09 - 0xFF не определено
  • RSV 0x00, зарезервировано
  • ATYP
    • 0x01 IPv4-адрес, часть DST.ADDR длиной 4 байта
    • 0x03 Доменное имя, первый байт части DST.ADDR — это длина доменного имени, остальная часть DST.ADDR — доменное имя, и в конце нет \0.
    • 0x04 IPv6-адрес длиной 16 байт.
  • Адрес привязки сервера BND.ADDR
  • BND.PORT Порт, к которому привязан сервер, в сетевом порядке байтов.

Используйте nodejs  для реализации запроса CONNECT

const net = require('net');
const dns = require('dns');
const AUTHMETHODS = { //只支持这两种方法认证
	NOAUTH: 0,
	USERPASS: 2
}

//创建socks5监听

let socket = net.createServer(sock => {

		//监听错误
		sock.on('error', (err) => {
			console.error('error code %s',err.code);
                        console.error(err);
		});

                sock.on('close', () => {
			sock.destroyed || sock.destroy();
		});

		sock.once('data', autherHandler.bind(sock)); //处理认证方式
	});

let autherHandler = function (data) {
	let sock = this;
	console.log('autherHandler ', data);
	const VERSION = parseInt(data[0], 10);
	if (VERSION != 5) { //不支持其他版本socks协议
		sock.destoryed || sock.destory();
		return false;
	}
	const methodBuf = data.slice(2); //方法列表

	let methods = [];
	for (let i = 0; i < methodBuf.length; i++)
		methods.push(methodBuf[i]);
	//先判断账号密码方式
	let kind = methods.find(method => method === AUTHMETHODS.USERPASS);
	if (kind) {
		let buf = Buffer.from([VERSION, AUTHMETHODS.USERPASS]);
		sock.write(buf);
		sock.once('data', passwdHandler.bind(sock));
	} else {
		kind = methods.find(method => method === AUTHMETHODS.NOAUTH);
		if (kind === 0) {
			let buf = Buffer.from([VERSION, AUTHMETHODS.NOAUTH]);
			sock.write(buf);
			sock.once('data', requestHandler.bind(sock));
		} else {
			let buf = Buffer.from([VERSION, 0xff]);
			sock.write(buf);
			return false;
		}
	}

}

/**
 * 认证账号密码
 */
let passwdHandler = function (data) {
	let sock = this;
	console.log('data ', data);
	let ulen = parseInt(data[1], 10);
	let username = data.slice(2, 2 + ulen).toString('utf8');
	let password = data.slice(3 + ulen).toString('utf8');
	if (username === 'admin' && password === '123456') {
		sock.write(Buffer.from([5, 0]));
	} else {
		sock.write(Buffer.from([5, 1]));
		return false;
	}
	sock.once('data', requestHandler.bind(sock));
}

/**
 * 处理客户端请求
 */
let requestHandler = function (data) {
	let sock = this;
	const VERSION = data[0];
	let cmd = data[1]; // 0x01 先支持 CONNECT连接
        if(cmd !== 1)
          console.error('不支持其他连接 %d',cmd);
        let flag = VERSION === 5 && cmd < 4 && data[2] === 0;
	if (! flag)
		return false;
	let atyp = data[3];
	let host,
	port = data.slice(data.length - 2).readInt16BE(0);
	let copyBuf = Buffer.allocUnsafe(data.length);
	data.copy(copyBuf);
	if (atyp === 1) { //使用ip 连接
		host = hostname(data.slice(4, 8));
		//开始连接主机!
	        connect(host, port, copyBuf, sock);

	} else if (atyp === 3) { //使用域名
		let len = parseInt(data[4], 10);
		host = data.slice(5, 5 + len).toString('utf8');
		if (!domainVerify(host)){
			console.log('domain is fialure %s ', host);
                        return false;
                }
		console.log('host %s', host);
		dns.lookup(host, (err, ip, version) => {
			if(err){
				console.log(err)
				return;
			}			
			connect(ip, port, copyBuf, sock);
		});

	}
}

let connect = function (host, port, data, sock) {
        if(port < 0 || host === '127.0.0.1')
           return;
	console.log('host %s port %d', host, port);
	let socket = new net.Socket();
	socket.connect(port, host, () => {
		data[1] = 0x00;
                if(sock.writable){
			sock.write(data);
			sock.pipe(socket);
			socket.pipe(sock);
		}
	});
 
        socket.on('close', () => {
		socket.destroyed || socket.destroy();
        });
        
	socket.on('error', err => {
		if (err) {
                        console.error('connect %s:%d err',host,port);
			data[1] = 0x03;
                        if(sock.writable)
			sock.end(data);
			console.error(err);
			socket.end();
		}
	})
}

let hostname = function (buf) {
	let hostName = '';
	if (buf.length === 4) {
		for (let i = 0; i < buf.length; i++) {
			hostName += parseInt(buf[i], 10);
			if (i !== 3)
				hostName += '.';
		}
	} else if (buf.length == 16) {
		for (let i = 0; i < 16; i += 2) {
			let part = buf.slice(i, i + 2).readUInt16BE(0).toString(16);
			hostName += part;
			if (i != 14)
				hostName += ':';
		}
	}
	return hostName;
}

/**
 * 校验域名是否合法
 */
let domainVerify = function (host) {
	let regex = new RegExp(/^([a-zA-Z0-9|\-|_]+\.)?[a-zA-Z0-9|\-|_]+\.[a-zA-Z0-9|\-|_]+(\.[a-zA-Z0-9|\-|_]+)*$/); 
	return regex.test(host);
}


socket.listen(8888,() => console.log('socks5 proxy running ...')).on('error', err => console.error(err));
                                                                                                                        

end

При использовании в связке с браузером я обнаружил, что нет возможности загрузить видео Douyu.Почему-то у Youku нет проблем. Я только что узнал некоторые знания о NodeJs, и письмо так себе.Если есть что-то, что не очень хорошо написано, пожалуйста, укажите это и обсудите это вместе. Когда я впервые посмотрел на протокол, я подумал, что после того, как клиент (браузер) и сервер завершат запрос аутентификации, обе стороны будут поддерживать длительное TCP-соединение, и клиент напрямую отправит пакет запроса инкапсуляции. Фактически каждый запрос клиента начинается с аутентификации, и каждый запрос не зависит друг от друга, поэтомуonceЭтот метод особенно подходит здесь