singbox.ts 2.9 KB

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