Skip to content

Commit

Permalink
feat: add Tuic support for Clash (Stash) and Surge
Browse files Browse the repository at this point in the history
  • Loading branch information
geekdada committed Oct 21, 2022
1 parent 0bd2ba2 commit 5bf51a6
Show file tree
Hide file tree
Showing 10 changed files with 199 additions and 19 deletions.
2 changes: 1 addition & 1 deletion lib/generator/artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { Environment } from 'nunjucks';
import path from 'path';
import { EventEmitter } from 'events';
import { deprecate } from 'util';
import { DEP009 } from '../misc/deprecation';

import { DEP009 } from '../misc/deprecation';
import { getProvider } from '../provider';
import {
ArtifactConfig,
Expand Down
21 changes: 20 additions & 1 deletion lib/provider/ClashProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
SnellNodeConfig,
SubscriptionUserinfo,
TrojanNodeConfig,
TuicNodeConfig,
VmessNodeConfig,
} from '../types';
import { lowercaseHeaderKeys } from '../utils';
Expand All @@ -28,7 +29,8 @@ type SupportConfigTypes =
| HttpNodeConfig
| ShadowsocksrNodeConfig
| SnellNodeConfig
| TrojanNodeConfig;
| TrojanNodeConfig
| TuicNodeConfig;

