Skip to content

Commit

Permalink
feat: support trojan websocket for loon
Browse files Browse the repository at this point in the history
  • Loading branch information
geekdada committed Mar 19, 2022
1 parent 5c745fe commit 63e66d6
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 28 deletions.
2 changes: 1 addition & 1 deletion lib/provider/__tests__/ClashProvider.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import test from 'ava';
import nock from 'nock';
import { NodeTypeEnum, SupportProviderEnum } from '../../types';

import { NodeTypeEnum, SupportProviderEnum } from '../../types';
import { RELAY_SERVICE } from '../../constant';
import ClashProvider, {
getClashSubscription,
Expand Down
2 changes: 1 addition & 1 deletion lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ export interface TrojanNodeConfig extends SimpleNodeConfig {
readonly sni?: string;
readonly 'udp-relay'?: boolean;
readonly tls13?: boolean;
readonly network?: 'default' | 'ws';
readonly network?: 'tcp' | 'ws';
readonly wsPath?: string;
readonly wsHeaders?: Record<string, string>;
}
Expand Down
34 changes: 28 additions & 6 deletions lib/utils/__tests__/loon.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ test('getLoonNodes', (t) => {
uuid: '1386f85e-657b-4d6e-9d56-78badb75e1fd',
},
]),
'测试 = vmess,1.1.1.1,443,method=chacha20-ietf-poly1305,"1386f85e-657b-4d6e-9d56-78badb75e1fd",transport:tcp,over-tls:true,tls-name:1.1.1.1,skip-cert-verify:false',
'测试 = vmess,1.1.1.1,443,method=chacha20-ietf-poly1305,"1386f85e-657b-4d6e-9d56-78badb75e1fd",transport=tcp,over-tls=true,tls-name=1.1.1.1,skip-cert-verify=false',
);
t.is(
getLoonNodes([
Expand All @@ -38,7 +38,7 @@ test('getLoonNodes', (t) => {
protoparam: '',
},
]),
'🇭🇰HK = ShadowsocksR,hk.example.com,10000,chacha20-ietf,"password",auth_aes128_md5,{},tls1.2_ticket_auth,{music.163.com}',
'🇭🇰HK = ShadowsocksR,hk.example.com,10000,chacha20-ietf,"password",protocol=auth_aes128_md5,protocol-param=,obfs=tls1.2_ticket_auth,obfs-param=music.163.com',
);
t.is(
getLoonNodes([
Expand All @@ -52,7 +52,7 @@ test('getLoonNodes', (t) => {
password: 'nndndnd',
},
]),
'test = https,a.com,443,snsms,"nndndnd"',
'test = https,a.com,443,snsms,"nndndnd",tls-name=a.com,skip-cert-verify=false',
);
t.is(
getLoonNodes([
Expand All @@ -64,7 +64,7 @@ test('getLoonNodes', (t) => {
password: 'password1',
},
]),
'trojan = trojan,example.com,443,"password1",tls-name:example.com,skip-cert-verify:false',
'trojan = trojan,example.com,443,"password1",tls-name=example.com,skip-cert-verify=false',
);
t.is(
getLoonNodes([
Expand All @@ -79,7 +79,7 @@ test('getLoonNodes', (t) => {
tfo: true,
},
]),
'trojan = trojan,example.com,443,"password1",tls-name:example.com,skip-cert-verify:true',
'trojan = trojan,example.com,443,"password1",tls-name=example.com,skip-cert-verify=true',
);
t.is(
getLoonNodes([
Expand All @@ -96,7 +96,29 @@ test('getLoonNodes', (t) => {
tls13: true,
},
]),
'trojan = trojan,example.com,443,"password1",tls-name:sni.example.com,skip-cert-verify:true',
'trojan = trojan,example.com,443,"password1",tls-name=sni.example.com,skip-cert-verify=true',
);
t.is(
getLoonNodes([
{
type: NodeTypeEnum.Trojan,
nodeName: 'trojan',
hostname: 'example.com',
port: 443,
password: 'password1',
sni: 'sni.example.com',
'udp-relay': true,
skipCertVerify: true,
tfo: true,
tls13: true,
network: 'ws',
wsPath: '/ws',
wsHeaders: {
host: 'example.com',
},
},
]),
'trojan = trojan,example.com,443,"password1",tls-name=sni.example.com,skip-cert-verify=true,transport=ws,path=/ws,host=example.com',
);
});

Expand Down
88 changes: 68 additions & 20 deletions lib/utils/loon.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { createLogger } from '@surgio/logger';
import _ from 'lodash';

import {
NodeNameFilterType,
Expand All @@ -9,7 +10,7 @@ import {
import { ERR_INVALID_FILTER } from '../constant';
import { applyFilter } from './filter';

const logger = createLogger({ service: 'surgio:utils' });
const logger = createLogger({ service: 'surgio:utils:loon' });

// @see https://www.notion.so/1-9809ce5acf524d868affee8dd5fc0a6e
export const getLoonNodes = function (
Expand Down Expand Up @@ -46,6 +47,14 @@ export const getLoonNodes = function (
}
}

if (nodeConfig.tfo) {
config.push('fast-open=true');
}

if (nodeConfig['udp-relay']) {
config.push('udp=true');
}

return config.join(',');
}

Expand All @@ -56,12 +65,20 @@ export const getLoonNodes = function (
nodeConfig.port,
nodeConfig.method,
JSON.stringify(nodeConfig.password),
nodeConfig.protocol,
`{${nodeConfig.protoparam}}`,
nodeConfig.obfs,
`{${nodeConfig.obfsparam}}`,
`protocol=${nodeConfig.protocol}`,
`protocol-param=${nodeConfig.protoparam}`,
`obfs=${nodeConfig.obfs}`,
`obfs-param=${nodeConfig.obfsparam}`,
];

if (nodeConfig.tfo) {
config.push('fast-open=true');
}

if (nodeConfig['udp-relay']) {
config.push('udp=true');
}

return config.join(',');
}

Expand All @@ -74,21 +91,27 @@ export const getLoonNodes = function (
? `method=chacha20-ietf-poly1305`
: `method=${nodeConfig.method}`,
JSON.stringify(nodeConfig.uuid),
`transport:${nodeConfig.network}`,
`transport=${nodeConfig.network}`,
];

if (nodeConfig.network === 'ws') {
config.push(
`path:${nodeConfig.path || '/'}`,
`host:${nodeConfig.host || nodeConfig.hostname}`,
`path=${nodeConfig.path || '/'}`,
`host=${nodeConfig.host || nodeConfig.hostname}`,
);

if (Object.keys(_.omit(nodeConfig.wsHeaders, 'host')).length > 0) {
logger.warn(
`Loon 不支持自定义额外的 Header 字段,节点 ${nodeConfig.nodeName} 可能不可用`,
);
}
}

if (nodeConfig.tls) {
config.push(
`over-tls:${nodeConfig.tls}`,
`tls-name:${nodeConfig.host || nodeConfig.hostname}`,
`skip-cert-verify:${nodeConfig.skipCertVerify === true}`,
`over-tls=${nodeConfig.tls}`,
`tls-name=${nodeConfig.host || nodeConfig.hostname}`,
`skip-cert-verify=${nodeConfig.skipCertVerify === true}`,
);
}

Expand All @@ -101,31 +124,56 @@ export const getLoonNodes = function (
nodeConfig.hostname,
nodeConfig.port,
JSON.stringify(nodeConfig.password),
`tls-name:${nodeConfig.sni || nodeConfig.hostname}`,
`skip-cert-verify:${nodeConfig.skipCertVerify === true}`,
`tls-name=${nodeConfig.sni || nodeConfig.hostname}`,
`skip-cert-verify=${nodeConfig.skipCertVerify === true}`,
];

if (nodeConfig.network === 'ws') {
config.push('transport=ws', `path=${nodeConfig.wsPath || '/'}`);

if (nodeConfig.wsHeaders) {
if (_.get(nodeConfig, 'wsHeaders.host')) {
config.push(`host=${nodeConfig.wsHeaders.host}`);
}

if (
Object.keys(_.omit(nodeConfig.wsHeaders, 'host')).length > 0
) {
logger.warn(
`Loon 不支持自定义额外的 Header 字段,节点 ${nodeConfig.nodeName} 可能不可用`,
);
}
}
}

return config.join(',');
}

case NodeTypeEnum.HTTPS:
return [
case NodeTypeEnum.HTTPS: {
const config: Array<string | number> = [
`${nodeConfig.nodeName} = https`,
nodeConfig.hostname,
nodeConfig.port,
nodeConfig.username /* istanbul ignore next */ || '',
JSON.stringify(nodeConfig.password) /* istanbul ignore next */ ||
'""',
].join(',');
JSON.stringify(
nodeConfig.password /* istanbul ignore next */ || '',
),
`tls-name=${nodeConfig.sni || nodeConfig.hostname}`,
`skip-cert-verify=${nodeConfig.skipCertVerify === true}`,
];

return config.join(',');
}

case NodeTypeEnum.HTTP:
return [
`${nodeConfig.nodeName} = http`,
nodeConfig.hostname,
nodeConfig.port,
nodeConfig.username /* istanbul ignore next */ || '',
JSON.stringify(nodeConfig.password) /* istanbul ignore next */ ||
'""',
JSON.stringify(
nodeConfig.password /* istanbul ignore next */ || '',
),
].join(',');

// istanbul ignore next
Expand Down
1 change: 1 addition & 0 deletions lib/utils/quantumult.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createLogger } from '@surgio/logger';
import _ from 'lodash';

import { ERR_INVALID_FILTER, OBFS_UA } from '../constant';
import {
HttpsNodeConfig,
Expand Down

0 comments on commit 63e66d6

Please sign in to comment.