singbox.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. import picocolors from 'picocolors';
  2. import { domainWildCardToRegex } from './misc';
  3. import { isProbablyIpv4, isProbablyIpv6 } from './is-fast-ip';
  4. const unsupported = Symbol('unsupported');
  5. const toNumberTuple = <T extends string>(key: T, value: string): [T, number] | null => {
  6. const tmp = Number(value);
  7. return Number.isNaN(tmp) ? null : [key, tmp];
  8. };
  9. // https://sing-box.sagernet.org/configuration/rule-set/source-format/
  10. const PROCESSOR: Record<string, ((raw: string, type: string, value: string) => [key: keyof SingboxHeadlessRule, value: Required<SingboxHeadlessRule>[keyof SingboxHeadlessRule][number]] | null) | typeof unsupported> = {
  11. DOMAIN: (_1, _2, value) => ['domain', value],
  12. 'DOMAIN-SUFFIX': (_1, _2, value) => ['domain_suffix', value],
  13. 'DOMAIN-KEYWORD': (_1, _2, value) => ['domain_keyword', value],
  14. 'DOMAIN-WILDCARD': (_1, _2, value) => ['domain_regex', domainWildCardToRegex(value)],
  15. GEOIP: unsupported,
  16. 'IP-CIDR': (_1, _2, value) => ['ip_cidr', value.endsWith(',no-resolve') ? value.slice(0, -11) : value],
  17. 'IP-CIDR6': (_1, _2, value) => ['ip_cidr', value.endsWith(',no-resolve') ? value.slice(0, -11) : value],
  18. 'IP-ASN': unsupported,
  19. 'SRC-IP': (_1, _2, value) => {
  20. if (value.includes('/')) {
  21. return ['source_ip_cidr', value];
  22. }
  23. if (isProbablyIpv4(value)) {
  24. return ['source_ip_cidr', value + '/32'];
  25. }
  26. if (isProbablyIpv6(value)) {
  27. return ['source_ip_cidr', value + '/128'];
  28. }
  29. return null;
  30. },
  31. 'SRC-IP-CIDR': (_1, _2, value) => ['source_ip_cidr', value.endsWith(',no-resolve') ? value.slice(0, -11) : value],
  32. 'SRC-PORT': (_1, _2, value) => toNumberTuple('source_port', value),
  33. 'DST-PORT': (_1, _2, value) => toNumberTuple('port', value),
  34. 'PROCESS-NAME': (_1, _2, value) => ((value.includes('/') || value.includes('\\')) ? ['process_path', value] : ['process_name', value]),
  35. // 'PROCESS-PATH': (_1, _2, value) => ['process_path', value],
  36. 'DEST-PORT': (_1, _2, value) => toNumberTuple('port', value),
  37. 'IN-PORT': (_1, _2, value) => toNumberTuple('source_port', value),
  38. 'URL-REGEX': unsupported,
  39. 'USER-AGENT': unsupported
  40. };
  41. interface SingboxHeadlessRule {
  42. domain?: string[],
  43. domain_suffix?: string[],
  44. domain_keyword?: string[],
  45. domain_regex?: string[],
  46. source_ip_cidr?: string[],
  47. ip_cidr?: string[],
  48. source_port?: number[],
  49. source_port_range?: string[],
  50. port?: number[],
  51. port_range?: string[],
  52. process_name?: string[],
  53. process_path?: string[]
  54. }
  55. interface SingboxSourceFormat {
  56. version: 2 | number & {},
  57. rules: SingboxHeadlessRule[]
  58. }
  59. export const surgeRulesetToSingbox = (rules: string[] | Set<string>): SingboxSourceFormat => {
  60. const rule: SingboxHeadlessRule = Array.from(rules).reduce<SingboxHeadlessRule>((acc, cur) => {
  61. let buf = '';
  62. let type = '';
  63. let i = 0;
  64. for (const len = cur.length; i < len; i++) {
  65. if (cur[i] === ',') {
  66. type = buf;
  67. break;
  68. }
  69. buf += cur[i];
  70. }
  71. if (type === '') {
  72. return acc;
  73. }
  74. const value = cur.slice(i + 1);
  75. if (type in PROCESSOR) {
  76. const proc = PROCESSOR[type];
  77. if (proc !== unsupported) {
  78. const r = proc(cur, type, value);
  79. if (r) {
  80. const [k, v] = r;
  81. acc[k] ||= [];
  82. (acc[k] as any).push(v);
  83. }
  84. }
  85. } else {
  86. console.log(picocolors.yellow(`[sing-box] unknown rule type: ${type}`), cur);
  87. }
  88. return acc;
  89. }, {});
  90. return {
  91. version: 2,
  92. rules: [rule]
  93. };
  94. };
  95. export const surgeDomainsetToSingbox = (domainset: string[]) => {
  96. const rule = domainset.reduce((acc, cur) => {
  97. if (cur[0] === '.') {
  98. acc.domain_suffix.push(cur.slice(1));
  99. } else {
  100. acc.domain.push(cur);
  101. }
  102. return acc;
  103. }, { domain: [] as string[], domain_suffix: [] as string[] } satisfies SingboxHeadlessRule);
  104. return {
  105. version: 2,
  106. rules: [rule]
  107. };
  108. };
  109. export const ipCidrListToSingbox = (ipCidrList: string[]): SingboxSourceFormat => {
  110. return {
  111. version: 2,
  112. rules: [{
  113. ip_cidr: ipCidrList
  114. }]
  115. };
  116. };