const logger = createLogger({
service: 'surgio:ClashProvider',
Expand Down Expand Up @@ -360,6 +362,23 @@ export const parseClashConfig = (
} as TrojanNodeConfig;
}

case 'tuic': {
return {
type: NodeTypeEnum.Tuic,
nodeName: item.name,
hostname: item.server,
port: item.port,
token: item.token,
'udp-relay': resolveUdpRelay(item.udp, udpRelay),
...('skip-cert-verify' in item
? { skipCertVerify: item['skip-cert-verify'] === true }
: null),
tls13: tls13 ?? false,
...('sni' in item ? { sni: item.sni } : null),
...('alpn' in item ? { alpn: item.alpn } : null),
} as TuicNodeConfig;
}

default:
logger.warn(
`不支持从 Clash 订阅中读取 ${item.type} 的节点,节点 ${item.name} 会被省略`,
Expand Down
3 changes: 3 additions & 0 deletions lib/provider/CustomProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ export default class CustomProvider extends Provider {
binPath: Joi.string(),
localPort: Joi.number(),
underlyingProxy: Joi.string(),
skipCertVerify: Joi.boolean().strict(),
sni: Joi.string(),
alpn: Joi.array().items(Joi.string()),
}).unknown();
const schema = Joi.object({
nodeList: Joi.array().items(nodeSchema).required(),
Expand Down
38 changes: 24 additions & 14 deletions lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export enum NodeTypeEnum {
Vmess = 'vmess',
Trojan = 'trojan',
Socks5 = 'socks5',
Tuic = 'tuic',
}

export enum SupportProviderEnum {
Expand Down Expand Up @@ -60,7 +61,8 @@ export interface CommandConfig {
readonly vmessAEAD?: boolean;
};
readonly clashConfig?: {
readonly ssrFormat: 'native' | 'legacy';
readonly ssrFormat?: 'native' | 'legacy';
readonly enableTuic?: boolean;
};
readonly surfboardConfig?: {
readonly vmessAEAD?: boolean;
Expand Down Expand Up @@ -186,10 +188,8 @@ export interface HttpNodeConfig extends SimpleNodeConfig {
readonly password: string;
}

export interface HttpsNodeConfig extends SimpleNodeConfig {
export interface HttpsNodeConfig extends TlsNodeConfig {
readonly type: NodeTypeEnum.HTTPS;
readonly hostname: string;
readonly port: number | string;
readonly username: string;
readonly password: string;
readonly tls13?: boolean;
Expand Down Expand Up @@ -253,21 +253,21 @@ export interface VmessNodeConfig extends SimpleNodeConfig {
readonly wsHeaders?: Record<string, string>;
}

export interface TrojanNodeConfig extends SimpleNodeConfig {
export interface TrojanNodeConfig extends TlsNodeConfig {
readonly type: NodeTypeEnum.Trojan;
readonly hostname: string;
readonly port: number | string;
readonly password: string;
readonly skipCertVerify?: boolean;
readonly alpn?: ReadonlyArray<string>;
readonly sni?: string;
readonly 'udp-relay'?: boolean;
readonly tls13?: boolean;
readonly network?: 'tcp' | 'ws';
readonly wsPath?: string;
readonly wsHeaders?: Record<string, string>;
}

export interface TuicNodeConfig extends TlsNodeConfig {
readonly type: NodeTypeEnum.Tuic;
readonly token: string;
readonly 'udp-relay'?: boolean;
}

export interface Socks5NodeConfig extends SimpleNodeConfig {
readonly type: NodeTypeEnum.Socks5;
readonly hostname: string;
Expand All @@ -284,11 +284,11 @@ export interface Socks5NodeConfig extends SimpleNodeConfig {
export interface SimpleNodeConfig {
readonly type: NodeTypeEnum;
nodeName: string;
readonly enable?: boolean;
enable?: boolean;

tfo?: boolean; // TCP Fast Open

mptcp?: boolean; // Multi-Path TCP

binPath?: string;
localPort?: number;
surgeConfig?: CommandConfig['surgeConfig'];
Expand All @@ -301,6 +301,15 @@ export interface SimpleNodeConfig {
testUrl?: string;
}

export interface TlsNodeConfig extends SimpleNodeConfig {
readonly hostname: string;
readonly port: number | string;
readonly tls13?: boolean;
readonly skipCertVerify?: boolean;
readonly sni?: string;
readonly alpn?: ReadonlyArray<string>;
}

export interface PlainObject {
readonly [name: string]: any;
}
Expand Down Expand Up @@ -340,7 +349,8 @@ export type PossibleNodeConfigType =
| SnellNodeConfig
| VmessNodeConfig
| TrojanNodeConfig
| Socks5NodeConfig;
| Socks5NodeConfig
| TuicNodeConfig;

export type ProxyGroupModifier = (
nodeList: ReadonlyArray<PossibleNodeConfigType>,
Expand Down
61 changes: 61 additions & 0 deletions lib/utils/__tests__/clash.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,4 +515,65 @@ test('getClashNodes', async (t) => {
},
],
);

t.deepEqual(
clash.getClashNodes([
{
nodeName: 'tuic',
type: NodeTypeEnum.Tuic,
hostname: '1.1.1.1',
port: 443,
token: 'password',
},
]),
[],
);

t.deepEqual(
clash.getClashNodes([
{
nodeName: 'tuic',
type: NodeTypeEnum.Tuic,
clashConfig: {
enableTuic: true,
},
hostname: '1.1.1.1',
port: 443,
token: 'password',
},
{
nodeName: 'tuic',
type: NodeTypeEnum.Tuic,
clashConfig: {
enableTuic: true,
},
hostname: '1.1.1.1',
port: 443,
token: 'password',
'udp-relay': false,
skipCertVerify: true,
alpn: ['h3'],
},
]),
[
{
type: 'tuic',
name: 'tuic',
server: '1.1.1.1',
port: 443,
token: 'password',
'skip-cert-verify': false,
},
{
type: 'tuic',
name: 'tuic',
server: '1.1.1.1',
port: 443,
token: 'password',
'skip-cert-verify': true,
udp: false,
alpn: ['h3'],
},
],
);
});
35 changes: 35 additions & 0 deletions lib/utils/__tests__/surge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,4 +477,39 @@ test('getSurgeNodes', async (t) => {
]),
'测试 6 = vmess, 1.1.1.1, 8080, username=1386f85e-657b-4d6e-9d56-78badb75e1fd, ws=true, ws-path=/, ws-headers="host:1.1.1.1|user-agent:Mozilla/5.0 (iPhone; CPU iPhone OS 13_5 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.1.1 Mobile/15E148 Safari/604.1", tls=true, tls13=true, skip-cert-verify=true, tfo=true, mptcp=true, test-url=http://www.google.com, vmess-aead=false',
);

t.is(
surge.getSurgeNodes([
{
type: NodeTypeEnum.Tuic,
nodeName: '测试 Tuic',
hostname: 'example.com',
port: 443,
token: 'token',
},
{
type: NodeTypeEnum.Tuic,
nodeName: '测试 Tuic',
hostname: 'example.com',
port: 443,
token: 'token',
alpn: ['h3'],
},
{
type: NodeTypeEnum.Tuic,
nodeName: '测试 Tuic',
hostname: 'example.com',
port: 443,
token: 'token',
alpn: ['h3'],
sni: 'sni.example.com',
skipCertVerify: true,
},
]),
[
'测试 Tuic = tuic, example.com, 443, token=token',
'测试 Tuic = tuic, example.com, 443, token=token, alpn=h3',
'测试 Tuic = tuic, example.com, 443, token=token, sni=sni.example.com, alpn=h3, skip-cert-verify=true',
].join('\n'),
);
});
30 changes: 28 additions & 2 deletions lib/utils/clash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ export const getClashNodes = function (
: null),
};

case NodeTypeEnum.Socks5: {
case NodeTypeEnum.Socks5:
return {
type: 'socks5',
name: nodeConfig.nodeName,
Expand All @@ -218,7 +218,33 @@ export const getClashNodes = function (
? { udp: nodeConfig.udpRelay }
: null),
};
}

case NodeTypeEnum.Tuic:
if (!nodeConfig.clashConfig?.enableTuic) {
logger.warn(
`默认不为 Clash 生成 Tuic 节点,节点 ${nodeConfig.nodeName} 会被省略。如需开启,请在配置文件中设置 clashConfig.enableTuic 为 true。`,
);
return null;
}
if (!nodeConfig.alpn || !nodeConfig.alpn.length) {
logger.warn(
`节点 ${nodeConfig.nodeName} 的 alpn 为空。Stash 客户端不支持 ALPN 为空,默认的 ALPN 为 h3。`,
);
}

return {
type: 'tuic',
name: nodeConfig.nodeName,
server: nodeConfig.hostname,
port: nodeConfig.port,
token: nodeConfig.token,
...(typeof nodeConfig['udp-relay'] === 'boolean'
? { udp: nodeConfig['udp-relay'] }
: null),
...(nodeConfig.alpn ? { alpn: nodeConfig.alpn } : null),
...(nodeConfig.sni ? { sni: nodeConfig.sni } : null),
'skip-cert-verify': nodeConfig.skipCertVerify === true,
};

// istanbul ignore next
default:
Expand Down
2 changes: 2 additions & 0 deletions lib/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export const normalizeConfig = (
},
clashConfig: {
ssrFormat: 'native',
enableTuic: false,
},
quantumultXConfig: {
vmessAEAD: true,
Expand Down Expand Up @@ -240,6 +241,7 @@ export const validateConfig = (userConfig: Partial<CommandConfig>): void => {
}).unknown(),
clashConfig: Joi.object({
ssrFormat: Joi.string().valid('native', 'legacy'),
enableTuic: Joi.bool().strict(),
}).unknown(),
analytics: Joi.boolean().strict(),
gateway: Joi.object({
Expand Down
2 changes: 1 addition & 1 deletion lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ export const generateClashProxyGroup = (
export const toYaml = (obj: JsonObject): string => YAML.stringify(obj);

export const pickAndFormatStringList = (
obj: any,
obj: Record<string, any>,
keyList: readonly string[],
): readonly string[] => {
const result: string[] = [];
Expand Down
24 changes: 24 additions & 0 deletions lib/utils/surge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import fs from 'fs-extra';
import _ from 'lodash';
import os from 'os';
import { join } from 'path';
import { Node } from 'yaml/types';
import { ERR_INVALID_FILTER, OBFS_UA } from '../constant';
import {
HttpNodeConfig,
Expand Down Expand Up @@ -451,6 +452,29 @@ export const getSurgeNodes = function (
return [nodeConfig.nodeName, config.join(', ')].join(' = ');
}

case NodeTypeEnum.Tuic: {
const config = [
'tuic',
nodeConfig.hostname,
nodeConfig.port,
...pickAndFormatStringList(nodeConfig, ['token', 'sni']),
...(Array.isArray(nodeConfig.alpn)
? [`alpn=${nodeConfig.alpn.join(',')}`]
: []),
...(typeof nodeConfig.underlyingProxy === 'string'
? [`underlying-proxy=${nodeConfig.underlyingProxy}`]
: []),
...(typeof nodeConfig.testUrl === 'string'
? [`test-url=${nodeConfig.testUrl}`]
: []),
...(typeof nodeConfig.skipCertVerify === 'boolean'
? [`skip-cert-verify=${nodeConfig.skipCertVerify}`]
: []),
];

return [nodeConfig.nodeName, config.join(', ')].join(' = ');
}

// istanbul ignore next
default:
logger.warn(
Expand Down

0 comments on commit 5bf51a6

Please sign in to comment.