blinksocks/src/presets/ss-base.js
2018-11-03 11:22:19 +08:00

210 lines
5.1 KiB
JavaScript

import net from 'net';
import ip from 'ip';
import { isValidHostname, numberToBuffer } from '../utils';
import { IPresetAddressing } from './defs';
const ATYP_V4 = 0x01;
const ATYP_V6 = 0x04;
const ATYP_DOMAIN = 0x03;
function getHostType(host) {
if (net.isIPv4(host)) {
return ATYP_V4;
}
if (net.isIPv6(host)) {
return ATYP_V6;
}
return ATYP_DOMAIN;
}
/**
* @description
* Deliver destination address.
*
* @examples
* {"name": "ss-base"}
*
* @protocol
*
* # TCP stream (client -> server)
* +------+----------+----------+----------+---------+
* | ATYP | DST.ADDR | DST.PORT | DATA | ... |
* +------+----------+----------+----------+---------+
* | 1 | Variable | 2 | Variable | ... |
* +------+----------+----------+----------+---------+
*
* # TCP chunks
* +----------+
* | DATA |
* +----------+
* | Variable |
* +----------+
*
* # UDP packet (client <-> server)
* +------+----------+----------+----------+
* | ATYP | DST.ADDR | DST.PORT | DATA |
* +------+----------+----------+----------+
* | 1 | Variable | 2 | Variable |
* +------+----------+----------+----------+
*
* @explain
* 1. ATYP is one of [0x01(ipv4), 0x03(hostname), 0x04(ipv6)].
* 2. If ATYP is 0x03, DST.ADDR[0] is len(DST.ADDR).
* 3. If ATYP is 0x04, DST.ADDR must be a 16 bytes ipv6 address.
*
* @reference
* https://shadowsocks.org/en/spec/Protocol.html
*/
export default class SsBasePreset extends IPresetAddressing {
_isConnecting = false;
_pending = Buffer.alloc(0);
_isHeaderSent = false;
_isHeaderRecv = false;
_atyp = ATYP_V4;
_host = null; // buffer
_port = null; // buffer
_headSize = 0;
get headSize() {
return this._headSize;
}
onInitTargetAddress({ host, port }) {
const type = getHostType(host);
this._atyp = type;
this._port = numberToBuffer(port);
this._host = type === ATYP_DOMAIN ? Buffer.from(host) : ip.toBuffer(host);
}
onDestroy() {
this._pending = null;
this._host = null;
this._port = null;
}
encodeHeader() {
const head = Buffer.from([
this._atyp,
...(this._atyp === ATYP_DOMAIN ? [this._host.length] : []),
...this._host,
...this._port
]);
this._headSize = head.length;
return head;
}
decodeHeader({ buffer, fail }) {
if (buffer.length < 7) {
return fail(`invalid length: ${buffer.length}`);
}
const atyp = buffer[0];
let addr; // string
let port; // number
let offset = 3;
switch (atyp) {
case ATYP_V4:
addr = ip.toString(buffer.slice(1, 5));
port = buffer.slice(5, 7).readUInt16BE(0);
offset += 4;
break;
case ATYP_V6:
if (buffer.length < 19) {
return fail(`invalid length: ${buffer.length}`);
}
addr = ip.toString(buffer.slice(1, 17));
port = buffer.slice(17, 19).readUInt16BE(0);
offset += 16;
break;
case ATYP_DOMAIN: {
const domainLen = buffer[1];
if (buffer.length < domainLen + 4) {
return fail(`invalid length: ${buffer.length}`);
}
addr = buffer.slice(2, 2 + domainLen).toString();
if (!isValidHostname(addr)) {
return fail(`addr=${addr} is an invalid hostname`);
}
port = buffer.slice(2 + domainLen, 4 + domainLen).readUInt16BE(0);
offset += (domainLen + 1);
break;
}
default:
return fail(`invalid atyp: ${atyp}`);
}
const data = buffer.slice(offset);
return { host: addr, port, data };
}
// tcp
clientOut({ buffer }) {
if (!this._isHeaderSent) {
this._isHeaderSent = true;
return Buffer.concat([this.encodeHeader(), buffer]);
} else {
return buffer;
}
}
serverIn({ buffer, next, fail }) {
if (!this._isHeaderRecv) {
// shadowsocks(python) aead cipher put [atyp][dst.addr][dst.port] into the first chunk
// we must wait onConnected() before next().
if (this._isConnecting) {
this._pending = Buffer.concat([this._pending, buffer]);
return;
}
const decoded = this.decodeHeader({ buffer, fail });
if (!decoded) {
return;
}
const { host, port, data } = decoded;
// notify to connect to the real server
this._isConnecting = true;
this.resolveTargetAddress({ host, port }, () => {
if (this._pending !== null) {
next(Buffer.concat([data, this._pending]));
}
this._isHeaderRecv = true;
this._isConnecting = false;
this._pending = null;
});
} else {
return buffer;
}
}
// udp
beforeOutUdp({ buffer }) {
return Buffer.concat([this.encodeHeader(), buffer]);
}
serverInUdp({ buffer, next, fail }) {
const decoded = this.decodeHeader({ buffer, fail });
if (!decoded) {
return;
}
const { host, port, data } = decoded;
this._atyp = getHostType(host);
this._host = this._atyp === ATYP_DOMAIN ? Buffer.from(host) : ip.toBuffer(host);
this._port = numberToBuffer(port);
this.resolveTargetAddress({ host, port }, () => next(data));
}
}