Quellcode durchsuchen

Editor: add proxy group docs

- Support `last_resort`
- Fix mmdb with file name
bdbai vor 3 Jahren
Ursprung
Commit
d9398c7514

+ 2 - 0
Maple.App/MonacoEditor/src/completion.ts

@@ -325,6 +325,8 @@ function completeProxyGroup(
             case facts.GROUP_PROPERTY_KEY_FALLBACK_CACHE:
             case facts.GROUP_PROPERTY_KEY_HEALTH_CHECK:
                 return generateBoolCandidates(range)
+            case facts.GROUP_PROPERTY_KEY_LAST_RESORT:
+                return collectProxyOrGroupSuggestions(model, struct, range)
         }
         return []
     }

+ 5 - 1
Maple.App/MonacoEditor/src/definition.ts

@@ -67,7 +67,11 @@ function provideDefinitionForProxyGroup(
     const currentArgId = kv.value.substring(0, position.column - kv.valueStartCol).split(',').length - 1
     const targetName = args[currentArgId].text
     if (targetName.includes('=')) {
-        return undefined
+        const argKv = parseKvLine(args[currentArgId].text, lineId, args[currentArgId].startCol)
+        if (argKv === undefined || argKv.key !== facts.GROUP_PROPERTY_KEY_LAST_RESORT) {
+            return undefined
+        }
+        return findProxyOrGroupKeyLocation(model, struct, argKv.value)
     }
     return findProxyOrGroupKeyLocation(model, struct, targetName)
 }

+ 29 - 29
Maple.App/MonacoEditor/src/facts.ts

