diff options
Diffstat (limited to 'web')
| -rw-r--r-- | web/assets/js/model/outbound.js | 143 | ||||
| -rw-r--r-- | web/html/form/outbound.html | 79 | ||||
| -rw-r--r-- | web/service/inbound.go | 2 | ||||
| -rw-r--r-- | web/web.go | 9 |
4 files changed, 209 insertions, 24 deletions
diff --git a/web/assets/js/model/outbound.js b/web/assets/js/model/outbound.js index 97602815..8db9d8e2 100644 --- a/web/assets/js/model/outbound.js +++ b/web/assets/js/model/outbound.js @@ -97,6 +97,74 @@ const Address_Port_Strategy = { TxtPortAndAddress: "txtportandaddress" }; +const DNSRuleActions = ['direct', 'drop', 'reject', 'hijack']; + +function normalizeDNSRuleField(value) { + if (value === null || value === undefined) { + return ''; + } + if (Array.isArray(value)) { + return value.map(item => item.toString().trim()).filter(item => item.length > 0).join(','); + } + return value.toString().trim(); +} + +function normalizeDNSRuleAction(action) { + action = ObjectUtil.isEmpty(action) ? 'direct' : action.toString().toLowerCase().trim(); + return DNSRuleActions.includes(action) ? action : 'direct'; +} + +function parseLegacyDNSBlockTypes(blockTypes) { + if (blockTypes === null || blockTypes === undefined || blockTypes === '') { + return []; + } + + if (Array.isArray(blockTypes)) { + return blockTypes + .map(item => Number(item)) + .filter(item => Number.isInteger(item) && item >= 0 && item <= 65535); + } + + if (typeof blockTypes === 'number') { + return Number.isInteger(blockTypes) && blockTypes >= 0 && blockTypes <= 65535 ? [blockTypes] : []; + } + + return blockTypes + .toString() + .split(',') + .map(item => item.trim()) + .filter(item => /^\d+$/.test(item)) + .map(item => Number(item)) + .filter(item => item >= 0 && item <= 65535); +} + +function buildLegacyDNSRules(nonIPQuery, blockTypes) { + const mode = ['reject', 'drop', 'skip'].includes(nonIPQuery) ? nonIPQuery : 'reject'; + const rules = []; + const parsedBlockTypes = parseLegacyDNSBlockTypes(blockTypes); + + if (parsedBlockTypes.length > 0) { + rules.push(new Outbound.DNSRule(mode === 'reject' ? 'reject' : 'drop', parsedBlockTypes.join(','))); + } + + rules.push(new Outbound.DNSRule('hijack', '1,28')); + rules.push(new Outbound.DNSRule(mode === 'skip' ? 'direct' : mode)); + + return rules; +} + +function getDNSRulesFromJson(json = {}) { + if (Array.isArray(json.rules) && json.rules.length > 0) { + return json.rules.map(rule => Outbound.DNSRule.fromJson(rule)); + } + + if (json.nonIPQuery !== undefined || json.blockTypes !== undefined) { + return buildLegacyDNSRules(json.nonIPQuery, json.blockTypes); + } + + return []; +} + Object.freeze(Protocols); Object.freeze(SSMethods); Object.freeze(TLS_FLOW_CONTROL); @@ -107,6 +175,7 @@ Object.freeze(WireguardDomainStrategy); Object.freeze(USERS_SECURITY); Object.freeze(MODE_OPTION); Object.freeze(Address_Port_Strategy); +Object.freeze(DNSRuleActions); class CommonClass { @@ -1277,20 +1346,69 @@ Outbound.BlackholeSettings = class extends CommonClass { }; } }; + +Outbound.DNSRule = class extends CommonClass { + constructor(action = 'direct', qtype = '', domain = '') { + super(); + this.action = action; + this.qtype = qtype; + this.domain = domain; + } + + static fromJson(json = {}) { + return new Outbound.DNSRule( + json.action, + normalizeDNSRuleField(json.qtype), + normalizeDNSRuleField(json.domain), + ); + } + + toJson() { + const rule = { + action: normalizeDNSRuleAction(this.action), + }; + + const qtype = normalizeDNSRuleField(this.qtype); + if (!ObjectUtil.isEmpty(qtype)) { + if (/^\d+$/.test(qtype)) { + rule.qtype = Number(qtype); + } else { + rule.qtype = qtype; + } + } + + const domains = normalizeDNSRuleField(this.domain) + .split(',') + .map(d => d.trim()) + .filter(d => d.length > 0); + if (domains.length > 0) { + rule.domain = domains; + } + + return rule; + } +}; + Outbound.DNSSettings = class extends CommonClass { constructor( network = 'udp', address = '', port = 53, - nonIPQuery = 'reject', - blockTypes = [] + rules = [] ) { super(); this.network = network; this.address = address; this.port = port; - this.nonIPQuery = nonIPQuery; - this.blockTypes = blockTypes; + this.rules = Array.isArray(rules) ? rules.map(rule => rule instanceof Outbound.DNSRule ? rule : Outbound.DNSRule.fromJson(rule)) : []; + } + + addRule(action = 'direct') { + this.rules.push(new Outbound.DNSRule(action)); + } + + delRule(index) { + this.rules.splice(index, 1); } static fromJson(json = {}) { @@ -1298,10 +1416,23 @@ Outbound.DNSSettings = class extends CommonClass { json.network, json.address, json.port, - json.nonIPQuery, - json.blockTypes, + getDNSRulesFromJson(json), ); } + + toJson() { + const json = { + network: this.network, + address: this.address, + port: this.port, + }; + + if (this.rules.length > 0) { + json.rules = Outbound.DNSRule.toJsonArray(this.rules); + } + + return json; + } }; Outbound.VmessSettings = class extends CommonClass { constructor(address, port, id, security) { diff --git a/web/html/form/outbound.html b/web/html/form/outbound.html index a9119cf0..c350d70f 100644 --- a/web/html/form/outbound.html +++ b/web/html/form/outbound.html @@ -190,22 +190,73 @@ > </a-select> </a-form-item> - <a-form-item label="non-IP queries"> - <a-select - v-model="outbound.settings.nonIPQuery" - :dropdown-class-name="themeSwitcher.currentTheme" - > - <a-select-option v-for="s in ['reject','drop','skip']" :value="s" - >[[ s ]]</a-select-option - > - </a-select> + <a-form-item label="Rules"> + <a-button + icon="plus" + type="primary" + size="small" + @click="outbound.settings.addRule()" + ></a-button> </a-form-item> - <a-form-item - v-if="outbound.settings.nonIPQuery === 'skip'" - label="Block Types" + + <a-form + v-for="(rule, index) in outbound.settings.rules" + :colon="false" + :label-col="{ md: {span:8} }" + :wrapper-col="{ md: {span:14} }" > - <a-input v-model.number="outbound.settings.blockTypes"></a-input> - </a-form-item> + <a-divider :style="{ margin: '0' }"> + Rule [[ index + 1 ]] + <a-icon + type="delete" + @click="() => outbound.settings.delRule(index)" + :style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }" + ></a-icon> + </a-divider> + + <a-form-item label="Action"> + <a-select + v-model="rule.action" + :dropdown-class-name="themeSwitcher.currentTheme" + > + <a-select-option v-for="action in DNSRuleActions" :value="action" + >[[ action ]]</a-select-option + > + </a-select> + </a-form-item> + + <a-form-item> + <template slot="label"> + <a-tooltip> + <template slot="title"> + <span>Single qtype (e.g. 28) or list/range (e.g. 1,3,23-24)</span> + </template> + QType + <a-icon type="question-circle"></a-icon> + </a-tooltip> + </template> + <a-input + v-model.trim="rule.qtype" + placeholder="1,3,23-24" + ></a-input> + </a-form-item> + + <a-form-item> + <template slot="label"> + <a-tooltip> + <template slot="title"> + <span>Comma-separated domain rules, e.g. domain:example.com,full:example.com</span> + </template> + Domain + <a-icon type="question-circle"></a-icon> + </a-tooltip> + </template> + <a-input + v-model.trim="rule.domain" + placeholder="domain:example.com,full:example.com" + ></a-input> + </a-form-item> + </a-form> </template> <!-- wireguard settings --> diff --git a/web/service/inbound.go b/web/service/inbound.go index 7d5d8932..8ab5e6a8 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -779,7 +779,7 @@ func (s *InboundService) writeBackClientSubID(sourceInboundID int, sourceProtoco } settingsBytes, err := json.Marshal(map[string][]model.Client{ - "clients": []model.Client{client}, + "clients": {client}, }) if err != nil { return false, err @@ -353,14 +353,17 @@ func (s *Server) startTask() { isTgbotenabled, err := s.settingService.GetTgbotEnabled() if (err == nil) && (isTgbotenabled) { runtime, err := s.settingService.GetTgbotRuntime() - if err != nil || runtime == "" { - logger.Errorf("Add NewStatsNotifyJob error[%s], Runtime[%s] invalid, will run default", err, runtime) + if err != nil { + logger.Warningf("Add NewStatsNotifyJob: failed to load runtime: %v; using default @daily", err) + runtime = "@daily" + } else if strings.TrimSpace(runtime) == "" { + logger.Warning("Add NewStatsNotifyJob runtime is empty, using default @daily") runtime = "@daily" } logger.Infof("Tg notify enabled,run at %s", runtime) _, err = s.cron.AddJob(runtime, job.NewStatsNotifyJob()) if err != nil { - logger.Warning("Add NewStatsNotifyJob error", err) + logger.Warningf("Add NewStatsNotifyJob: failed to schedule runtime %q: %v", runtime, err) return } |
