singbox.ts 3.0 KB

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