@@ -49,8 +49,8 @@ export const GENERAL_SETTING_KEYS: IGeneralSettingDef[] = [
     { name: SETTING_LOGOUTPUT, desc: 'Path to the log file.', kind: 'other' },
     { name: SETTING_DNS_SERVER, desc: 'A comma separated list of DNS servers.', kind: 'other' },
     { name: SETTING_DNS_INTERFACE, desc: 'Specify on which interface DNS requests will be routed.', kind: 'interface' },
-    { name: SETTING_ALWAYS_REAL_IP, desc: 'A comma separated list of domain names that will always be resolved to real IP addresses.\n\nThis option conflicts with `' + SETTING_ALWAYS_FAKE_IP + '`', kind: 'other' },
-    { name: SETTING_ALWAYS_FAKE_IP, desc: 'A comma separated list of domain names that will always be resolved to fake IP addresses.\n\nThis option conflicts with `' + SETTING_ALWAYS_REAL_IP + '`', kind: 'other' },
+    { name: SETTING_ALWAYS_REAL_IP, desc: 'A comma separated list of domain name keywords that will always be resolved to real IP addresses.\n\nThis option conflicts with `' + SETTING_ALWAYS_FAKE_IP + '`', kind: 'other' },
+    { name: SETTING_ALWAYS_FAKE_IP, desc: 'A comma separated list of domain name keywords that will always be resolved to fake IP addresses.\n\nThis option conflicts with `' + SETTING_ALWAYS_REAL_IP + '`', kind: 'other' },
     { name: SETTING_ROUTING_DOMAIN_RESOLVE, desc: 'Specify whether Leaf should resolve IP addresses for routing.\n\nFor `GEOIP` and `IP-CIDR` rules to match domain name requests, this should be enabled.', kind: 'other' },
     { name: SETTING_HTTP_INTERFACE, desc: 'Specify on which interface HTTP inbound will be listening on.', kind: 'interface' },
     { name: SETTING_INTERFACE, desc: 'Specify on which interface HTTP inbound will be listening on.\n\nAlias for `' + SETTING_HTTP_INTERFACE + '`.', kind: 'interface' },
@@ -131,8 +131,8 @@ export const PROXY_PROPERTY_KEYS_DESC_MAP = new Map([
     [PROXY_PROPERTY_KEY_SNI, 'Server name (SNI), or host name for TLS transport.'],
     [PROXY_PROPERTY_KEY_QUIC, 'Specify whether QUIC transport should be enabled.'],
     [PROXY_PROPERTY_KEY_AMUX, 'Specify whether AMUX transport should be enabled.'],
-    [PROXY_PROPERTY_KEY_AMUX_MAX, 'Maximum number of connections per AMUX session.'],
-    [PROXY_PROPERTY_KEY_AMUX_CON, 'Maximum number of streams per AMUX session.'],
+    [PROXY_PROPERTY_KEY_AMUX_MAX, 'Maximum number of streams per AMUX session.'],
+    [PROXY_PROPERTY_KEY_AMUX_CON, 'Maximum number of concurrent connections per AMUX session.'],
     [PROXY_PROPERTY_KEY_INTERFACE, 'Specify the interface proxy requests should bind to.'],
 ])
 
@@ -209,17 +209,6 @@ export interface IProxyGroupDef {
     snippet: string,
 }
 
-export const GROUP_TYPES: IProxyGroupDef[] = [
-    { name: GROUP_TYPE_CHAIN, desc: '', snippet: 'chain, ${1:proxy1}, ${2:proxy2}, ${3:proxy3}' },
-    { name: GROUP_TYPE_TRYALL, desc: '', snippet: 'tryall, ${1:proxy1}, ${2:proxy2}, ${3:proxy3}' },
-    { name: GROUP_TYPE_STATIC, desc: '', snippet: 'static, ${1:proxy1}, ${2:proxy2}, ${3:proxy3}' },
-    { name: GROUP_TYPE_FAILOVER, desc: '', snippet: 'failover, ${1:proxy1}, ${2:proxy2}, ${3:proxy3}, health-check=${4|true,false|}, check-interval=${5:600}, fail-timeout=${6:5}, failover=${7|true,false|}' },
-    { name: GROUP_TYPE_FALLBACK, desc: '', snippet: 'fallback, ${1:proxy1}, ${2:proxy2}, ${3:proxy3}, check-interval=${4:600}, fail-timeout=${5:5}' },
-    { name: GROUP_TYPE_FAILOVER_URL_TEST, desc: '', snippet: 'url-test, ${1:proxy1}, ${2:proxy2}, ${3:proxy3}, check-interval=${4:600}, fail-timeout=${5:5}' },
-    { name: GROUP_TYPE_SELECT, desc: '', snippet: 'select, ${1:proxy1}, ${2:proxy2}, ${3:proxy3}' },
-]
-export const GROUP_TYPES_MAP: Map<string, IProxyGroupDef> = new Map(GROUP_TYPES.map(g => [g.name, g]))
-
 export const GROUP_PROPERTY_KEY_DELAY_BASE = 'delay-base'
 export const GROUP_PROPERTY_KEY_METHOD = 'method'
 export const GROUP_PROPERTY_KEY_FAIL_TIMEOUT = 'fail-timeout'
@@ -235,19 +224,19 @@ export const GROUP_PROPERTY_KEY_CACHE_TIMEOUT = 'cache-timeout'
 export const GROUP_PROPERTY_KEY_LAST_RESORT = 'last-resort'
 
 export const GROUP_PROPERTY_KEYS_DESC_MAP = new Map([
-    [GROUP_PROPERTY_KEY_DELAY_BASE, ''],
-    [GROUP_PROPERTY_KEY_METHOD, ''],
-    [GROUP_PROPERTY_KEY_FAIL_TIMEOUT, ''],
-    [GROUP_PROPERTY_KEY_HEALTH_CHECK, ''],
-    [GROUP_PROPERTY_KEY_HEALTH_CHECK_TIMEOUT, ''],
-    [GROUP_PROPERTY_KEY_HEALTH_CHECK_DELAY, ''],
-    [GROUP_PROPERTY_KEY_HEALTH_CHECK_ACTIVE, ''],
-    [GROUP_PROPERTY_KEY_CHECK_INTERVAL, ''],
-    [GROUP_PROPERTY_KEY_FAILOVER, ''],
-    [GROUP_PROPERTY_KEY_FALLBACK_CACHE, ''],
-    [GROUP_PROPERTY_KEY_CACHE_SIZE, ''],
-    [GROUP_PROPERTY_KEY_CACHE_TIMEOUT, ''],
-    [GROUP_PROPERTY_KEY_LAST_RESORT, ''],
+    [GROUP_PROPERTY_KEY_DELAY_BASE, 'Specify the interval before starting the next connection attempt when all previous attemps are pending.'],
+    [GROUP_PROPERTY_KEY_METHOD, 'Selection method.\n\n`random`: Randomly choose an actor for each request.\n\n`random-once`: Randomly choose an actor when Leaf starts.\n\n`rr`: Round-robin.'],
+    [GROUP_PROPERTY_KEY_FAIL_TIMEOUT, 'Timeout for an actor to establish a connection, including TCP handshake and TLS handshake and protocol-specific initialization.'],
+    [GROUP_PROPERTY_KEY_HEALTH_CHECK, 'Specify whether a health check should be performed periodically to measure latencies of the actors.'],
+    [GROUP_PROPERTY_KEY_HEALTH_CHECK_TIMEOUT, 'Timeout for an actor to establish a connection during a health check, including TCP handshake and TLS handshake and protocol-specific initialization.'],
+    [GROUP_PROPERTY_KEY_HEALTH_CHECK_DELAY, 'Delay before starting a health check.'],
+    [GROUP_PROPERTY_KEY_HEALTH_CHECK_ACTIVE, 'Specify the interval where health checks are skipped if there is no new connections.'],
+    [GROUP_PROPERTY_KEY_CHECK_INTERVAL, 'Specify the interval between health checks.'],
+    [GROUP_PROPERTY_KEY_FAILOVER, 'Specify whether to switch to the next actor when the current actor fails.'],
+    [GROUP_PROPERTY_KEY_FALLBACK_CACHE, 'Specify whether previous succeeded actors should be cached for future connections.'],
+    [GROUP_PROPERTY_KEY_CACHE_SIZE, 'Maximum number of actors to cache.'],
+    [GROUP_PROPERTY_KEY_CACHE_TIMEOUT, 'Cache timeout in **minutes**.'],
+    [GROUP_PROPERTY_KEY_LAST_RESORT, 'Specify the actor to use when all actors in the list fail.'],
 ])
 
 export const GROUP_METHOD_RANDOM = 'random'
@@ -257,6 +246,17 @@ export const GROUP_METHOD_ROUND_ROBIN = 'rr'
 export const KNOWN_GROUP_METHODS = [GROUP_METHOD_RANDOM, GROUP_METHOD_RANDOM_ONCE, GROUP_METHOD_ROUND_ROBIN]
 export const KNOWN_GROUP_METHODS_SET = new Set(KNOWN_GROUP_METHODS)
 
+export const GROUP_TYPES: IProxyGroupDef[] = [
+    { name: GROUP_TYPE_CHAIN, desc: 'Chaining proxies.', snippet: 'chain, ${1:proxy1}, ${2:proxy2}, ${3:proxy3}' },
+    { name: GROUP_TYPE_TRYALL, desc: 'Concurrently initiate connection attempts to all proxies in order with an optional delay.', snippet: 'tryall, ${1:proxy1}, ${2:proxy2}, ${3:proxy3}' },
+    { name: GROUP_TYPE_STATIC, desc: 'Select a proxy from the list by random or round robin.', snippet: 'static, ${1:proxy1}, ${2:proxy2}, ${3:proxy3}' },
+    { name: GROUP_TYPE_FAILOVER, desc: 'Try all proxies one-by-one until success. Periodic health checks are performed in background to monitor the status of the proxies.', snippet: 'failover, ${1:proxy1}, ${2:proxy2}, ${3:proxy3}, health-check=${4|true,false|}, check-interval=${5:600}, fail-timeout=${6:5}, failover=${7|true,false|}' },
+    { name: GROUP_TYPE_FALLBACK, desc: 'Try all proxies one-by-one until success. Periodic health checks are performed in background to monitor the status of the proxies.\n\nAlias for `' + GROUP_TYPE_FAILOVER + '`.', snippet: 'fallback, ${1:proxy1}, ${2:proxy2}, ${3:proxy3}, check-interval=${4:600}, fail-timeout=${5:5}' },
+    { name: GROUP_TYPE_FAILOVER_URL_TEST, desc: 'Select the best proxy from periodic health checks.\n\nEquivalent to `' + GROUP_TYPE_FAILOVER + '` with `' + GROUP_PROPERTY_KEY_FAILOVER + '`=`false`.', snippet: 'url-test, ${1:proxy1}, ${2:proxy2}, ${3:proxy3}, check-interval=${4:600}, fail-timeout=${5:5}' },
+    { name: GROUP_TYPE_SELECT, desc: 'Select a proxy through Leaf control API.', snippet: 'select, ${1:proxy1}, ${2:proxy2}, ${3:proxy3}' },
+]
+export const GROUP_TYPES_MAP: Map<string, IProxyGroupDef> = new Map(GROUP_TYPES.map(g => [g.name, g]))
+
 export const PROXY_GROUP_PROPERTY_KEY_MAP: Record<string, IProxyPropertyKeyDef> = {
     [GROUP_TYPE_CHAIN]: { required: new Set(), allowed: new Set() },
     [GROUP_TYPE_TRYALL]: { required: new Set(), allowed: new Set([GROUP_PROPERTY_KEY_DELAY_BASE]) },
@@ -332,7 +332,7 @@ export const RULE_TYPES: IRuleTypeDef[] = [
     { name: RULE_TYPE_DOMAIN_SUFFIX, desc: 'Match connections with destination domain names that end with the specified string.', snippet: 'DOMAIN-SUFFIX, ${1:example.com}, ${2:proxy}' },
     { name: RULE_TYPE_DOMAIN_KEYWORD, desc: 'Match connections with destination domain names that contain the specified keyword.', snippet: 'DOMAIN-KEYWORD, ${1:keyword}, ${2:proxy}' },
     { name: RULE_TYPE_GEOIP, desc: 'Match connections with destination IP addresses located within the specified country.\n\nMake sure a valid GeoIP database file `geo.mmdb` exists in the configuration folder.\n\nTo match domain name requests, enable `' + SETTING_ROUTING_DOMAIN_RESOLVE + '` in General section.', snippet: 'GEOIP, ${1:us}, ${2:proxy}' },
-    { name: RULE_TYPE_EXTERNAL, desc: 'Match connections using an external GeoIP or V2Ray geosite database file.\n\nV2Ray geosite: `site:<file>:<group>` or `site:<group>` with database file default to "site.dat".\n\nGeoIP: `mmdb:<country code>`. Make sure a valid GeoIP database file `geo.mmdb` exists in the configuration folder.', snippet: 'EXTERNAL, ${1|site:geolocation-cn,site:geosite.dat:category-ads-all,mmdb:cn|}, ${2:proxy}' },
+    { name: RULE_TYPE_EXTERNAL, desc: 'Match connections using an external GeoIP or V2Ray geosite database file.\n\nV2Ray geosite: `site:<file>:<group>` or `site:<group>` with database file default to "site.dat".\n\nGeoIP: `mmdb:<country code>`. Make sure a valid GeoIP database file `geo.mmdb` exists in the configuration folder.', snippet: 'EXTERNAL, ${1|site:geolocation-cn,site:geosite.dat:category-ads-all,mmdb:cn,mmdb:geo.mmdb:cn|}, ${2:proxy}' },
     { name: RULE_TYPE_PORT_RANGE, desc: 'Match connections with destination ports within the range.\n\nThe port range is specified by a lower bound and a upper bound, separated by a dash ("`").', snippet: 'PORT-RANGE, ${1:8000-9000}, ${2:proxy}' },
     { name: RULE_TYPE_NETWORK, desc: 'Match TCP or UDP requests.', snippet: 'NETWORK, ${1|TCP,UDP|}, ${2:proxy}' },
     { name: RULE_TYPE_INBOUND_TAG, desc: 'Match connections with the specified inbound tag.', snippet: 'INBOUND-TAG, ${1:inbound-tag}, ${2:proxy}' },

+ 21 - 10
Maple.App/MonacoEditor/src/hover.ts

@@ -260,24 +260,35 @@ function provideProxyGroupHover(
             currentArg.startCol + currentArg.text.length,
         ))
     }
-    if (colId < argKv.keyStartCol || colId > argKv.keyStartCol + argKv.key.length) {
+    if (colId < argKv.keyStartCol) {
         return undefined
     }
-    const desc = facts.GROUP_PROPERTY_KEYS_DESC_MAP.get(argKv.key)
-    if (desc === undefined) {
+    if (colId < argKv.valueStartCol) {
+        const desc = facts.GROUP_PROPERTY_KEYS_DESC_MAP.get(argKv.key)
+        if (desc === undefined) {
+            return {
+                range: new monaco.Range(lineId, argKv.keyStartCol, lineId, argKv.keyStartCol + argKv.key.length),
+                contents: [
+                    { value: '(property) ' + argKv.key },
+                ],
+            }
+        }
         return {
             range: new monaco.Range(lineId, argKv.keyStartCol, lineId, argKv.keyStartCol + argKv.key.length),
             contents: [
                 { value: '(property) ' + argKv.key },
+                { value: desc },
             ],
         }
-    }
-    return {
-        range: new monaco.Range(lineId, argKv.keyStartCol, lineId, argKv.keyStartCol + argKv.key.length),
-        contents: [
-            { value: '(property) ' + argKv.key },
-            { value: desc },
-        ],
+    } else if (argKv.key === facts.GROUP_PROPERTY_KEY_LAST_RESORT) {
+        return provideProxyOrGroupNameHover(model, struct, argKv.value, new monaco.Range(
+            lineId,
+            argKv.valueStartCol,
+            lineId,
+            argKv.valueStartCol + argKv.value.length,
+        ))
+    } else {
+        return undefined
     }
 }
 

+ 16 - 0
Maple.App/MonacoEditor/src/reference.ts

@@ -39,6 +39,22 @@ function findProxyOrGroupReferenceLocations(
                     range: new monaco.Range(kv.lineId, kv.keyStartCol, kv.lineId, kv.keyStartCol + kv.key.length),
                 })
             }
