mtcute/packages/socks-proxy/index.ts

420 lines
14 KiB
TypeScript
Raw Normal View History

import { connect } from 'node:net'
// @ts-expect-error no typings
import { normalize } from 'ip6'
import type { tl } from '@mtcute/node'
import { BaseTcpTransport, IntermediatePacketCodec, MtArgumentError, NodePlatform, TransportState, assertNever } from '@mtcute/node'
import { dataViewFromBuffer } from '@mtcute/node/utils.js'
const p = new NodePlatform()
2021-05-23 12:27:16 +03:00
/**
* An error has occurred while connecting to an SOCKS proxy
*/
export class SocksProxyConnectionError extends Error {
readonly proxy: SocksProxySettings
constructor(proxy: SocksProxySettings, message: string) {
2023-09-24 01:32:22 +03:00
super(`Error while connecting to ${proxy.host}:${proxy.port}: ${message}`)
2021-05-23 12:27:16 +03:00
this.proxy = proxy
}
}
2022-08-29 16:22:57 +03:00
/**
* Settings for a SOCKS4/5 proxy
*/
2021-05-23 12:27:16 +03:00
export interface SocksProxySettings {
/**
* Host or IP of the proxy (e.g. `proxy.example.com`, `1.2.3.4`)
*/
host: string
/**
* Port of the proxy (e.g. `8888`)
*/
port: number
/**
* Proxy authorization username, if needed
*/
user?: string
/**
* Proxy authorization password, if needed
*/
password?: string
/**
* Version of the SOCKS proxy (4 or 5)
*
2022-08-29 16:22:57 +03:00
* @default `5`
2021-05-23 12:27:16 +03:00
*/
version?: 4 | 5
}
function writeIpv4(ip: string, buf: Uint8Array, offset: number): void {
2021-05-23 12:27:16 +03:00
const parts = ip.split('.')
2021-05-23 12:27:16 +03:00
if (parts.length !== 4) {
2023-09-22 15:32:28 +03:00
throw new MtArgumentError('Invalid IPv4 address')
2021-05-23 12:27:16 +03:00
}
for (let i = 0; i < 4; i++) {
const n = Number.parseInt(parts[i])
if (Number.isNaN(n) || n < 0 || n > 255) {
2023-09-22 15:32:28 +03:00
throw new MtArgumentError('Invalid IPv4 address')
2021-05-23 12:27:16 +03:00
}
buf[offset + i] = n
}
}
function buildSocks4ConnectRequest(ip: string, port: number, username = ''): Uint8Array {
const userId = p.utf8Encode(username)
const buf = new Uint8Array(9 + userId.length)
2021-05-23 12:27:16 +03:00
buf[0] = 0x04 // VER
buf[1] = 0x01 // CMD = establish a TCP/IP stream connection
dataViewFromBuffer(buf).setUint16(2, port, false)
2021-05-23 12:27:16 +03:00
writeIpv4(ip, buf, 4) // DSTIP
buf.set(userId, 8)
2021-05-23 12:27:16 +03:00
buf[8 + userId.length] = 0x00 // ID (null-termination)
return buf
}
function buildSocks5Greeting(authAvailable: boolean): Uint8Array {
const buf = new Uint8Array(authAvailable ? 4 : 3)
2021-05-23 12:27:16 +03:00
buf[0] = 0x05 // VER
2021-05-23 12:27:16 +03:00
if (authAvailable) {
buf[1] = 0x02 // NAUTH
buf[2] = 0x00 // AUTH[0] = No authentication
buf[3] = 0x02 // AUTH[1] = Username/password
} else {
buf[1] = 0x01 // NAUTH
buf[2] = 0x00 // AUTH[0] = No authentication
}
return buf
}
function buildSocks5Auth(username: string, password: string) {
const usernameBuf = p.utf8Encode(username)
const passwordBuf = p.utf8Encode(password)
2021-05-23 12:27:16 +03:00
if (usernameBuf.length > 255) {
2023-09-24 01:32:22 +03:00
throw new MtArgumentError(`Too long username (${usernameBuf.length} > 255)`)
}
if (passwordBuf.length > 255) {
2023-09-24 01:32:22 +03:00
throw new MtArgumentError(`Too long password (${passwordBuf.length} > 255)`)
}
2021-05-23 12:27:16 +03:00
const buf = new Uint8Array(3 + usernameBuf.length + passwordBuf.length)
2021-05-23 12:27:16 +03:00
buf[0] = 0x01 // VER of auth
buf[1] = usernameBuf.length
buf.set(usernameBuf, 2)
2021-05-23 12:27:16 +03:00
buf[2 + usernameBuf.length] = passwordBuf.length
buf.set(passwordBuf, 3 + usernameBuf.length)
2021-05-23 12:27:16 +03:00
return buf
}
function writeIpv6(ip: string, buf: Uint8Array, offset: number): void {
// eslint-disable-next-line ts/no-unsafe-call
ip = normalize(ip) as string
2021-05-23 12:27:16 +03:00
const parts = ip.split(':')
2021-05-23 12:27:16 +03:00
if (parts.length !== 8) {
2023-09-22 15:32:28 +03:00
throw new MtArgumentError('Invalid IPv6 address')
2021-05-23 12:27:16 +03:00
}
const dv = dataViewFromBuffer(buf)
2021-05-23 12:27:16 +03:00
for (let i = 0, j = offset; i < 8; i++, j += 2) {
const n = Number.parseInt(parts[i])
if (Number.isNaN(n) || n < 0 || n > 0xFFFF) {
2023-09-22 15:32:28 +03:00
throw new MtArgumentError('Invalid IPv6 address')
2021-05-23 12:27:16 +03:00
}
dv.setUint16(j, n, false)
2021-05-23 12:27:16 +03:00
}
}
function buildSocks5Connect(ip: string, port: number, ipv6 = false): Uint8Array {
const buf = new Uint8Array(ipv6 ? 22 : 10)
const dv = dataViewFromBuffer(buf)
2021-05-23 12:27:16 +03:00
buf[0] = 0x05 // VER
buf[1] = 0x01 // CMD = establish a TCP/IP stream connection
buf[2] = 0x00 // RSV
2021-05-23 12:27:16 +03:00
if (ipv6) {
buf[3] = 0x04 // TYPE = IPv6
writeIpv6(ip, buf, 4) // ADDR
dv.setUint16(20, port, false)
2021-05-23 12:27:16 +03:00
} else {
buf[3] = 0x01 // TYPE = IPv4
writeIpv4(ip, buf, 4) // ADDR
dv.setUint16(8, port, false)
2021-05-23 12:27:16 +03:00
}
return buf
}
const SOCKS4_ERRORS: Record<number, string> = {
91: 'Request rejected or failed',
92: 'Request failed because client is not running identd',
93: "Request failed because client's identd could not confirm the user ID in the request",
2021-05-23 12:27:16 +03:00
}
const SOCKS5_ERRORS: Record<number, string> = {
1: 'General failure',
2: 'Connection not allowed by ruleset',
3: 'Network unreachable',
4: 'Host unreachable',
5: 'Connection refused by destination host',
6: 'TTL expired',
7: 'Command not supported / protocol error',
8: 'Address type not supported',
2021-05-23 12:27:16 +03:00
}
/**
* TCP transport that connects via a SOCKS4/5 proxy.
*/
2021-07-14 21:08:13 +03:00
export abstract class BaseSocksTcpTransport extends BaseTcpTransport {
2021-05-23 12:27:16 +03:00
readonly _proxy: SocksProxySettings
constructor(proxy: SocksProxySettings) {
super()
2023-09-24 01:32:22 +03:00
if (proxy.version != null && proxy.version !== 4 && proxy.version !== 5) {
2021-05-23 12:27:16 +03:00
throw new SocksProxyConnectionError(
proxy,
`Invalid SOCKS version: ${proxy.version}`,
2021-05-23 12:27:16 +03:00
)
}
2021-05-23 12:27:16 +03:00
this._proxy = proxy
}
connect(dc: tl.RawDcOption): void {
if (this._state !== TransportState.Idle) {
2023-09-22 15:32:28 +03:00
throw new MtArgumentError('Transport is not IDLE')
}
2021-05-23 12:27:16 +03:00
if (!this.packetCodecInitialized) {
this._packetCodec.on('error', err => this.emit('error', err))
this._packetCodec.on('packet', buf => this.emit('message', buf))
2021-05-23 12:27:16 +03:00
this.packetCodecInitialized = true
}
this._state = TransportState.Connecting
this._currentDc = dc
2023-09-24 01:32:22 +03:00
this._socket = connect(this._proxy.port, this._proxy.host, this._onProxyConnected.bind(this))
2021-05-23 12:27:16 +03:00
this._socket.on('error', this.handleError.bind(this))
this._socket.on('close', this.close.bind(this))
}
private _onProxyConnected() {
let packetHandler: (msg: Uint8Array) => void
2021-05-23 12:27:16 +03:00
if (this._proxy.version === 4) {
packetHandler = (msg) => {
if (msg[0] !== 0x04) {
// VER, must be 4
this._socket!.emit(
'error',
2023-09-24 01:32:22 +03:00
new SocksProxyConnectionError(this._proxy, `Server returned version ${msg[0]}`),
2021-05-23 12:27:16 +03:00
)
2021-05-23 12:27:16 +03:00
return
}
const code = msg[1]
2023-09-24 01:32:22 +03:00
this.log.debug('[%s:%d] CONNECT returned code %d', this._proxy.host, this._proxy.port, code)
2021-05-23 12:27:16 +03:00
if (code === 0x5A) {
2021-05-23 12:27:16 +03:00
this._socket!.off('data', packetHandler)
this._socket!.on('data', data => this._packetCodec.feed(data))
2021-05-23 12:27:16 +03:00
this.handleConnect()
} else {
const msg
= code in SOCKS4_ERRORS ? SOCKS4_ERRORS[code] : `Unknown error code: 0x${code.toString(16)}`
2023-09-24 01:32:22 +03:00
this._socket!.emit('error', new SocksProxyConnectionError(this._proxy, msg))
2021-05-23 12:27:16 +03:00
}
}
2023-09-24 01:32:22 +03:00
this.log.debug('[%s:%d] connected to proxy, sending CONNECT', this._proxy.host, this._proxy.port)
2021-05-23 12:27:16 +03:00
try {
this._socket!.write(
2023-09-24 01:32:22 +03:00
buildSocks4ConnectRequest(this._currentDc!.ipAddress, this._currentDc!.port, this._proxy.user),
2021-05-23 12:27:16 +03:00
)
} catch (e) {
this._socket!.emit('error', e)
}
} else {
let state: 'greeting' | 'auth' | 'connect' = 'greeting'
const sendConnect = () => {
2023-09-24 01:32:22 +03:00
this.log.debug('[%s:%d] sending CONNECT', this._proxy.host, this._proxy.port)
2021-05-23 12:27:16 +03:00
try {
this._socket!.write(
2023-09-24 01:32:22 +03:00
buildSocks5Connect(this._currentDc!.ipAddress, this._currentDc!.port, this._currentDc!.ipv6),
2021-05-23 12:27:16 +03:00
)
state = 'connect'
} catch (e) {
this._socket!.emit('error', e)
}
}
packetHandler = (msg) => {
switch (state) {
case 'greeting': {
if (msg[0] !== 0x05) {
// VER, must be 5
this._socket!.emit(
'error',
2023-09-24 01:32:22 +03:00
new SocksProxyConnectionError(this._proxy, `Server returned version ${msg[0]}`),
2021-05-23 12:27:16 +03:00
)
2021-05-23 12:27:16 +03:00
return
}
const chosen = msg[1]
this.log.debug(
2021-05-23 12:27:16 +03:00
'[%s:%d] GREETING returned auth method %d',
this._proxy.host,
this._proxy.port,
chosen,
2021-05-23 12:27:16 +03:00
)
switch (chosen) {
case 0x00:
// "No authentication"
sendConnect()
break
case 0x02:
// Username/password
2023-09-24 01:32:22 +03:00
if (!this._proxy.user || !this._proxy.password) {
2021-05-23 12:27:16 +03:00
// should not happen
this._socket!.emit(
'error',
new SocksProxyConnectionError(
this._proxy,
'Authentication is required, but not provided',
),
2021-05-23 12:27:16 +03:00
)
break
}
try {
2023-09-24 01:32:22 +03:00
this._socket!.write(buildSocks5Auth(this._proxy.user, this._proxy.password))
2021-05-23 12:27:16 +03:00
state = 'auth'
} catch (e) {
this._socket!.emit('error', e)
}
break
case 0xFF:
2021-05-23 12:27:16 +03:00
default:
// "no acceptable methods were offered"
this._socket!.emit(
'error',
new SocksProxyConnectionError(
this._proxy,
'Authentication is required, but not provided/supported',
),
2021-05-23 12:27:16 +03:00
)
break
}
break
}
case 'auth':
if (msg[0] !== 0x01) {
// VER of auth, must be 1
this._socket!.emit(
'error',
2023-09-24 01:32:22 +03:00
new SocksProxyConnectionError(this._proxy, `Server returned version ${msg[0]}`),
2021-05-23 12:27:16 +03:00
)
2021-05-23 12:27:16 +03:00
return
}
2023-09-24 01:32:22 +03:00
this.log.debug('[%s:%d] AUTH returned code %d', this._proxy.host, this._proxy.port, msg[1])
2021-05-23 12:27:16 +03:00
if (msg[1] === 0x00) {
// success
sendConnect()
} else {
this._socket!.emit(
'error',
2023-09-24 01:32:22 +03:00
new SocksProxyConnectionError(this._proxy, 'Authentication failure'),
2021-05-23 12:27:16 +03:00
)
}
break
2021-05-23 12:27:16 +03:00
case 'connect': {
if (msg[0] !== 0x05) {
// VER, must be 5
this._socket!.emit(
'error',
2023-09-24 01:32:22 +03:00
new SocksProxyConnectionError(this._proxy, `Server returned version ${msg[0]}`),
2021-05-23 12:27:16 +03:00
)
2021-05-23 12:27:16 +03:00
return
}
const code = msg[1]
2023-09-24 01:32:22 +03:00
this.log.debug('[%s:%d] CONNECT returned code %d', this._proxy.host, this._proxy.port, code)
2021-05-23 12:27:16 +03:00
if (code === 0x00) {
// Request granted
this._socket!.off('data', packetHandler)
this._socket!.on('data', data => this._packetCodec.feed(data))
2021-05-23 12:27:16 +03:00
this.handleConnect()
} else {
const msg
= code in SOCKS5_ERRORS
? SOCKS5_ERRORS[code]
: `Unknown error code: 0x${code.toString(16)}`
2023-09-24 01:32:22 +03:00
this._socket!.emit('error', new SocksProxyConnectionError(this._proxy, msg))
}
break
}
default:
assertNever(state)
}
2021-05-23 12:27:16 +03:00
}
2023-09-24 01:32:22 +03:00
this.log.debug('[%s:%d] connected to proxy, sending GREETING', this._proxy.host, this._proxy.port)
2021-05-23 12:27:16 +03:00
try {
2023-09-24 01:32:22 +03:00
this._socket!.write(buildSocks5Greeting(Boolean(this._proxy.user && this._proxy.password)))
2021-05-23 12:27:16 +03:00
} catch (e) {
this._socket!.emit('error', e)
}
}
this._socket!.on('data', packetHandler)
}
}
2022-08-29 16:22:57 +03:00
/**
* Socks TCP transport using an intermediate packet codec.
*
* Should be the one passed as `transport` to `TelegramClient` constructor
2022-08-29 16:22:57 +03:00
* (unless you want to use a custom codec).
*/
2021-07-14 21:08:13 +03:00
export class SocksTcpTransport extends BaseSocksTcpTransport {
2024-08-18 09:31:23 +03:00
_packetCodec: IntermediatePacketCodec = new IntermediatePacketCodec()
2021-05-23 12:27:16 +03:00
}