+            for (const arg of args
+                .map(a => parseKvLine(a.text, kv.lineId, a.startCol))
+                .filter((kv): kv is ILeafConfKvItem =>
+                    kv?.key === facts.GROUP_PROPERTY_KEY_LAST_RESORT
+                    && kv.value === targetName)
+            ) {
+                ret.push({
+                    uri: model.uri,
+                    range: new monaco.Range(
+                        arg.lineId,
+                        arg.valueStartCol,
+                        arg.lineId,
+                        arg.valueStartCol + arg.value.length,
+                    ),
+                })
+            }
             return ret
         })
     const ruleLocations = struct.sections.filter(s => s.sectionName === facts.SECTION_RULE)

+ 40 - 26
Maple.App/MonacoEditor/src/validate.ts

@@ -475,6 +475,7 @@ function validateProxyGroupItem(
     }
     const argsWithKv = args.slice(firstKvArgId)
     const visitedKvs: Map<string, ILeafConfKvItem> = new Map()
+    const lastResortSegs: ILeafConfTextSpan[] = []
     const requiredVisited = new Map([...typeKeyDef.required].map(k => [k, false]))
     for (const arg of argsWithKv) {
         const kv = parseKvLine(arg.text, item.lineId, arg.startCol)
@@ -543,6 +544,10 @@ function validateProxyGroupItem(
                 validateI32(kv.value, item.lineId, kv.valueStartCol, errors)
                 break
             case facts.GROUP_PROPERTY_KEY_LAST_RESORT:
+                lastResortSegs.push({
+                    text: kv.value,
+                    startCol: kv.valueStartCol,
+                })
                 break
             case facts.GROUP_PROPERTY_KEY_METHOD:
                 if (!facts.KNOWN_GROUP_METHODS_SET.has(kv.value)) {
@@ -593,8 +598,8 @@ function validateProxyGroupItem(
         }
     }
 
-    const argsWithoutKv = args.slice(1, firstKvArgId)
-    if (argsWithoutKv.length === 0) {
+    const actorSegs = args.slice(1, firstKvArgId).concat(lastResortSegs)
+    if (actorSegs.length === 0) {
         errors.push({
             severity: monaco.MarkerSeverity.Error,
             startLineNumber: item.lineId,
@@ -604,7 +609,7 @@ function validateProxyGroupItem(
             message: `A proxy group must have at least one actor.`,
         })
     }
-    for (const arg of argsWithoutKv) {
+    for (const arg of actorSegs) {
         if (arg.text === '') {
             errors.push({
                 severity: monaco.MarkerSeverity.Error,
@@ -884,7 +889,7 @@ function validateRules(
                                 })
                                 continue
                             }
-                            const segs = ruleItem.text.split(':')
+                            const segs = ruleItem.text.split(':').map(s => s.trim())
                             if (segs.length < 2) {
                                 errors.push({
                                     severity: monaco.MarkerSeverity.Error,
@@ -896,18 +901,33 @@ function validateRules(
                                 })
                                 continue
                             }
+                            if (segs.length > 3) {
+                                errors.push({
+                                    severity: monaco.MarkerSeverity.Error,
+                                    startLineNumber: currLineId,
+                                    startColumn: ruleItem.startCol,
+                                    endLineNumber: currLineId,
+                                    endColumn: ruleItem.startCol + ruleItem.text.length,
+                                    message: `An external site rule cannot have more than three components.`,
+                                })
+                            }
+
                             if (segs[0] === facts.RULE_EXTERNAL_SOURCE_MMDB) {
+                                let codeSegId = 1
                                 if (segs.length > 2) {
-                                    errors.push({
-                                        severity: monaco.MarkerSeverity.Error,
-                                        startLineNumber: currLineId,
-                                        startColumn: ruleItem.startCol,
-                                        endLineNumber: currLineId,
-                                        endColumn: ruleItem.startCol + ruleItem.text.length,
-                                        message: `An external MMDB rule cannot have more than two components.`,
-                                    })
+                                    codeSegId = 2
+                                    if (segs[1] === '') {
+                                        errors.push({
+                                            severity: monaco.MarkerSeverity.Error,
+                                            startLineNumber: currLineId,
+                                            startColumn: ruleItem.startCol,
+                                            endLineNumber: currLineId,
+                                            endColumn: ruleItem.startCol + ruleItem.text.length,
+                                            message: `Empty GeoIP database file name.`,
+                                        })
+                                    }
                                 }
-                                if (segs[1] === '') {
+                                if (segs[codeSegId] === '') {
                                     errors.push({
                                         severity: monaco.MarkerSeverity.Error,
                                         startLineNumber: currLineId,
@@ -918,17 +938,10 @@ function validateRules(
                                     })
                                 }
                             } else if (segs[0] === facts.RULE_EXTERNAL_SOURCE_SITE) {
-                                if (segs.length > 3) {
-                                    errors.push({
-                                        severity: monaco.MarkerSeverity.Error,
-                                        startLineNumber: currLineId,
-                                        startColumn: ruleItem.startCol,
-                                        endLineNumber: currLineId,
-                                        endColumn: ruleItem.startCol + ruleItem.text.length,
-                                        message: `An external site rule cannot have more than three components.`,
-                                    })
-                                }
-                                if (segs.length === 3 && segs[1] === '') {
+                                let groupSegId = 1
+                                if (segs.length > 2 && segs[1] === '') {
+                                    groupSegId = 2
+                                    if (segs[1] === '') {
                                     errors.push({
                                         severity: monaco.MarkerSeverity.Error,
                                         startLineNumber: currLineId,
@@ -938,7 +951,8 @@ function validateRules(
                                         message: `An external site rule must have a non-empty database file name.`,
                                     })
                                 }
-                                if (segs.length === 2 && segs[1] === '' || segs.length === 3 && segs[2] === '') {
+                                }
+                                if (segs[groupSegId] === '') {
                                     errors.push({
                                         severity: monaco.MarkerSeverity.Error,
                                         startLineNumber: currLineId,
@@ -955,7 +969,7 @@ function validateRules(
                                     startColumn: ruleItem.startCol,
                                     endLineNumber: currLineId,
                                     endColumn: ruleItem.startCol + ruleItem.text.length,
-                                    message: `Unknown external rule source "${segs[0]}".`,
+                                    message: `Unknown external rule source "${segs[0]}". ${facts.RULE_EXTERNAL_SOURCE_MMDB} or ${facts.RULE_EXTERNAL_SOURCE_SITE} expected.`,
                                 })
                             }
                         }