From 2c68e336ff182bbe6927f07a2f9f7f221d04e5a1 Mon Sep 17 00:00:00 2001 From: actions-user Date: Sat, 28 Dec 2024 15:58:41 +0800 Subject: [PATCH] update 2024-12-28 15:58:41 --- luci-app-fchomo/.prepare.sh | 63 + luci-app-fchomo/Makefile | 45 + luci-app-fchomo/docs/Ruleset-URI-Scheme.md | 69 + luci-app-fchomo/docs/css/ClearnessDark.css | 209 ++ luci-app-fchomo/docs/img/shark-taiko.gif | Bin 0 -> 163293 bytes .../htdocs/luci-static/resources/fchomo.js | 929 ++++++ .../resources/view/fchomo/client.js | 1074 +++++++ .../resources/view/fchomo/global.js | 743 +++++ .../resources/view/fchomo/hosts.js | 41 + .../luci-static/resources/view/fchomo/log.js | 129 + .../luci-static/resources/view/fchomo/node.js | 1210 ++++++++ .../resources/view/fchomo/ruleset.js | 336 +++ .../resources/view/fchomo/server.js | 375 +++ luci-app-fchomo/po/templates/fchomo.pot | 2635 ++++++++++++++++ luci-app-fchomo/po/zh-cn | 1 + luci-app-fchomo/po/zh_Hans/fchomo.po | 2664 +++++++++++++++++ .../root/etc/capabilities/fchomo.json | 47 + luci-app-fchomo/root/etc/config/fchomo | 70 + .../root/etc/fchomo/scripts/clean_log.sh | 17 + .../root/etc/fchomo/scripts/fchomo.uc | 134 + .../root/etc/fchomo/scripts/firewall_post.ut | 454 +++ .../root/etc/fchomo/scripts/firewall_pre.ut | 30 + .../etc/fchomo/scripts/generate_client.uc | 766 +++++ .../etc/fchomo/scripts/generate_server.uc | 105 + .../etc/fchomo/scripts/update_resources.sh | 253 ++ luci-app-fchomo/root/etc/init.d/fchomo | 475 +++ .../root/etc/uci-defaults/luci-app-fchomo | 72 + .../uci-defaults/luci-app-fchomo-migration | 15 + .../share/luci/menu.d/luci-app-fchomo.json | 69 + .../usr/share/rpcd/acl.d/luci-app-fchomo.json | 25 + .../root/usr/share/rpcd/ucode/luci.fchomo | 379 +++ 31 files changed, 13434 insertions(+) create mode 100755 luci-app-fchomo/.prepare.sh create mode 100644 luci-app-fchomo/Makefile create mode 100644 luci-app-fchomo/docs/Ruleset-URI-Scheme.md create mode 100644 luci-app-fchomo/docs/css/ClearnessDark.css create mode 100644 luci-app-fchomo/docs/img/shark-taiko.gif create mode 100644 luci-app-fchomo/htdocs/luci-static/resources/fchomo.js create mode 100644 luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/client.js create mode 100644 luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/global.js create mode 100644 luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/hosts.js create mode 100644 luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/log.js create mode 100644 luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js create mode 100644 luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/ruleset.js create mode 100644 luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/server.js create mode 100644 luci-app-fchomo/po/templates/fchomo.pot create mode 120000 luci-app-fchomo/po/zh-cn create mode 100644 luci-app-fchomo/po/zh_Hans/fchomo.po create mode 100644 luci-app-fchomo/root/etc/capabilities/fchomo.json create mode 100644 luci-app-fchomo/root/etc/config/fchomo create mode 100755 luci-app-fchomo/root/etc/fchomo/scripts/clean_log.sh create mode 100755 luci-app-fchomo/root/etc/fchomo/scripts/fchomo.uc create mode 100755 luci-app-fchomo/root/etc/fchomo/scripts/firewall_post.ut create mode 100755 luci-app-fchomo/root/etc/fchomo/scripts/firewall_pre.ut create mode 100755 luci-app-fchomo/root/etc/fchomo/scripts/generate_client.uc create mode 100755 luci-app-fchomo/root/etc/fchomo/scripts/generate_server.uc create mode 100755 luci-app-fchomo/root/etc/fchomo/scripts/update_resources.sh create mode 100755 luci-app-fchomo/root/etc/init.d/fchomo create mode 100755 luci-app-fchomo/root/etc/uci-defaults/luci-app-fchomo create mode 100755 luci-app-fchomo/root/etc/uci-defaults/luci-app-fchomo-migration create mode 100644 luci-app-fchomo/root/usr/share/luci/menu.d/luci-app-fchomo.json create mode 100644 luci-app-fchomo/root/usr/share/rpcd/acl.d/luci-app-fchomo.json create mode 100755 luci-app-fchomo/root/usr/share/rpcd/ucode/luci.fchomo diff --git a/luci-app-fchomo/.prepare.sh b/luci-app-fchomo/.prepare.sh new file mode 100755 index 0000000000..0ed2a0e150 --- /dev/null +++ b/luci-app-fchomo/.prepare.sh @@ -0,0 +1,63 @@ +#!/bin/bash +PKG_NAME="$1" +CURDIR="$2" +PKG_BUILD_DIR="$3" +PKG_BUILD_BIN="$PKG_BUILD_DIR/bin" +export PATH="$PATH:$PKG_BUILD_BIN" + +OS=linux +ARCH=amd64 +JQVERSION=1.7.1 +DOCNAME=Ruleset-URI-Scheme +SHARKNAME=shark-taiko.gif + +mkdir -p "$PKG_BUILD_BIN" +curl -L "https://github.com/jqlang/jq/releases/download/jq-${JQVERSION}/jq-${OS}-${ARCH}" -o "$PKG_BUILD_BIN"/jq +chmod +x "$PKG_BUILD_BIN"/jq +latest="$(curl -L https://api.github.com/repos/kpym/gm/releases/latest | jq -rc '.tag_name' 2>/dev/null)" +curl -L "https://github.com/kpym/gm/releases/download/${latest}/gm_${latest#v}_Linux_intel64.tar.gz" -o- | tar -xz -C "$PKG_BUILD_BIN" +latest="$(curl -L https://api.github.com/repos/tdewolff/minify/releases/latest | jq -rc '.tag_name' 2>/dev/null)" +curl -L "https://github.com/tdewolff/minify/releases/download/${latest}/minify_${OS}_${ARCH}.tar.gz" -o- | tar -xz -C "$PKG_BUILD_BIN" +chmod -R +x "$PKG_BUILD_BIN" + +cp "$CURDIR"/docs/$DOCNAME.md "$PKG_BUILD_DIR" +pushd "$PKG_BUILD_DIR" +gm $DOCNAME.md +p=$(sed -n '/github.min.css/=' $DOCNAME.html) +{ +head -n$(( $p -1 )) $DOCNAME.html +echo '' +tail -n +$(( $p +1 )) $DOCNAME.html +} > buildin.html +popd +minify "$PKG_BUILD_DIR"/buildin.html | base64 | tr -d '\n' > "$PKG_BUILD_DIR"/base64 +sed -i "s|'cmxzdHBsYWNlaG9sZGVy'|'$(cat "$PKG_BUILD_DIR"/base64)'|" "$PKG_BUILD_DIR"/htdocs/luci-static/resources/fchomo.js +# shaka +echo -n "'" > "$PKG_BUILD_DIR"/base64 +base64 "$CURDIR"/docs/img/$SHARKNAME | tr -d '\n' >> "$PKG_BUILD_DIR"/base64 +echo "'" >> "$PKG_BUILD_DIR"/base64 +p=$(sed -n "/'c2hhcmstdGFpa28uZ2lm'/=" "$PKG_BUILD_DIR"/htdocs/luci-static/resources/fchomo.js) +{ +head -n$(( $p -1 )) "$PKG_BUILD_DIR"/htdocs/luci-static/resources/fchomo.js +cat "$PKG_BUILD_DIR"/base64 +tail -n +$(( $p +1 )) "$PKG_BUILD_DIR"/htdocs/luci-static/resources/fchomo.js +} > "$PKG_BUILD_DIR"/htdocs/luci-static/resources/fchomo.js.new +mv -f "$PKG_BUILD_DIR"/htdocs/luci-static/resources/fchomo.js.new "$PKG_BUILD_DIR"/htdocs/luci-static/resources/fchomo.js + +if [ -d "$CURDIR/.git" ]; then + config="$CURDIR/.git/config" +else + config="$(sed "s|^gitdir:\s*|$CURDIR/|;s|$|/config|" "$CURDIR/.git")" +fi +[ -n "$(sed -En '/^\[remote /{h;:top;n;/^\[/b;s,(https?://gitcode\.(com|net)),\1,;T top;H;x;s|\n\s*|: |;p;}' "$config")" ] && { + for d in luasrc ucode htdocs root src; do + rm -rf "$PKG_BUILD_DIR"/$d + done + mkdir -p "$PKG_BUILD_DIR"/htdocs/luci-static/resources/view + touch "$PKG_BUILD_DIR"/htdocs/luci-static/resources/view/$PKG_NAME.js + mkdir -p "$PKG_BUILD_DIR"/root/usr/share/luci/menu.d + touch "$PKG_BUILD_DIR"/root/usr/share/luci/menu.d/$PKG_NAME.json +} +exit 0 diff --git a/luci-app-fchomo/Makefile b/luci-app-fchomo/Makefile new file mode 100644 index 0000000000..531b5e8052 --- /dev/null +++ b/luci-app-fchomo/Makefile @@ -0,0 +1,45 @@ +# SPDX-License-Identifier: GPL-2.0 +# +# Copyright (C) 2024 Anya Lin + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=FullCombo Mihomo supplies maximum customization on OpenWrt. +LUCI_PKGARCH:=all +LUCI_DEPENDS:= \ + +mihomo \ + +ca-bundle \ + +firewall4 \ + +kmod-inet-diag \ + +kmod-nft-tproxy \ + +yq +wget-ssl \ +# +ip-full +kmod-tun +dnsmasq-full \ + +PKG_NAME:=luci-app-fchomo + +define Package/luci-app-fchomo/conffiles +/etc/config/fchomo +/etc/fchomo/certs/ +/etc/fchomo/provider/ +/etc/fchomo/ruleset/ +/etc/fchomo/resources/ +/etc/fchomo/templates/ +/etc/fchomo/resources.json +/etc/fchomo/geoip.dat +/etc/fchomo/geosite.dat +/etc/fchomo/asn.mmdb +/etc/fchomo/cache.db +endef + +PKG_UNPACK=$(CURDIR)/.prepare.sh $(PKG_NAME) $(CURDIR) $(PKG_BUILD_DIR) + +define Package/luci-app-fchomo/prerm +#!/bin/sh +uci delete firewall.fchomo_pre +uci delete firewall.fchomo_post +uci commit firewall +endef + +include $(TOPDIR)/feeds/luci/luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-fchomo/docs/Ruleset-URI-Scheme.md b/luci-app-fchomo/docs/Ruleset-URI-Scheme.md new file mode 100644 index 0000000000..e76070b38b --- /dev/null +++ b/luci-app-fchomo/docs/Ruleset-URI-Scheme.md @@ -0,0 +1,69 @@ +# Import rule-set links format + +## Structure + +**remote:** `http[s]://[auth@]?fmt=&behav=[&key=value][#label]` +**local:** `file://[host]?fmt=&behav=[&fill=][#label]` +**inline:** `inline://?behav=[#label]` + +## Components + +### Scheme + +Can be `http` or `https` or `file` or `inline`. + +### Auth + +Add it only if required by the target host. + +### Host + +The format is `hostname[:port]`. +`hostname` can be **Domain** or **IP Address**. +`:port` is optional, add it only if required by the target host. + +### Path + +The shortest format is `/`. + +### QueryParameters + ++ `fmt`: Required. Available values ​​refer to **format**. ++ `behav`: Required. Available values ​​refer to **behavior**. ++ `sec`: Optional. Available under **remote**. Available values ​​refer to **interval**. ++ `rawq`: Optional. Available under **remote**. Available values ​​refer to **rawQuery**. ++ `fill`: Optional. Available under **local**. Available values ​​refer to **filler**. + +#### format + +Can be `text` or `yaml` or `mrs`. Rule file format. + +#### behavior + +Can be `domain` or `ipcidr` or `classical`. Rule file behavior. + +#### interval + +The update interval for the Rule set, in seconds or /^(\d+)(s|m|h|d)?$/. + +#### rawQuery + +This parameter is required if the original link contains a url query. +Encrypt the part `key1=value1&key2=value2` after `?` in the original link with `encodeURIComponent` and use it as the payload of this parameter. + +#### filler + +Base64edStr format file content. + +### Base64edStr + +Generation steps: + + 1. Base64 encode payload. + 2. Replace all `+` with `-` and all `/` with `_` in base64 string. + 3. Remove all `=` from the EOF the base64 string. + +### URIFragment + +Ruleset label. Empty strings are not recommended. +Need encoded by `encodeURIComponent`. diff --git a/luci-app-fchomo/docs/css/ClearnessDark.css b/luci-app-fchomo/docs/css/ClearnessDark.css new file mode 100644 index 0000000000..be9f14c6ca --- /dev/null +++ b/luci-app-fchomo/docs/css/ClearnessDark.css @@ -0,0 +1,209 @@ +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote { + margin: 0; + padding: 0; +} +body { + font-family: "Helvetica Neue", Helvetica, "Hiragino Sans GB", Arial, sans-serif; + font-size: 13px; + line-height: 18px; + color: #fff; + background-color: #282a36; + margin: 10px 13px 10px 13px; +} +a { + color: #59acf3; +} +a:hover { + color: #a7d8ff; + text-decoration: none; +} +a img { + border: none; +} +p { + margin-bottom: 9px; +} +h1, +h2, +h3, +h4, +h5, +h6 { + color: #fff; + line-height: 36px; +} +h1 { + margin-bottom: 18px; + font-size: 30px; +} +h2 { + font-size: 24px; +} +h3 { + font-size: 18px; +} +h4 { + font-size: 16px; +} +h5 { + font-size: 14px; +} +h6 { + font-size: 13px; +} +hr { + margin: 0 0 19px; + border: 0; + border-bottom: 1px solid #ccc; +} +blockquote { + padding: 13px 13px 21px 15px; + margin-bottom: 18px; + font-family:georgia,serif; + font-style: italic; +} +blockquote:before { + content:"\201C"; + font-size:40px; + margin-left:-10px; + font-family:georgia,serif; + color:#eee; +} +blockquote p { + font-size: 14px; + font-weight: 300; + line-height: 18px; + margin-bottom: 0; + font-style: italic; +} +code, pre { + font-family: Monaco, Andale Mono, Courier New, monospace; +} +code { + color: #ff4a14; + padding: 1px 3px; + font-size: 12px; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} +pre { + display: block; + padding: 14px; + margin: 0 0 18px; + line-height: 16px; + font-size: 11px; + border: 1px solid #bf370f; + white-space: pre; + white-space: pre-wrap; + word-wrap: break-word; +} +pre code { + background-color: #282a36; + color: #ff4a14; + font-size: 11px; + padding: 0; +} +@media screen and (min-width: 768px) { + body { + width: 748px; + margin:10px auto; + } +} + +/** + * obsidian.css + * Obsidian style + * ported by Alexander Marenin (http://github.com/ioncreature) + */ + +.hljs { + display: block; + overflow-x: auto; + padding: 0.5em; + background: #282b2e; +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-literal, +.hljs-selector-id { + color: #93c763; +} + +.hljs-number { + color: #ffcd22; +} + +.hljs { + color: #e0e2e4; +} + +.hljs-attribute { + color: #668bb0; +} + +.hljs-code, +.hljs-class .hljs-title, +.hljs-section { + color: white; +} + +.hljs-regexp, +.hljs-link { + color: #d39745; +} + +.hljs-meta { + color: #557182; +} + +.hljs-tag, +.hljs-name, +.hljs-bullet, +.hljs-subst, +.hljs-emphasis, +.hljs-type, +.hljs-built_in, +.hljs-selector-attr, +.hljs-selector-pseudo, +.hljs-addition, +.hljs-variable, +.hljs-template-tag, +.hljs-template-variable { + color: #8cbbad; +} + +.hljs-string, +.hljs-symbol { + color: #ec7600; +} + +.hljs-comment, +.hljs-quote, +.hljs-deletion { + color: #818e96; +} + +.hljs-selector-class { + color: #A082BD +} + +.hljs-keyword, +.hljs-selector-tag, +.hljs-literal, +.hljs-doctag, +.hljs-title, +.hljs-section, +.hljs-type, +.hljs-name, +.hljs-strong { + font-weight: bold; +} diff --git a/luci-app-fchomo/docs/img/shark-taiko.gif b/luci-app-fchomo/docs/img/shark-taiko.gif new file mode 100644 index 0000000000000000000000000000000000000000..3c3a97eed7358ca39dd69a3b22538dc15312e0ee GIT binary patch literal 163293 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 li[data-value="-"]').remove(); + return dl; + } + }), + + // thanks to homeproxy + calcStringMD5: function(e) { + /* Thanks to https://stackoverflow.com/a/41602636 */ + function h(a, b) { + var c, d, e, f, g; + e = a & 2147483648; + f = b & 2147483648; + c = a & 1073741824; + d = b & 1073741824; + g = (a & 1073741823) + (b & 1073741823); + return c & d ? g ^ 2147483648 ^ e ^ f : c | d ? g & 1073741824 ? g ^ 3221225472 ^ e ^ f : g ^ 1073741824 ^ e ^ f : g ^ e ^ f; + } + function k(a, b, c, d, e, f, g) { a = h(a, h(h(b & c | ~b & d, e), g)); return h(a << f | a >>> 32 - f, b); } + function l(a, b, c, d, e, f, g) { a = h(a, h(h(b & d | c & ~d, e), g)); return h(a << f | a >>> 32 - f, b); } + function m(a, b, d, c, e, f, g) { a = h(a, h(h(b ^ d ^ c, e), g)); return h(a << f | a >>> 32 - f, b); } + function n(a, b, d, c, e, f, g) { a = h(a, h(h(d ^ (b | ~c), e), g)); return h(a << f | a >>> 32 - f, b); } + function p(a) { + var b = '', d = ''; + for (var c = 0; 3 >= c; c++) d = a >>> 8 * c & 255, d = '0' + d.toString(16), b += d.substr(d.length - 2, 2); + return b; + } + + var f = [], q, r, s, t, a, b, c, d; + e = function(a) { + a = a.replace(/\r\n/g, '\n'); + for (var b = '', d = 0; d < a.length; d++) { + var c = a.charCodeAt(d); + 128 > c ? b += String.fromCharCode(c) : (127 < c && 2048 > c ? b += String.fromCharCode(c >> 6 | 192) : + (b += String.fromCharCode(c >> 12 | 224), b += String.fromCharCode(c >> 6 & 63 | 128)), + b += String.fromCharCode(c & 63 | 128)) + } + return b; + }(e); + f = function(b) { + var c = b.length, a = c + 8; + for (var d = 16 * ((a - a % 64) / 64 + 1), e = Array(d - 1), f = 0, g = 0; g < c;) + a = (g - g % 4) / 4, f = g % 4 * 8, e[a] |= b.charCodeAt(g) << f, g++; + a = (g - g % 4) / 4; e[a] |= 128 << g % 4 * 8; e[d - 2] = c << 3; e[d - 1] = c >>> 29; + return e; + }(e); + a = 1732584193; + b = 4023233417; + c = 2562383102; + d = 271733878; + + for (e = 0; e < f.length; e += 16) q = a, r = b, s = c, t = d, + a = k(a, b, c, d, f[e + 0], 7, 3614090360), d = k(d, a, b, c, f[e + 1], 12, 3905402710), + c = k(c, d, a, b, f[e + 2], 17, 606105819), b = k(b, c, d, a, f[e + 3], 22, 3250441966), + a = k(a, b, c, d, f[e + 4], 7, 4118548399), d = k(d, a, b, c, f[e + 5], 12, 1200080426), + c = k(c, d, a, b, f[e + 6], 17, 2821735955), b = k(b, c, d, a, f[e + 7], 22, 4249261313), + a = k(a, b, c, d, f[e + 8], 7, 1770035416), d = k(d, a, b, c, f[e + 9], 12, 2336552879), + c = k(c, d, a, b, f[e + 10], 17, 4294925233), b = k(b, c, d, a, f[e + 11], 22, 2304563134), + a = k(a, b, c, d, f[e + 12], 7, 1804603682), d = k(d, a, b, c, f[e + 13], 12, 4254626195), + c = k(c, d, a, b, f[e + 14], 17, 2792965006), b = k(b, c, d, a, f[e + 15], 22, 1236535329), + a = l(a, b, c, d, f[e + 1], 5, 4129170786), d = l(d, a, b, c, f[e + 6], 9, 3225465664), + c = l(c, d, a, b, f[e + 11], 14, 643717713), b = l(b, c, d, a, f[e + 0], 20, 3921069994), + a = l(a, b, c, d, f[e + 5], 5, 3593408605), d = l(d, a, b, c, f[e + 10], 9, 38016083), + c = l(c, d, a, b, f[e + 15], 14, 3634488961), b = l(b, c, d, a, f[e + 4], 20, 3889429448), + a = l(a, b, c, d, f[e + 9], 5, 568446438), d = l(d, a, b, c, f[e + 14], 9, 3275163606), + c = l(c, d, a, b, f[e + 3], 14, 4107603335), b = l(b, c, d, a, f[e + 8], 20, 1163531501), + a = l(a, b, c, d, f[e + 13], 5, 2850285829), d = l(d, a, b, c, f[e + 2], 9, 4243563512), + c = l(c, d, a, b, f[e + 7], 14, 1735328473), b = l(b, c, d, a, f[e + 12], 20, 2368359562), + a = m(a, b, c, d, f[e + 5], 4, 4294588738), d = m(d, a, b, c, f[e + 8], 11, 2272392833), + c = m(c, d, a, b, f[e + 11], 16, 1839030562), b = m(b, c, d, a, f[e + 14], 23, 4259657740), + a = m(a, b, c, d, f[e + 1], 4, 2763975236), d = m(d, a, b, c, f[e + 4], 11, 1272893353), + c = m(c, d, a, b, f[e + 7], 16, 4139469664), b = m(b, c, d, a, f[e + 10], 23, 3200236656), + a = m(a, b, c, d, f[e + 13], 4, 681279174), d = m(d, a, b, c, f[e + 0], 11, 3936430074), + c = m(c, d, a, b, f[e + 3], 16, 3572445317), b = m(b, c, d, a, f[e + 6], 23, 76029189), + a = m(a, b, c, d, f[e + 9], 4, 3654602809), d = m(d, a, b, c, f[e + 12], 11, 3873151461), + c = m(c, d, a, b, f[e + 15], 16, 530742520), b = m(b, c, d, a, f[e + 2], 23, 3299628645), + a = n(a, b, c, d, f[e + 0], 6, 4096336452), d = n(d, a, b, c, f[e + 7], 10, 1126891415), + c = n(c, d, a, b, f[e + 14], 15, 2878612391), b = n(b, c, d, a, f[e + 5], 21, 4237533241), + a = n(a, b, c, d, f[e + 12], 6, 1700485571), d = n(d, a, b, c, f[e + 3], 10, 2399980690), + c = n(c, d, a, b, f[e + 10], 15, 4293915773), b = n(b, c, d, a, f[e + 1], 21, 2240044497), + a = n(a, b, c, d, f[e + 8], 6, 1873313359), d = n(d, a, b, c, f[e + 15], 10, 4264355552), + c = n(c, d, a, b, f[e + 6], 15, 2734768916), b = n(b, c, d, a, f[e + 13], 21, 1309151649), + a = n(a, b, c, d, f[e + 4], 6, 4149444226), d = n(d, a, b, c, f[e + 11], 10, 3174756917), + c = n(c, d, a, b, f[e + 2], 15, 718787259), b = n(b, c, d, a, f[e + 9], 21, 3951481745), + a = h(a, q), b = h(b, r), c = h(c, s), d = h(d, t); + return (p(a) + p(b) + p(c) + p(d)).toLowerCase(); + }, + + // thanks to homeproxy + decodeBase64Str: function(str) { + if (!str) + return null; + + /* Thanks to luci-app-ssr-plus */ + str = str.replace(/-/g, '+').replace(/_/g, '/'); + var padding = (4 - (str.length % 4)) % 4; + if (padding) + str = str + Array(padding + 1).join('='); + + return decodeURIComponent(Array.prototype.map.call(atob(str), (c) => + '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2) + ).join('')); + }, + + generateRand: function(type, length) { + var byteArr; + if (['base64', 'hex'].includes(type)) + byteArr = crypto.getRandomValues(new Uint8Array(length)); + switch (type) { + case 'base64': + /* Thanks to https://stackoverflow.com/questions/9267899 */ + return btoa(String.fromCharCode.apply(null, byteArr)); + case 'hex': + return Array.from(byteArr, (byte) => + (byte & 255).toString(16).padStart(2, '0') + ).join(''); + case 'uuid': + /* Thanks to https://stackoverflow.com/a/2117523 */ + return (location.protocol === 'https:') ? crypto.randomUUID() : + ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, (c) => + (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16) + ); + default: + return null; + }; + }, + + getFeatures: function() { + const callGetFeatures = rpc.declare({ + object: 'luci.fchomo', + method: 'get_features', + expect: { '': {} } + }); + + return L.resolveDefault(callGetFeatures(), {}); + }, + + getServiceStatus: function(instance) { + var conf = 'fchomo'; + const callServiceList = rpc.declare({ + object: 'service', + method: 'list', + params: ['name'], + expect: { '': {} } + }); + + return L.resolveDefault(callServiceList(conf), {}) + .then((res) => { + var isRunning = false; + try { + isRunning = res[conf]['instances'][instance].running; + } catch (e) {} + return isRunning; + }); + }, + + getClashAPI: function(instance) { + const callGetClashAPI = rpc.declare({ + object: 'luci.fchomo', + method: 'get_clash_api', + params: ['instance'], + expect: { '': {} } + }); + + return L.resolveDefault(callGetClashAPI(instance), {}); + }, + + // thanks to homeproxy + loadDefaultLabel: function(section_id) { + var label = uci.get(this.config, section_id, 'label'); + if (label) { + return label; + } else { + uci.set(this.config, section_id, 'label', section_id); + return section_id; + } + }, + + // thanks to homeproxy + loadModalTitle: function(title, addtitle, section_id) { + var label = uci.get(this.config, section_id, 'label'); + return label ? title + ' » ' + label : addtitle; + }, + + loadProxyGroupLabel: function(preadds, section_id) { + delete this.keylist; + delete this.vallist; + + preadds?.forEach((arr) => { + this.value.apply(this, arr); + }); + uci.sections(this.config, 'proxy_group', (res) => { + if (res.enabled !== '0') + this.value(res['.name'], res.label); + }); + + return this.super('load', section_id); + }, + + loadNodeLabel: function(section_id) { + delete this.keylist; + delete this.vallist; + + this.value('', _('-- Please choose --')); + uci.sections(this.config, 'node', (res) => { + if (res.enabled !== '0') + this.value(res['.name'], res.label); + }); + + return this.super('load', section_id); + }, + + loadProviderLabel: function(section_id) { + delete this.keylist; + delete this.vallist; + + this.value('', _('-- Please choose --')); + uci.sections(this.config, 'provider', (res) => { + if (res.enabled !== '0') + this.value(res['.name'], res.label); + }); + + return this.super('load', section_id); + }, + + loadRulesetLabel: function(behaviors, section_id) { + delete this.keylist; + delete this.vallist; + + this.value('', _('-- Please choose --')); + uci.sections(this.config, 'ruleset', (res) => { + if (res.enabled !== '0') + if (behaviors ? behaviors.includes(res.behavior) : true) + this.value(res['.name'], res.label); + }); + + return this.super('load', section_id); + }, + + loadSubRuleGroup: function(section_id) { + delete this.keylist; + delete this.vallist; + + this.value('', _('-- Please choose --')); + let groups = {}; + uci.sections(this.config, 'subrules', (res) => { + if (res.enabled !== '0') + groups[res.group] = res.group; + }); + Object.keys(groups).forEach((group) => { + this.value(group, group); + }); + + return this.super('load', section_id); + }, + + renderStatus: function(self, ElId, isRunning, instance, noGlobal) { + var visible = isRunning && (isRunning.http || isRunning.https); + + return E([ + E('button', { + 'class': 'cbi-button cbi-button-apply' + (noGlobal ? ' hidden' : ''), + 'click': ui.createHandlerFn(this, self.handleReload, instance) + }, [ _('Reload') ]), + self.updateStatus(self, E('span', { id: ElId, style: 'border: unset; font-style: italic; font-weight: bold' }), isRunning ? true : false), + E('a', { + 'class': 'cbi-button cbi-button-apply %s'.format(visible ? '' : 'hidden'), + 'href': visible ? self.getDashURL(self, isRunning) : '', + 'target': '_blank', + 'rel': 'noreferrer noopener' + }, [ _('Open Dashboard') ]) + ]); + }, + updateStatus: function(self, El, isRunning, instance, noGlobal) { + if (El) { + El.style.color = isRunning ? 'green' : 'red'; + El.innerHTML = ' %s%s '.format(noGlobal ? instance + ' ' : '', isRunning ? _('Running') : _('Not Running')); + /* Dashboard button */ + if (El.nextSibling?.localName === 'a') + self.getClashAPI(instance).then((res) => { + let visible = isRunning && (res.http || res.https); + if (visible) { + El.nextSibling.classList.remove('hidden'); + } else + El.nextSibling.classList.add('hidden'); + + El.nextSibling.href = visible ? self.getDashURL(self, Object.assign(res, isRunning)) : ''; + }); + } + + return El; + }, + getDashURL: function(self, isRunning) { + var tls = isRunning.https ? 's' : '', + host = window.location.hostname, + port = isRunning.https ? isRunning.https.split(':').pop() : isRunning.http.split(':').pop(), + secret = isRunning.secret, + repo = isRunning.dashboard_repo; + + return 'http%s://%s:%s/ui/'.format(tls, host, port) + + String.format(self.dashrepos_urlparams[repo] || '', host, port, secret) + }, + + renderResDownload: function(self, section_id) { + var section_type = this.section.sectiontype; + var type = uci.get(this.config, section_id, 'type'), + url = uci.get(this.config, section_id, 'url'), + header = uci.get(this.config, section_id, 'header'); + + var El = E([ + E('button', { + class: 'cbi-button cbi-button-add', + disabled: (type !== 'http') || null, + click: ui.createHandlerFn(this, function(section_type, section_id, type, url, header) { + if (type === 'http') { + return self.downloadFile(section_type, section_id, url, header).then((res) => { + ui.addNotification(null, E('p', _('Download successful.'))); + }).catch((e) => { + ui.addNotification(null, E('p', _('Download failed: %s').format(e))); + }); + } else + return ui.addNotification(null, E('p', _('Unable to download unsupported type: %s').format(type))); + }, section_type, section_id, type, url, header) + }, [ _('🡇') ]) //🗘 + ]); + + return El; + }, + + renderSectionAdd: function(prefmt, LC, extra_class) { + var el = form.GridSection.prototype.renderSectionAdd.apply(this, [ extra_class ]), + nameEl = el.querySelector('.cbi-section-create-name'); + ui.addValidator(nameEl, 'uciname', true, (v) => { + var button = el.querySelector('.cbi-section-create > .cbi-button-add'); + var prefix = prefmt?.prefix ? prefmt.prefix : '', + suffix = prefmt?.suffix ? prefmt.suffix : ''; + + if (!v) { + button.disabled = true; + return true; + } else if (LC && (v !== v.toLowerCase())) { + button.disabled = true; + return _('Expecting: %s').format(_('Lowercase only')); + } else if (uci.get(this.config, v)) { + button.disabled = true; + return _('Expecting: %s').format(_('unique UCI identifier')); + } else if (uci.get(this.config, prefix + v + suffix)) { + button.disabled = true; + return _('Expecting: %s').format(_('unique identifier')); + } else { + button.disabled = null; + return true; + } + }, 'blur', 'keyup'); + + return el; + }, + + handleAdd: function(prefmt, ev, name) { + var prefix = prefmt?.prefix ? prefmt.prefix : '', + suffix = prefmt?.suffix ? prefmt.suffix : ''; + + return form.GridSection.prototype.handleAdd.apply(this, [ ev, prefix + name + suffix ]); + }, + + handleReload: function(instance, ev, section_id) { + var instance = instance || ''; + return fs.exec('/etc/init.d/fchomo', ['reload', instance]) + .then((res) => { /* return window.location = window.location.href.split('#')[0] */ }) + .catch((e) => { + ui.addNotification(null, E('p', _('Failed to execute "/etc/init.d/fchomo %s %s" reason: %s').format('reload', instance, e))) + }) + }, + + handleRemoveIdles: function(self) { + var section_type = this.sectiontype; + + let loaded = []; + uci.sections(this.config, section_type, (section, sid) => loaded.push(sid)); + + return self.lsDir(section_type).then((res) => { + let sectionEl = E('div', { class: 'cbi-section' }, []); + + res.filter(e => !loaded.includes(e)).forEach((filename) => { + sectionEl.appendChild(E('div', { class: 'cbi-value' }, [ + E('label', { + class: 'cbi-value-title', + id: 'rmidles.' + filename + '.label' + }, [ filename ]), + E('div', { class: 'cbi-value-field' }, [ + E('button', { + class: 'cbi-button cbi-button-negative important', + id: 'rmidles.' + filename + '.button', + click: ui.createHandlerFn(this, function(filename) { + return self.removeFile(section_type, filename).then((res) => { + let node = document.getElementById('rmidles.' + filename + '.label'); + node.innerHTML = '%s'.format(node.innerHTML); + node = document.getElementById('rmidles.' + filename + '.button'); + node.classList.add('hidden'); + }); + }, filename) + }, [ _('Remove') ]) + ]) + ])); + }); + + ui.showModal(_('Remove idles'), [ + sectionEl, + E('div', { class: 'right' }, [ + E('button', { + class: 'btn cbi-button-action', + click: ui.hideModal + }, [ _('Complete') ]) + ]) + ]); + }); + }, + + textvalue2Value: function(section_id) { + var cval = this.cfgvalue(section_id); + var i = this.keylist.indexOf(cval); + + return this.vallist[i]; + }, + + validateAuth: function(section_id, value) { + if (!value) + return true; + if (!value.match(/^[\w-]{3,}:[^:]+$/)) + return _('Expecting: %s').format('[A-Za-z0-9_-]{3,}:[^:]+'); + + return true; + }, + validateAuthUsername: function(section_id, value) { + if (!value) + return true; + if (!value.match(/^[\w-]{3,}$/)) + return _('Expecting: %s').format('[A-Za-z0-9_-]{3,}'); + + return true; + }, + validateAuthPassword: function(section_id, value) { + if (!value) + return true; + if (!value.match(/^[^:]+$/)) + return _('Expecting: %s').format('[^:]+'); + + return true; + }, + + validateCommonPort: function(section_id, value) { + // thanks to homeproxy + var stubValidator = { + factory: validation, + apply: function(type, value, args) { + if (value != null) + this.value = value; + + return validation.types[type].apply(this, args); + }, + assert: function(condition) { + return !!condition; + } + }; + + if (value && !value.match(/common(_stun)?/)) { + var ports = []; + for (var i of value.split(',')) { + if (!stubValidator.apply('port', i) && !stubValidator.apply('portrange', i)) + return _('Expecting: %s').format(_('valid port value')); + if (ports.includes(i)) + return _('Port %s alrealy exists!').format(i); + ports = ports.concat(i); + } + } + + return true; + }, + + validateJson: function(section_id, value) { + if (!value) + return true; + + try { + var obj = JSON.parse(value.trim()); + if (!obj) + return _('Expecting: %s').format(_('valid JSON format')); + } + catch(e) { + return _('Expecting: %s').format(_('valid JSON format')); + } + + return true; + }, + + validateBase64Key: function(length, section_id, value) { + /* Thanks to luci-proto-wireguard */ + if (value) + if (value.length !== length || !value.match(/^(?:[A-Za-z0-9+\/]{4})*(?:[A-Za-z0-9+\/]{2}==|[A-Za-z0-9+\/]{3}=)?$/) || value[length-1] !== '=') + return _('Expecting: %s').format(_('valid base64 key with %d characters').format(length)); + + return true; + }, + + validateShadowsocksPassword: function(self, encmode, section_id, value) { + var length = self.shadowsocks_cipher_length[encmode]; + if (typeof length !== 'undefined') { + length = Math.ceil(length/3)*4; + if (encmode.match(/^2022-/)) { + return self.validateBase64Key(length, section_id, value); + } else { + if (length === 0 && !value) + return _('Expecting: %s').format(_('non-empty value')); + if (length !== 0 && value.length !== length) + return _('Expecting: %s').format(_('valid key length with %d characters').format(length)); + } + } else + return true; + + return true; + }, + + validateBytesize: function(section_id, value) { + if (!value) + return true; + + if (!value.match(/^(\d+)(k|m|g)?b?$/)) + return _('Expecting: %s').format('^(\\d+)(k|m|g)?b?$'); + + return true; + }, + validateTimeDuration: function(section_id, value) { + if (!value) + return true; + + if (!value.match(/^(\d+)(s|m|h|d)?$/)) + return _('Expecting: %s').format('^(\\d+)(s|m|h|d)?$'); + + return true; + }, + + validateUniqueValue: function(section_id, value) { + if (!value) + return _('Expecting: %s').format(_('non-empty value')); + + var duplicate = false; + uci.sections(this.config, this.section.sectiontype, (res) => { + if (res['.name'] !== section_id) + if (res[this.option] === value) + duplicate = true; + }); + if (duplicate) + return _('Expecting: %s').format(_('unique value')); + + return true; + }, + + validateUrl: function(section_id, value) { + if (!value) + return true; + + try { + var url = new URL(value); + if (!url.hostname) + return _('Expecting: %s').format(_('valid URL')); + } + catch(e) { + return _('Expecting: %s').format(_('valid URL')); + } + + return true; + }, + + validateUUID: function(section_id, value) { + if (!value) + return true; + else if (value.match('^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') === null) + return _('Expecting: %s').format(_('valid uuid')); + + return true; + }, + + lsDir: function(type) { + const callLsDir = rpc.declare({ + object: 'luci.fchomo', + method: 'dir_ls', + params: ['type'], + expect: { '': {} } + }); + + return L.resolveDefault(callLsDir(type), {}).then((res) => { + if (res.result) { + return res.result; + } else + throw res.error || 'unknown error'; + }); + }, + + readFile: function(type, filename) { + const callReadFile = rpc.declare({ + object: 'luci.fchomo', + method: 'file_read', + params: ['type', 'filename'], + expect: { '': {} } + }); + + return L.resolveDefault(callReadFile(type, filename), {}).then((res) => { + if (res.content ?? true) { + return res.content; + } else + throw res.error || 'unknown error'; + }); + }, + + writeFile: function(type, filename, content) { + const callWriteFile = rpc.declare({ + object: 'luci.fchomo', + method: 'file_write', + params: ['type', 'filename', 'content'], + expect: { '': {} } + }); + + return L.resolveDefault(callWriteFile(type, filename, content), {}).then((res) => { + if (res.result) { + return res.result; + } else + throw res.error || 'unknown error'; + }); + }, + + downloadFile: function(type, filename, url, header) { + const callDownloadFile = rpc.declare({ + object: 'luci.fchomo', + method: 'file_download', + params: ['type', 'filename', 'url', 'header'], + expect: { '': {} } + }); + + return L.resolveDefault(callDownloadFile(type, filename, url, header), {}).then((res) => { + if (res.result) { + return res.result; + } else + throw res.error || 'unknown error'; + }); + }, + + removeFile: function(type, filename) { + const callRemoveFile = rpc.declare({ + object: 'luci.fchomo', + method: 'file_remove', + params: ['type', 'filename'], + expect: { '': {} } + }); + + return L.resolveDefault(callRemoveFile(type, filename), {}).then((res) => { + if (res.result) { + return res.result; + } else + throw res.error || 'unknown error'; + }); + }, + + // thanks to homeproxy + uploadCertificate: function(type, filename, ev) { + const callWriteCertificate = rpc.declare({ + object: 'luci.fchomo', + method: 'certificate_write', + params: ['filename'], + expect: { '': {} } + }); + + return ui.uploadFile('/tmp/fchomo_certificate.tmp', ev.target) + .then(L.bind((btn, res) => { + return L.resolveDefault(callWriteCertificate(filename), {}).then((ret) => { + if (ret.result === true) + ui.addNotification(null, E('p', _('Your %s was successfully uploaded. Size: %sB.').format(type, res.size))); + else + ui.addNotification(null, E('p', _('Failed to upload %s, error: %s.').format(type, ret.error))); + }); + }, this, ev.target)) + .catch((e) => { ui.addNotification(null, E('p', e.message)) }); + }, + uploadInitialPack: function(ev, section_id) { + const callWriteInitialPack = rpc.declare({ + object: 'luci.fchomo', + method: 'initialpack_write', + expect: { '': {} } + }); + + return ui.uploadFile('/tmp/fchomo_initialpack.tmp', ev.target) + .then(L.bind((btn, res) => { + return L.resolveDefault(callWriteInitialPack(), {}).then((ret) => { + if (ret.result === true) { + ui.addNotification(null, E('p', _('Successfully uploaded.'))); + return window.location = window.location.href.split('#')[0]; + } else + ui.addNotification(null, E('p', _('Failed to upload, error: %s.').format(ret.error))); + }); + }, this, ev.target)) + .catch((e) => { ui.addNotification(null, E('p', e.message)) }); + } +}); diff --git a/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/client.js b/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/client.js new file mode 100644 index 0000000000..b3650a921d --- /dev/null +++ b/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/client.js @@ -0,0 +1,1074 @@ +'use strict'; +'require form'; +'require poll'; +'require uci'; +'require view'; + +'require fchomo as hm'; +'require tools.widgets as widgets'; + +function loadDNSServerLabel(section_id) { + delete this.keylist; + delete this.vallist; + + this.value('default-dns', _('Default DNS (issued by WAN)')); + this.value('system-dns', _('System DNS')); + this.value('block-dns', _('Block DNS queries')); + uci.sections(this.config, 'dns_server', (res) => { + if (res.enabled !== '0') + this.value(res['.name'], res.label); + }); + + return this.super('load', section_id); +} +function validateNameserver(section_id, value) { + const arr = value.trim().split(' '); + if (arr.length > 1 && arr.includes('block-dns')) + return _('Expecting: %s').format(_('If Block is selected, uncheck others')); + + return true; +} + +class DNSAddress { + constructor(address) { + this.input = address || ''; + [this.addr, this.rawparams] = this.input.split('#'); + if (this.rawparams) { + if (this.rawparams.match(/^[^=&]+(&|$)/)) + this.rawparams = 'detour=' + this.rawparams + } else + this.rawparams = ''; + this.params = new URLSearchParams(this.rawparams); + } + + parseParam(param) { + return this.params.has(param) ? decodeURI(this.params.get(param)) : null; + } + + setParam(param, value) { + if (value) { + this.params.set(param, value); + } else + this.params.delete(param); + + return this + } + + toString() { + return this.addr + (this.params.size === 0 ? '' : '#' + + ['detour', 'h3', 'ecs', 'ecs-override'].map((k) => { + return this.params.has(k) ? '%s=%s'.format(k, encodeURI(this.params.get(k))) : null; + }).filter(v => v).join('&') + ); + } +} + +class RulesEntry { + constructor(entry) { + this.input = entry || ''; + var content = this.input; + this.subrule = content.split(','); + if (this.subrule.shift() === 'SUB-RULE') { + var subrule_payload = this.subrule.join(',').match(/^\(.*\)/); + if (subrule_payload) { + content = subrule_payload[0].slice(1, -1); + this.subrule = this.subrule.pop() || ' '; + } else { + content = this.subrule.join(','); + this.subrule = ' '; + } + } else + this.subrule = false; + this.rawparams = content.split(','); + this.type = this.rawparams.shift() || ''; + var logical_payload, rawfactor; + (function(rawparams_typecuted) { + logical_payload = rawparams_typecuted.match(/^\(.*\)/); + if (logical_payload) { + rawfactor = logical_payload[0]; + this.rawparams = rawparams_typecuted.replace(/^\(.*\),?/, '').split(','); + } else + rawfactor = this.rawparams.shift() || ''; + }.call(this, this.rawparams.join(','))); + this.detour = this.rawparams.shift() || ''; + if (this.type === 'MATCH') + this.detour = rawfactor; + + this.payload = []; + if (logical_payload) { // ꓹ ႇ ❟ + if (rawfactor.match(/^\(.*\)$/)) // LOGICAL_TPYE,() + rawfactor.slice(1, -1).split('ꓹ').forEach((payload) => { // U+A4F9 + if (payload.match(/^\(.*\)$/)) { // (payload) + let arr = payload.slice(1, -1).split('‚'); // U+201A + this.payload.push({ type: arr[0] || '', factor: arr[1] || '' }); + } + }); + } else + this.payload[0] = { type: this.type, factor: rawfactor }; + + this.params = {}; + if (this.rawparams.length > 0) { + this.rawparams.forEach((k) => { + this.params[k] = 'true'; + }); + } + this.rawparams = this.rawparams.join(','); + } + + setKey(key, value) { + this[key] = value; + + return this + } + + getPayload(n) { + return this.payload[n] || {}; + } + + setPayload(n, obj) { + this.payload[n] ||= {}; + + Object.keys(obj).forEach((key) => { + this.payload[n][key] = obj[key] || null; + }); + + return this + } + + getParam(param) { + return this.params[param] || null; + } + + setParam(param, value) { + if (value) { + this.params[param] = value; + } else + this.params[param] = null; + + return this + } + + toString() { + var logical = hm.rules_logical_type.map(e => e[0] || e).includes(this.type), + factor = ''; + if (logical) { + let n = hm.rules_logical_payload_count[this.type] || 0; + factor = '(%s)'.format(this.payload.slice(0, n).map((payload) => { + return '(%s‚%s)'.format(payload.type || '', payload.factor || ''); + }).join('ꓹ')); + } else + factor = this.payload[0].factor; + + if (this.subrule) { + return 'SUB-RULE,(%s),%s'.format([this.type, factor].join(','), this.subrule); + } else + if (this.type === 'MATCH') { + return [this.type, this.detour].join(','); + } else + return [this.type, factor, this.detour].concat( + ['no-resolve', 'src'].filter(k => this.params[k]) + ).join(','); + } +} + +function strToFlag(string) { + if (!string) + return null; + + switch(string) { + case 'true': + return '1'; + case 'false': + return '0'; + default: + return null; + } +} +function flagToStr(flag) { + if (!flag) + return null; + + switch(flag) { + case '1': + return 'true'; + default: + return null; + } +} + +function renderPayload(s, total, uciconfig) { + // common payload + var initPayload = function(o, n, key, uciconfig) { + o.load = L.bind(function(n, key, uciconfig, section_id) { + return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayload(n)[key]; + }, o, n, key, uciconfig); + o.onchange = function(ev, section_id, value) { + var UIEl = this.section.getUIElement(section_id, 'entry'); + + let n = this.option.match(/^payload(\d+)_/)[1]; + var newvalue = new RulesEntry(UIEl.getValue()).setPayload(n, {factor: value}).toString(); + + UIEl.node.previousSibling.innerText = newvalue; + return UIEl.setValue(newvalue); + } + o.write = function() {}; + o.rmempty = false; + o.modalonly = true; + } + + var o, prefix; + for (var n=0; n { + o.value.apply(o, res); + }) + Object.keys(hm.rules_logical_payload_count).forEach((key) => { + if (n < hm.rules_logical_payload_count[key]) + o.depends('type', key); + }) + initPayload(o, n, 'type', uciconfig); + o.onchange = function(ev, section_id, value) { + var UIEl = this.section.getUIElement(section_id, 'entry'); + + let n = this.option.match(/^payload(\d+)_/)[1]; + var newvalue = new RulesEntry(UIEl.getValue()).setPayload(n, {type: value}).toString(); + + UIEl.node.previousSibling.innerText = newvalue; + return UIEl.setValue(newvalue); + } + + o = s.option(form.Value, prefix + 'general', _('Factor') + ` ${n+1}`); + if (n === 0) { + o.depends({type: /\bDOMAIN\b/}); + o.depends({type: /\bGEO(SITE|IP)\b/}); + o.depends({type: /\bASN\b/}); + o.depends({type: /\bPROCESS\b/}); + } + o.depends(Object.fromEntries([[prefix + 'type', /\bDOMAIN\b/]])); + o.depends(Object.fromEntries([[prefix + 'type', /\bGEO(SITE|IP)\b/]])); + o.depends(Object.fromEntries([[prefix + 'type', /\bASN\b/]])); + o.depends(Object.fromEntries([[prefix + 'type', /\bPROCESS\b/]])); + initPayload(o, n, 'factor', uciconfig); + + o = s.option(form.Value, prefix + 'ip', _('Factor') + ` ${n+1}`); + o.datatype = 'cidr'; + if (n === 0) { + o.depends({type: /\b(CIDR|CIDR6)\b/}); + o.depends({type: /\bIP-SUFFIX\b/}); + } + o.depends(Object.fromEntries([[prefix + 'type', /\b(CIDR|CIDR6)\b/]])); + o.depends(Object.fromEntries([[prefix + 'type', /\bIP-SUFFIX\b/]])); + initPayload(o, n, 'factor', uciconfig); + + o = s.option(form.Value, prefix + 'port', _('Factor') + ` ${n+1}`); + o.datatype = 'or(port, portrange)'; + if (n === 0) + o.depends({type: /\bPORT\b/}); + o.depends(Object.fromEntries([[prefix + 'type', /\bPORT\b/]])); + initPayload(o, n, 'factor', uciconfig); + + o = s.option(form.ListValue, prefix + 'l4', _('Factor') + ` ${n+1}`); + o.value('udp', _('UDP')); + o.value('tcp', _('TCP')); + if (n === 0) + o.depends('type', 'NETWORK'); + o.depends(prefix + 'type', 'NETWORK'); + initPayload(o, n, 'factor', uciconfig); + + o = s.option(form.Value, prefix + 'dscp', _('Factor') + ` ${n+1}`); + o.datatype = 'range(0, 63)'; + if (n === 0) + o.depends('type', 'DSCP'); + o.depends(prefix + 'type', 'DSCP'); + initPayload(o, n, 'factor', uciconfig); + + o = s.option(form.ListValue, prefix + 'rule_set', _('Factor') + ` ${n+1}`); + o.value('', _('-- Please choose --')); + if (n === 0) + o.depends('type', 'RULE-SET'); + o.depends(prefix + 'type', 'RULE-SET'); + initPayload(o, n, 'factor', uciconfig); + o.load = L.bind(function(n, key, uciconfig, section_id) { + hm.loadRulesetLabel.call(this, null, section_id); + + return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getPayload(n)[key]; + }, o, n, 'factor', uciconfig) + } +} + +function renderRules(s, uciconfig) { + var o; + + o = s.option(form.DummyValue, 'entry', _('Entry')); + o.load = function(section_id) { + return form.DummyValue.prototype.load.call(this, section_id) || '%s,%s,%s'.format(hm.rules_type[0][0], '', hm.preset_outbound.full[0][0]); + } + o.write = L.bind(form.AbstractValue.prototype.write, o); + o.remove = L.bind(form.AbstractValue.prototype.remove, o); + o.editable = true; + + o = s.option(form.ListValue, 'type', _('Type')); + o.default = hm.rules_type[0][0]; + [...hm.rules_type, ...hm.rules_logical_type].forEach((res) => { + o.value.apply(o, res); + }) + o.load = function(section_id) { + return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).type; + } + o.validate = function(section_id, value) { + // params only available for types other than + // https://github.com/muink/mihomo/blob/43f21c0b412b7a8701fe7a2ea6510c5b985a53d6/config/config.go#L1050 + // https://github.com/muink/mihomo/blob/43f21c0b412b7a8701fe7a2ea6510c5b985a53d6/rules/parser.go#L12 + if (['GEOIP', 'IP-ASN', 'IP-CIDR', 'IP-CIDR6', 'IP-SUFFIX', 'RULE-SET'].includes(value)) { + ['no-resolve', 'src'].forEach((opt) => { + let UIEl = this.section.getUIElement(section_id, opt); + UIEl.node.querySelector('input').disabled = null; + }); + } else { + ['no-resolve', 'src'].forEach((opt) => { + let UIEl = this.section.getUIElement(section_id, opt); + UIEl.setValue(''); + UIEl.node.querySelector('input').disabled = 'true'; + }); + + var UIEl = this.section.getUIElement(section_id, 'entry'); + + var newvalue = new RulesEntry(UIEl.getValue()).setParam('no-resolve').setParam('src').toString(); + + UIEl.node.previousSibling.innerText = newvalue; + UIEl.setValue(newvalue); + } + + return true; + } + o.onchange = function(ev, section_id, value) { + var UIEl = this.section.getUIElement(section_id, 'entry'); + + var newvalue = new RulesEntry(UIEl.getValue()).setKey('type', value).toString(); + + UIEl.node.previousSibling.innerText = newvalue; + return UIEl.setValue(newvalue); + } + o.write = function() {}; + o.rmempty = false; + o.modalonly = true; + + renderPayload(s, Math.max(...Object.values(hm.rules_logical_payload_count)), uciconfig); + + o = s.option(form.ListValue, 'detour', _('Proxy group')); + o.renderWidget = function(/* ... */) { + var frameEl = form.ListValue.prototype.renderWidget.apply(this, arguments); + + frameEl.querySelector('select').style["min-width"] = '10em'; + + return frameEl; + } + o.load = function(section_id) { + hm.loadProxyGroupLabel.call(this, hm.preset_outbound.full, section_id); + + return new RulesEntry(uci.get(uciconfig, section_id, 'entry')).detour; + } + o.onchange = function(ev, section_id, value) { + var UIEl = this.section.getUIElement(section_id, 'entry'); + + var newvalue = new RulesEntry(UIEl.getValue()).setKey('detour', value).toString(); + + UIEl.node.previousSibling.innerText = newvalue; + return UIEl.setValue(newvalue); + } + o.write = function() {}; + //o.depends('SUB-RULE', '0'); + o.editable = true; + + o = s.option(form.Flag, 'src', _('src')); + o.default = o.disabled; + o.load = function(section_id) { + return strToFlag(new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getParam('src')); + } + o.onchange = function(ev, section_id, value) { + var UIEl = this.section.getUIElement(section_id, 'entry'); + + var newvalue = new RulesEntry(UIEl.getValue()).setParam('src', flagToStr(value)).toString(); + + UIEl.node.previousSibling.innerText = newvalue; + UIEl.setValue(newvalue); + } + o.write = function() {}; + o.depends('SUB-RULE', '0'); + o.modalonly = true; + + o = s.option(form.Flag, 'no-resolve', _('no-resolve')); + o.default = o.disabled; + o.load = function(section_id) { + return strToFlag(new RulesEntry(uci.get(uciconfig, section_id, 'entry')).getParam('no-resolve')); + } + o.onchange = function(ev, section_id, value) { + var UIEl = this.section.getUIElement(section_id, 'entry'); + + var newvalue = new RulesEntry(UIEl.getValue()).setParam('no-resolve', flagToStr(value)).toString(); + + UIEl.node.previousSibling.innerText = newvalue; + UIEl.setValue(newvalue); + } + o.write = function() {}; + o.depends('SUB-RULE', '0'); + o.modalonly = true; +} + +return view.extend({ + load: function() { + return Promise.all([ + uci.load('fchomo') + ]); + }, + + render: function(data) { + var dashboard_repo = uci.get(data[0], 'api', 'dashboard_repo'); + + let m, s, o, ss, so; + + m = new form.Map('fchomo', _('Mihomo client')); + + s = m.section(form.TypedSection); + s.render = function () { + poll.add(function () { + return hm.getServiceStatus('mihomo-c').then((isRunning) => { + hm.updateStatus(hm, document.getElementById('_client_bar'), isRunning ? { dashboard_repo: dashboard_repo } : false, 'mihomo-c', true); + }); + }); + + return E('div', { class: 'cbi-section' }, [ + E('p', [ + hm.renderStatus(hm, '_client_bar', false, 'mihomo-c', true) + ]) + ]); + } + + s = m.section(form.NamedSection, 'routing', 'fchomo', null); + + /* Proxy Group START */ + s.tab('group', _('Proxy Group')); + + /* Client switch */ + o = s.taboption('group', form.Button, '_reload_client', _('Quick Reload')); + o.inputtitle = _('Reload'); + o.inputstyle = 'apply'; + o.onclick = L.bind(hm.handleReload, o, 'mihomo-c'); + + o = s.taboption('group', form.Flag, 'client_enabled', _('Enable')); + o.default = o.disabled; + + /* Proxy Group */ + o = s.taboption('group', form.SectionValue, '_group', form.GridSection, 'proxy_group', null); + ss = o.subsection; + var prefmt = { 'prefix': 'group_', 'suffix': '' }; + ss.addremove = true; + ss.rowcolors = true; + ss.sortable = true; + ss.nodescriptions = true; + ss.modaltitle = L.bind(hm.loadModalTitle, ss, _('Proxy Group'), _('Add a proxy group')); + ss.sectiontitle = L.bind(hm.loadDefaultLabel, ss); + ss.renderSectionAdd = L.bind(hm.renderSectionAdd, ss, prefmt, true); + ss.handleAdd = L.bind(hm.handleAdd, ss, prefmt); + + ss.tab('field_general', _('General fields')); + ss.tab('field_override', _('Override fields')); + ss.tab('field_health', _('Health fields')); + + /* General fields */ + so = ss.taboption('field_general', form.Value, 'label', _('Label')); + so.load = L.bind(hm.loadDefaultLabel, so); + so.validate = function(section_id, value) { + if (value.match(/[,]/)) + return _('Expecting: %s').format(_('not included ","')); + + return hm.validateUniqueValue.call(this, section_id, value); + } + so.modalonly = true; + + so = ss.taboption('field_general', form.Flag, 'enabled', _('Enable')); + so.default = so.enabled; + so.editable = true; + + so = ss.taboption('field_general', form.ListValue, 'type', _('Type')); + so.default = hm.proxy_group_type[0][0]; + hm.proxy_group_type.forEach((res) => { + so.value.apply(so, res); + }) + + so = ss.taboption('field_general', form.MultiValue, 'groups', _('Group')); + hm.preset_outbound.full.forEach((res) => { + so.value.apply(so, res); + }) + so.load = L.bind(hm.loadProxyGroupLabel, so, hm.preset_outbound.full); + so.editable = true; + + so = ss.taboption('field_general', form.MultiValue, 'proxies', _('Node')); + so.value('', _('-- Please choose --')); + so.load = L.bind(hm.loadNodeLabel, so); + so.validate = function(section_id, value) { + if (this.section.getOption('include_all').formvalue(section_id) === '1' || + this.section.getOption('include_all_proxies').formvalue(section_id) === '1') + this.getUIElement(section_id, this.option).node.setAttribute('disabled', ''); + else + this.getUIElement(section_id, this.option).node.removeAttribute('disabled'); + + return true; + } + so.editable = true; + + so = ss.taboption('field_general', form.MultiValue, 'use', _('Provider')); + so.value('', _('-- Please choose --')); + so.load = L.bind(hm.loadProviderLabel, so); + so.validate = function(section_id, value) { + if (this.section.getOption('include_all').formvalue(section_id) === '1' || + this.section.getOption('include_all_providers').formvalue(section_id) === '1') + this.getUIElement(section_id, this.option).node.setAttribute('disabled', ''); + else + this.getUIElement(section_id, this.option).node.removeAttribute('disabled'); + + return true; + } + so.editable = true; + + so = ss.taboption('field_general', form.Flag, 'include_all', _('Include all'), + _('Includes all Proxy Node and Provider.')); + so.default = so.disabled; + so.editable = true; + + so = ss.taboption('field_general', form.Flag, 'include_all_proxies', _('Include all node'), + _('Includes all Proxy Node.')); + so.default = so.disabled; + so.editable = true; + + so = ss.taboption('field_general', form.Flag, 'include_all_providers', _('Include all provider'), + _('Includes all Provider.')); + so.default = so.disabled; + so.editable = true; + + /* Override fields */ + so = ss.taboption('field_override', form.Flag, 'disable_udp', _('Disable UDP')); + so.default = so.disabled; + so.modalonly = true; + + so = ss.taboption('field_override', widgets.DeviceSelect, 'interface_name', _('Bind interface'), + _('Bind outbound interface.
') + + _('Priority: Proxy Node > Proxy Group > Global.')); + so.multiple = false; + so.noaliases = true; + so.modalonly = true; + + so = ss.taboption('field_override', form.Value, 'routing_mark', _('Routing mark'), + _('Priority: Proxy Node > Proxy Group > Global.')); + so.datatype = 'uinteger'; + so.modalonly = true; + + /* Health fields */ + /* Url-test/Fallback/Load-balance */ + so = ss.taboption('field_health', form.Value, 'url', _('Health check URL')); + so.default = hm.health_checkurls[0][0]; + hm.health_checkurls.forEach((res) => { + so.value.apply(so, res); + }) + so.validate = L.bind(hm.validateUrl, so); + so.depends({type: 'select', '!reverse': true}); + so.modalonly = true; + + so = ss.taboption('field_health', form.Value, 'interval', _('Health check interval'), + _('In seconds. %s will be used if empty.').format('600')); + so.placeholder = '600'; + so.validate = L.bind(hm.validateTimeDuration, so); + so.depends({type: 'select', '!reverse': true}); + so.modalonly = true; + + so = ss.taboption('field_health', form.Value, 'timeout', _('Health check timeout'), + _('In millisecond. %s will be used if empty.').format('5000')); + so.datatype = 'uinteger'; + so.placeholder = '5000'; + so.depends({type: 'select', '!reverse': true}); + so.modalonly = true; + + so = ss.taboption('field_health', form.Flag, 'lazy', _('Lazy'), + _('No testing is performed when this provider node is not in use.')); + so.default = so.enabled; + so.depends({type: 'select', '!reverse': true}); + so.modalonly = true; + + so = ss.taboption('field_health', form.Value, 'expected_status', _('Health check expected status'), + _('Expected HTTP code. 204 will be used if empty. ') + + _('For format see %s.') + .format('https://wiki.metacubex.one/config/proxy-groups/#expected-status', _('Expected status'))); + so.placeholder = '200/302/400-503'; + so.depends({type: 'select', '!reverse': true}); + so.modalonly = true; + + so = ss.taboption('field_health', form.Value, 'max_failed_times', _('Max count of failures'), + _('Exceeding this triggers a forced health check. 5 will be used if empty.')); + so.datatype = 'uinteger'; + so.placeholder = '5'; + so.depends({type: 'select', '!reverse': true}); + so.modalonly = true; + + /* Url-test fields */ + so = ss.taboption('field_general', form.Value, 'tolerance', _('Node switch tolerance'), + _('In millisecond. %s will be used if empty.').format('150')); + so.datatype = 'uinteger'; + so.placeholder = '150'; + so.depends('type', 'url-test'); + so.modalonly = true; + + /* Load-balance fields */ + so = ss.taboption('field_general', form.ListValue, 'strategy', _('Strategy'), + _('For details, see %s.') + .format('https://wiki.metacubex.one/config/proxy-groups/load-balance/#strategy', _('Strategy'))); + so.default = hm.load_balance_strategy[0][0]; + hm.load_balance_strategy.forEach((res) => { + so.value.apply(so, res); + }) + so.depends('type', 'load-balance'); + so.modalonly = true; + + /* General fields */ + so = ss.taboption('field_general', form.DynamicList, 'filter', _('Node filter'), + _('Filter nodes that meet keywords or regexps.')); + so.placeholder = '(?i)港|hk|hongkong|hong kong'; + so.modalonly = true; + + so = ss.taboption('field_general', form.DynamicList, 'exclude_filter', _('Node exclude filter'), + _('Exclude nodes that meet keywords or regexps.')); + so.placeholder = 'xxx'; + so.modalonly = true; + + so = ss.taboption('field_general', form.DynamicList, 'exclude_type', _('Node exclude type'), + _('Exclude matched node types. Available types see here.') + .format('https://wiki.metacubex.one/config/proxy-groups/#exclude-type')); + so.placeholder = 'Shadowsocks|Trojan'; + so.modalonly = true; + /* Proxy Group END */ + + /* Routing rules START */ + s.tab('rules', _('Routing rule')); + + /* Routing rules */ + o = s.taboption('rules', form.SectionValue, '_rules', form.GridSection, 'rules', null); + ss = o.subsection; + var prefmt = { 'prefix': '', 'suffix': '_host' }; + ss.addremove = true; + ss.rowcolors = true; + ss.sortable = true; + ss.nodescriptions = true; + ss.modaltitle = L.bind(hm.loadModalTitle, ss, _('Routing rule'), _('Add a routing rule')); + ss.sectiontitle = L.bind(hm.loadDefaultLabel, ss); + ss.renderSectionAdd = L.bind(hm.renderSectionAdd, ss, prefmt, false); + ss.handleAdd = L.bind(hm.handleAdd, ss, prefmt); + + so = ss.option(form.Value, 'label', _('Label')); + so.load = L.bind(hm.loadDefaultLabel, so); + so.validate = L.bind(hm.validateUniqueValue, so); + so.modalonly = true; + + so = ss.option(form.Flag, 'enabled', _('Enable')); + so.default = so.enabled; + so.editable = true; + + renderRules(ss, data[0]); + + so = ss.option(form.Flag, 'SUB-RULE', _('SUB-RULE')); + so.default = so.disabled; + so.load = function(section_id) { + return strToFlag(new RulesEntry(uci.get(data[0], section_id, 'entry')).subrule ? 'true' : 'false'); + } + so.validate = function(section_id, value) { + value = this.formvalue(section_id); + + this.section.getUIElement(section_id, 'detour').node.querySelector('select').disabled = (value === '1') ? 'true' : null; + + return true; + } + so.onchange = function(ev, section_id, value) { + var UIEl = this.section.getUIElement(section_id, 'entry'); + + var newvalue = new RulesEntry(UIEl.getValue()).setKey('subrule', value === '1' ? ' ' : false).toString(); + + UIEl.node.previousSibling.innerText = newvalue; + return UIEl.setValue(newvalue); + } + so.write = function() {}; + so.modalonly = true; + + so = ss.option(form.ListValue, 'sub_rule', _('Sub rule')); + so.load = function(section_id) { + hm.loadSubRuleGroup.call(this, section_id); + + return new RulesEntry(uci.get(data[0], section_id, 'entry')).subrule || ''; + } + so.onchange = function(ev, section_id, value) { + var UIEl = this.section.getUIElement(section_id, 'entry'); + + var newvalue = new RulesEntry(UIEl.getValue()).setKey('subrule', value).toString(); + + UIEl.node.previousSibling.innerText = newvalue; + return UIEl.setValue(newvalue); + } + so.rmempty = false; + so.write = function() {}; + so.depends('SUB-RULE', '1'); + so.modalonly = true; + /* Routing rules END */ + + /* Sub rules START */ + s.tab('subrules', _('Sub rule')); + + /* Sub rules */ + o = s.taboption('subrules', form.SectionValue, '_subrules', form.GridSection, 'subrules', null); + ss = o.subsection; + var prefmt = { 'prefix': '', 'suffix': '_subhost' }; + ss.addremove = true; + ss.rowcolors = true; + ss.sortable = true; + ss.nodescriptions = true; + ss.modaltitle = L.bind(hm.loadModalTitle, ss, _('Sub rule'), _('Add a sub rule')); + ss.sectiontitle = L.bind(hm.loadDefaultLabel, ss); + ss.renderSectionAdd = L.bind(hm.renderSectionAdd, ss, prefmt, false); + ss.handleAdd = L.bind(hm.handleAdd, ss, prefmt); + + so = ss.option(form.Value, 'label', _('Label')); + so.load = L.bind(hm.loadDefaultLabel, so); + so.validate = L.bind(hm.validateUniqueValue, so); + so.modalonly = true; + + so = ss.option(form.Flag, 'enabled', _('Enable')); + so.default = so.enabled; + so.editable = true; + + so = ss.option(form.Value, 'group', _('Sub rule group')); + so.value('sub-rule1'); + so.rmempty = false; + so.validate = L.bind(hm.validateAuthUsername, so); + so.editable = true; + + renderRules(ss, data[0]); + /* Sub rules END */ + + /* DNS settings START */ + s.tab('dns', _('DNS settings')); + + /* DNS settings */ + o = s.taboption('dns', form.SectionValue, '_dns', form.NamedSection, 'dns', 'fchomo', null); + ss = o.subsection; + + so = ss.option(form.Value, 'port', _('Listen port')); + so.datatype = 'port' + so.placeholder = '7853'; + so.rmempty = false; + + so = ss.option(form.Flag, 'ipv6', _('IPv6 support')); + so.default = so.enabled; + + so = ss.option(form.MultiValue, 'boot_server', _('Boot DNS server'), + _('Used to resolve the domain of the DNS server. Must be IP.')); + so.default = 'default-dns'; + so.load = L.bind(loadDNSServerLabel, so); + so.validate = L.bind(validateNameserver, so); + so.rmempty = false; + + so = ss.option(form.MultiValue, 'bootnode_server', _('Boot DNS server (Node)'), + _('Used to resolve the domain of the Proxy node.')); + so.default = 'default-dns'; + so.load = L.bind(loadDNSServerLabel, so); + so.validate = L.bind(validateNameserver, so); + so.rmempty = false; + + so = ss.option(form.MultiValue, 'default_server', _('Default DNS server')); + so.description = uci.get(data[0], so.section.section, 'fallback_server') ? _('Final DNS server (Used to Domestic-IP response)') : _('Final DNS server'); + so.default = 'default-dns'; + so.load = L.bind(loadDNSServerLabel, so); + so.validate = L.bind(validateNameserver, so); + so.rmempty = false; + + so = ss.option(form.MultiValue, 'fallback_server', _('Fallback DNS server')); + so.description = uci.get(data[0], so.section.section, 'fallback_server') ? _('Final DNS server (Used to Overseas-IP response)') : _('Fallback DNS server'); + so.load = L.bind(loadDNSServerLabel, so); + so.validate = L.bind(validateNameserver, so); + so.onchange = function(ev, section_id, value) { + var ddesc = this.section.getUIElement(section_id, 'default_server').node.nextSibling; + var fdesc = ev.target.nextSibling; + if (value.length > 0) { + ddesc.innerHTML = _('Final DNS server (Used to Domestic-IP response)'); + fdesc.innerHTML = _('Final DNS server (Used to Overseas-IP response)'); + } else { + ddesc.innerHTML = _('Final DNS server'); + fdesc.innerHTML = _('Fallback DNS server'); + } + } + /* DNS settings END */ + + /* DNS server START */ + s.tab('dns_server', _('DNS server')); + + /* DNS server */ + o = s.taboption('dns_server', form.SectionValue, '_dns_server', form.GridSection, 'dns_server', null); + ss = o.subsection; + var prefmt = { 'prefix': 'dns_', 'suffix': '' }; + ss.addremove = true; + ss.rowcolors = true; + ss.sortable = true; + ss.nodescriptions = true; + ss.modaltitle = L.bind(hm.loadModalTitle, ss, _('DNS server'), _('Add a DNS server')); + ss.sectiontitle = L.bind(hm.loadDefaultLabel, ss); + ss.renderSectionAdd = L.bind(hm.renderSectionAdd, ss, prefmt, true); + ss.handleAdd = L.bind(hm.handleAdd, ss, prefmt); + + so = ss.option(form.Value, 'label', _('Label')); + so.load = L.bind(hm.loadDefaultLabel, so); + so.validate = L.bind(hm.validateUniqueValue, so); + so.modalonly = true; + + so = ss.option(form.Flag, 'enabled', _('Enable')); + so.default = so.enabled; + so.editable = true; + + so = ss.option(form.DummyValue, 'address', _('Address')); + so.write = L.bind(form.AbstractValue.prototype.write, so); + so.remove = L.bind(form.AbstractValue.prototype.remove, so); + so.editable = true; + + so = ss.option(form.Value, 'addr', _('Address')); + so.load = function(section_id) { + return new DNSAddress(uci.get(data[0], section_id, 'address')).addr; + } + so.validate = function(section_id, value) { + if (value.match('#')) + return _('Expecting: %s').format(_('No add\'l params')); + + // params only available on DoH + // https://github.com/muink/mihomo/blob/43f21c0b412b7a8701fe7a2ea6510c5b985a53d6/config/config.go#L1211C8-L1211C14 + if (value.match(/^https?:\/\//)){ + this.section.getUIElement(section_id, 'h3').node.querySelector('input').disabled = null; + this.section.getUIElement(section_id, 'ecs').node.querySelector('input').disabled = null; + this.section.getUIElement(section_id, 'ecs-override').node.querySelector('input').disabled = null; + } else { + var UIEl = this.section.getUIElement(section_id, 'address'); + + var newvalue = new DNSAddress(UIEl.getValue()).setParam('h3').setParam('ecs').setParam('ecs-override').toString(); + + UIEl.node.previousSibling.innerText = newvalue; + UIEl.setValue(newvalue); + + ['h3', 'ecs', 'ecs-override'].forEach((opt) => { + let UIEl = this.section.getUIElement(section_id, opt); + UIEl.setValue(''); + UIEl.node.querySelector('input').disabled = 'true'; + }); + } + + return true; + } + so.onchange = function(ev, section_id, value) { + var UIEl = this.section.getUIElement(section_id, 'address'); + + var newvalue = ('N' + UIEl.getValue()).replace(/^[^#]+/, value); + + UIEl.node.previousSibling.innerText = newvalue; + return UIEl.setValue(newvalue); + } + so.write = function() {}; + so.rmempty = false; + so.modalonly = true; + + so = ss.option(form.ListValue, 'detour', _('Proxy group')); + so.renderWidget = function(/* ... */) { + var frameEl = form.ListValue.prototype.renderWidget.apply(this, arguments); + + frameEl.querySelector('select').style["min-width"] = '10em'; + + return frameEl; + } + so.load = function(section_id) { + hm.loadProxyGroupLabel.call(this, hm.preset_outbound.dns, section_id); + + return new DNSAddress(uci.get(data[0], section_id, 'address')).parseParam('detour'); + } + so.onchange = function(ev, section_id, value) { + var UIEl = this.section.getUIElement(section_id, 'address'); + + var newvalue = new DNSAddress(UIEl.getValue()).setParam('detour', value).toString(); + + UIEl.node.previousSibling.innerText = newvalue; + return UIEl.setValue(newvalue); + } + so.write = function() {}; + so.editable = true; + + so = ss.option(form.Flag, 'h3', _('HTTP/3')); + so.default = so.disabled; + so.load = function(section_id) { + return strToFlag(new DNSAddress(uci.get(data[0], section_id, 'address')).parseParam('h3')); + } + so.onchange = function(ev, section_id, value) { + var UIEl = this.section.getUIElement(section_id, 'address'); + + var newvalue = new DNSAddress(UIEl.getValue()).setParam('h3', flagToStr(value)).toString(); + + UIEl.node.previousSibling.innerText = newvalue; + return UIEl.setValue(newvalue); + } + so.write = function() {}; + so.modalonly = true; + + so = ss.option(form.Value, 'ecs', _('EDNS Client Subnet')); + so.datatype = 'cidr'; + so.load = function(section_id) { + return new DNSAddress(uci.get(data[0], section_id, 'address')).parseParam('ecs'); + } + so.onchange = function(ev, section_id, value) { + var UIEl = this.section.getUIElement(section_id, 'address'); + + var newvalue = new DNSAddress(UIEl.getValue()).setParam('ecs', value).toString(); + + UIEl.node.previousSibling.innerText = newvalue; + UIEl.setValue(newvalue); + } + so.write = function() {}; + so.modalonly = true; + + so = ss.option(form.Flag, 'ecs-override', _('ECS override'), + _('Override ECS in original request.')); + so.default = so.disabled; + so.load = function(section_id) { + return strToFlag(new DNSAddress(uci.get(data[0], section_id, 'address')).parseParam('ecs-override')); + } + so.onchange = function(ev, section_id, value) { + var UIEl = this.section.getUIElement(section_id, 'address'); + + var newvalue = new DNSAddress(UIEl.getValue()).setParam('ecs-override', flagToStr(value)).toString(); + + UIEl.node.previousSibling.innerText = newvalue; + UIEl.setValue(newvalue); + } + so.write = function() {}; + so.depends({'ecs': /.+/}); + so.modalonly = true; + /* DNS server END */ + + /* DNS policy START */ + s.tab('dns_policy', _('DNS policy')); + + /* DNS policy */ + o = s.taboption('dns_policy', form.SectionValue, '_dns_policy', form.GridSection, 'dns_policy', null); + ss = o.subsection; + var prefmt = { 'prefix': '', 'suffix': '_domain' }; + ss.addremove = true; + ss.rowcolors = true; + ss.sortable = true; + ss.nodescriptions = true; + ss.modaltitle = L.bind(hm.loadModalTitle, ss, _('DNS policy'), _('Add a DNS policy')); + ss.sectiontitle = L.bind(hm.loadDefaultLabel, ss); + ss.renderSectionAdd = L.bind(hm.renderSectionAdd, ss, prefmt, false); + ss.handleAdd = L.bind(hm.handleAdd, ss, prefmt); + + so = ss.option(form.Value, 'label', _('Label')); + so.load = L.bind(hm.loadDefaultLabel, so); + so.validate = L.bind(hm.validateUniqueValue, so); + so.modalonly = true; + + so = ss.option(form.Flag, 'enabled', _('Enable')); + so.default = so.enabled; + so.editable = true; + + so = ss.option(form.ListValue, 'type', _('Type')); + so.value('domain', _('Domain')); + so.value('geosite', _('Geosite')); + so.value('rule_set', _('Rule set')); + so.default = 'domain'; + + so = ss.option(form.DynamicList, 'domain', _('Domain'), + _('Match domain. Support wildcards.')); + so.depends('type', 'domain'); + so.modalonly = true; + + so = ss.option(form.DynamicList, 'geosite', _('Geosite'), + _('Match geosite.')); + so.depends('type', 'geosite'); + so.modalonly = true; + + so = ss.option(form.MultiValue, 'rule_set', _('Rule set'), + _('Match rule set.')); + so.value('', _('-- Please choose --')); + so.load = L.bind(hm.loadRulesetLabel, so, ['domain', 'classical']); + so.depends('type', 'rule_set'); + so.modalonly = true; + + so = ss.option(form.DummyValue, '_entry', _('Entry')); + so.load = function(section_id) { + var option = uci.get(data[0], section_id, 'type'); + + return uci.get(data[0], section_id, option)?.join(','); + } + so.modalonly = false; + + so = ss.option(form.MultiValue, 'server', _('DNS server')); + so.value('default-dns'); + so.default = 'default-dns'; + so.load = L.bind(loadDNSServerLabel, so); + so.validate = L.bind(validateNameserver, so); + so.rmempty = false; + so.editable = true; + + so = ss.option(form.ListValue, 'proxy', _('Proxy group'), + _('Override the Proxy group of DNS server.')); + so.renderWidget = function(/* ... */) { + var frameEl = form.ListValue.prototype.renderWidget.apply(this, arguments); + + frameEl.querySelector('select').style["min-width"] = '10em'; + + return frameEl; + } + so.default = hm.preset_outbound.direct[0][0]; + hm.preset_outbound.direct.forEach((res) => { + so.value.apply(so, res); + }) + so.load = L.bind(hm.loadProxyGroupLabel, so, hm.preset_outbound.direct); + so.editable = true; + /* DNS policy END */ + + /* Fallback filter START */ + s.tab('fallback_filter', _('Fallback filter')); + + /* Fallback filter */ + o = s.taboption('fallback_filter', form.SectionValue, '_fallback_filter', form.NamedSection, 'dns', 'fchomo', null); + o.depends({'fchomo.dns.fallback_server': /.+/}); + ss = o.subsection; + + so = ss.option(form.Flag, 'fallback_filter_geoip', _('Geoip enable')); + so.default = so.enabled; + + so = ss.option(form.Value, 'fallback_filter_geoip_code', _('Geoip code'), + _('Match response with geoip.
') + + _('The matching %s will be deemed as not-poisoned.').format(_('IP'))); + so.default = 'cn'; + so.placeholder = 'cn'; + so.rmempty = false; + so.retain = true; + so.depends('fallback_filter_geoip', '1'); + + so = ss.option(form.DynamicList, 'fallback_filter_geosite', _('Geosite'), + _('Match geosite.
') + + _('The matching %s will be deemed as poisoned.').format(_('Domain'))); + + so = ss.option(form.DynamicList, 'fallback_filter_ipcidr', _('IP CIDR'), + _('Match response with ipcidr.
') + + _('The matching %s will be deemed as poisoned.').format(_('IP'))); + so.datatype = 'list(cidr)'; + + so = ss.option(form.DynamicList, 'fallback_filter_domain', _('Domain'), + _('Match domain. Support wildcards.
') + + _('The matching %s will be deemed as poisoned.').format(_('Domain'))); + /* Fallback filter END */ + + return m.render(); + } +}); diff --git a/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/global.js b/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/global.js new file mode 100644 index 0000000000..b4be49b222 --- /dev/null +++ b/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/global.js @@ -0,0 +1,743 @@ +'use strict'; +'require form'; +'require network'; +'require poll'; +'require rpc'; +'require uci'; +'require ui'; +'require view'; + +'require fchomo as hm'; +'require tools.firewall as fwtool'; +'require tools.widgets as widgets'; + +const callResVersion = rpc.declare({ + object: 'luci.fchomo', + method: 'resources_get_version', + params: ['type', 'repo'], + expect: { '': {} } +}); + +const callCrondSet = rpc.declare({ + object: 'luci.fchomo', + method: 'crond_set', + params: ['type', 'expr'], + expect: { '': {} } +}); + +function handleResUpdate(type, repo) { + const callResUpdate = rpc.declare({ + object: 'luci.fchomo', + method: 'resources_update', + params: ['type', 'repo'], + expect: { '': {} } + }); + + // Dynamic repo + var label; + if (repo) { + var section_id = this.section.section; + var weight = document.getElementById(this.cbid(section_id)); + if (weight) + repo = weight.firstChild.value, + label = weight.firstChild.selectedOptions[0].label; + } + + return L.resolveDefault(callResUpdate(type, repo), {}).then((res) => { + switch (res.status) { + case 0: + this.description = (repo ? label + ' ' : '') + _('Successfully updated.'); + break; + case 1: + this.description = (repo ? label + ' ' : '') + _('Update failed.'); + break; + case 2: + this.description = (repo ? label + ' ' : '') + _('Already in updating.'); + break; + case 3: + this.description = (repo ? label + ' ' : '') + _('Already at the latest version.'); + break; + default: + this.description = (repo ? label + ' ' : '') + _('Unknown error.'); + break; + } + + return this.map.reset(); + }); +} + +function renderResVersion(El, type, repo) { + return L.resolveDefault(callResVersion(type, repo), {}).then((res) => { + var resEl = E([ + E('button', { + 'class': 'cbi-button cbi-button-apply', + 'click': ui.createHandlerFn(this, handleResUpdate, type, repo) + }, [ _('Check update') ]), + updateResVersion(E('span', { style: 'border: unset; font-weight: bold; align-items: center' }), res.version) + ]); + + if (El) { + El.appendChild(resEl); + El.lastChild.style.display = 'flex'; + } else + El = resEl; + + return El; + }); +} + +function updateResVersion(El, version) { + if (El) { + El.style.color = version ? 'green' : 'red'; + El.innerHTML = ' %s'.format(version || _('not found')); + } + + return El; +} + +return view.extend({ + load: function() { + return Promise.all([ + uci.load('fchomo'), + hm.getFeatures(), + network.getHostHints(), + hm.getServiceStatus('mihomo-c'), + hm.getClashAPI('mihomo-c'), + hm.getServiceStatus('mihomo-s'), + hm.getClashAPI('mihomo-s'), + callResVersion('geoip').then((res) => { return res.version }), + callResVersion('geosite').then((res) => { return res.version }) + ]); + }, + + render: function(data) { + var features = data[1], + hosts = data[2]?.hosts, + CisRunning = data[3], + CclashAPI = data[4], + SisRunning = data[5], + SclashAPI = data[6], + res_ver_geoip = data[7], + res_ver_geosite = data[8]; + + var dashboard_repo = uci.get(data[0], 'api', 'dashboard_repo'); + + let m, s, o, ss, so; + + m = new form.Map('fchomo', _('FullCombo Mihomo'), + ''); + + s = m.section(form.NamedSection, 'config', 'fchomo'); + + /* Overview START */ + s.tab('status', _('Overview')); + + /* Service status */ + o = s.taboption('status', form.SectionValue, '_status', form.NamedSection, 'config', 'fchomo', _('Service status')); + ss = o.subsection; + + so = ss.option(form.DummyValue, '_core_version', _('Core version')); + so.cfgvalue = function() { + return E('strong', [features.core_version || _('Unknown')]); + } + + so = ss.option(form.DummyValue, '_luciapp_version', _('Application version')); + so.cfgvalue = function() { + return E('strong', [features.luciapp_version || _('Unknown')]); + } + + so = ss.option(form.DummyValue, '_client_status', _('Client status')); + so.cfgvalue = function() { return hm.renderStatus(hm, '_client_bar', CisRunning ? { ...CclashAPI, dashboard_repo: dashboard_repo } : false, 'mihomo-c') } + poll.add(function() { + return hm.getServiceStatus('mihomo-c').then((isRunning) => { + hm.updateStatus(hm, document.getElementById('_client_bar'), isRunning ? { dashboard_repo: dashboard_repo } : false, 'mihomo-c'); + }); + }) + + so = ss.option(form.DummyValue, '_server_status', _('Server status')); + so.cfgvalue = function() { return hm.renderStatus(hm, '_server_bar', SisRunning ? { ...SclashAPI, dashboard_repo: dashboard_repo } : false, 'mihomo-s') } + poll.add(function() { + return hm.getServiceStatus('mihomo-s').then((isRunning) => { + hm.updateStatus(hm, document.getElementById('_server_bar'), isRunning ? { dashboard_repo: dashboard_repo } : false, 'mihomo-s'); + }); + }) + + so = ss.option(form.Button, '_reload', _('Reload All')); + so.inputtitle = _('Reload'); + so.inputstyle = 'apply'; + so.onclick = L.bind(hm.handleReload, so, null); + + so = ss.option(form.DummyValue, '_conn_check', _('Connection check')); + so.cfgvalue = function() { + const callConnStat = rpc.declare({ + object: 'luci.fchomo', + method: 'connection_check', + params: ['url'], + expect: { '': {} } + }); + + var ElId = '_connection_check_results'; + + return E([ + E('button', { + 'class': 'cbi-button cbi-button-apply', + 'click': ui.createHandlerFn(this, function() { + var weight = document.getElementById(ElId); + + weight.innerHTML = ''; + return hm.checkurls.forEach((site) => { + L.resolveDefault(callConnStat(site[0]), {}).then((res) => { + weight.innerHTML += ' %s'.format((res.httpcode && res.httpcode.match(/^20\d$/)) ? 'green' : 'red', site[1]); + }); + }); + }) + }, [ _('Check') ]), + E('strong', { id: ElId }, [ + E('span', { style: 'color:gray' }, ' ' + _('unchecked')) + ]) + ]); + } + + /* Resources management */ + o = s.taboption('status', form.SectionValue, '_config', form.NamedSection, 'resources', 'fchomo', _('Resources management')); + ss = o.subsection; + + if (!res_ver_geoip || !res_ver_geosite) { + so = ss.option(form.Button, '_upload_initia', _('Upload initial package')); + so.inputstyle = 'action'; + so.inputtitle = _('Upload...'); + so.onclick = L.bind(hm.uploadInitialPack, so); + } + + so = ss.option(form.Flag, 'auto_update', _('Auto update'), + _('Auto update resources.')); + so.default = so.disabled; + so.rmempty = false; + so.write = function(section_id, formvalue) { + if (formvalue == 1) { + callCrondSet('resources', uci.get(data[0], section_id, 'auto_update_expr')); + } else + callCrondSet('resources'); + + return this.super('write', section_id, formvalue); + } + + so = ss.option(form.Value, 'auto_update_expr', _('Cron expression'), + _('The default value is 2:00 every day.')); + so.default = '0 2 * * *'; + so.placeholder = '0 2 * * *'; + so.rmempty = false; + so.retain = true; + so.depends('auto_update', '1'); + so.write = function(section_id, formvalue) { + callCrondSet('resources', formvalue); + + return this.super('write', section_id, formvalue); + }; + so.remove = function(section_id) { + callCrondSet('resources'); + + return this.super('remove', section_id); + }; + + so = ss.option(form.ListValue, '_dashboard_version', _('Dashboard version')); + so.default = hm.dashrepos[0][0]; + hm.dashrepos.forEach((repo) => { + so.value.apply(so, repo); + }) + so.renderWidget = function(/* ... */) { + var El = form.ListValue.prototype.renderWidget.apply(this, arguments); + + El.className = 'control-group'; + El.firstChild.style.width = '10em'; + + return renderResVersion.call(this, El, 'dashboard', this.default); + } + so.onchange = function(ev, section_id, value) { + this.default = value; + + var weight = ev.target; + if (weight) + return L.resolveDefault(callResVersion('dashboard', value), {}).then((res) => { + updateResVersion(weight.lastChild, res.version); + }); + } + so.write = function() {}; + + so = ss.option(form.DummyValue, '_geoip_version', _('GeoIP version')); + so.cfgvalue = function() { return renderResVersion.call(this, null, 'geoip') }; + + so = ss.option(form.DummyValue, '_geosite_version', _('GeoSite version')); + so.cfgvalue = function() { return renderResVersion.call(this, null, 'geosite') }; + + so = ss.option(form.DummyValue, '_asn_version', _('ASN version')); + so.cfgvalue = function() { return renderResVersion.call(this, null, 'asn') }; + + so = ss.option(form.DummyValue, '_china_ip4_version', _('China IPv4 list version')); + so.cfgvalue = function() { return renderResVersion.call(this, null, 'china_ip4') }; + + so = ss.option(form.DummyValue, '_china_ip6_version', _('China IPv6 list version')); + so.cfgvalue = function() { return renderResVersion.call(this, null, 'china_ip6') }; + + so = ss.option(form.DummyValue, '_gfw_list_version', _('GFW list version')); + so.cfgvalue = function() { return renderResVersion.call(this, null, 'gfw_list') }; + + so = ss.option(form.DummyValue, '_china_list_version', _('China list version')); + so.cfgvalue = function() { return renderResVersion.call(this, null, 'china_list') }; + /* Overview END */ + + /* General START */ + s.tab('general', _('General')); + + /* General settings */ + o = s.taboption('general', form.SectionValue, '_global', form.NamedSection, 'global', 'fchomo', _('General settings')); + ss = o.subsection; + + so = ss.option(form.ListValue, 'mode', _('Operation mode')); + so.value('direct', _('Direct')); + so.value('rule', _('Rule')); + so.value('global', _('Global')); + so.default = 'rule'; + + so = ss.option(form.ListValue, 'find_process_mode', _('Process matching mode')); + so.value('always', _('Enable')); + so.value('strict', _('Auto')); + so.value('off', _('Disable')); + so.default = 'off'; + + so = ss.option(form.ListValue, 'log_level', _('Log level')); + so.value('silent', _('Silent')); + so.value('error', _('Error')); + so.value('warning', _('Warning')); + so.value('info', _('Info')); + so.value('debug', _('Debug')); + so.default = 'warning'; + + so = ss.option(form.Flag, 'etag_support', _('ETag support')); + so.default = so.enabled; + + so = ss.option(form.Flag, 'ipv6', _('IPv6 support')); + so.default = so.enabled; + + so = ss.option(form.Flag, 'unified_delay', _('Unified delay')); + so.default = so.disabled; + + so = ss.option(form.Flag, 'tcp_concurrent', _('TCP concurrency')); + so.default = so.disabled; + + so = ss.option(form.Value, 'keep_alive_interval', _('TCP-Keep-Alive interval'), + _('In seconds. %s will be used if empty.').format('30')); + so.placeholder = '30'; + so.validate = L.bind(hm.validateTimeDuration, so); + + so = ss.option(form.Value, 'keep_alive_idle', _('TCP-Keep-Alive idle timeout'), + _('In seconds. %s will be used if empty.').format('600')); + so.placeholder = '600'; + so.validate = L.bind(hm.validateTimeDuration, so); + + /* Global Authentication */ + o = s.taboption('general', form.SectionValue, '_global', form.NamedSection, 'global', 'fchomo', _('Global Authentication')); + ss = o.subsection; + + so = ss.option(form.DynamicList, 'authentication', _('User Authentication')); + so.datatype = 'list(string)'; + so.placeholder = 'user1:pass1'; + so.validate = L.bind(hm.validateAuth, so); + + so = ss.option(form.DynamicList, 'skip_auth_prefixes', _('No Authentication IP ranges')); + so.datatype = 'list(cidr)'; + so.placeholder = '127.0.0.1/8'; + /* General END */ + + /* Inbound START */ + s.tab('inbound', _('Inbound')); + + /* Listen ports */ + o = s.taboption('inbound', form.SectionValue, '_inbound', form.NamedSection, 'inbound', 'fchomo', _('Listen ports')); + ss = o.subsection; + + so = ss.option(form.Value, 'mixed_port', _('Mixed port')); + so.datatype = 'port'; + so.placeholder = '7890'; + so.rmempty = false; + + so = ss.option(form.Value, 'redir_port', _('Redir port')); + so.datatype = 'port'; + so.placeholder = '7891'; + so.rmempty = false; + + so = ss.option(form.Value, 'tproxy_port', _('Tproxy port')); + so.datatype = 'port'; + so.placeholder = '7892'; + so.rmempty = false; + + so = ss.option(form.Value, 'tunnel_port', _('DNS port')); + so.datatype = 'port'; + so.placeholder = '7893'; + so.rmempty = false; + + so = ss.option(form.ListValue, 'proxy_mode', _('Proxy mode')); + so.value('redir', _('Redirect TCP')); + if (features.hm_has_tproxy) + so.value('redir_tproxy', _('Redirect TCP + TProxy UDP')); + if (features.hm_has_ip_full && features.hm_has_tun) { + so.value('redir_tun', _('Redirect TCP + Tun UDP')); + so.value('tun', _('Tun TCP/UDP')); + } else + so.description = _('To enable Tun support, you need to install ip-full and kmod-tun'); + so.default = 'redir_tproxy'; + so.rmempty = false; + + /* Tun settings */ + o = s.taboption('inbound', form.SectionValue, '_inbound', form.NamedSection, 'inbound', 'fchomo', _('Tun settings')); + ss = o.subsection; + + so = ss.option(form.ListValue, 'tun_stack', _('Stack'), + _('Tun stack.')); + so.value('system', _('System')); + if (features.with_gvisor) { + so.value('gvisor', _('gVisor')); + so.value('mixed', _('Mixed')); + } + so.default = 'system'; + so.rmempty = false; + so.onchange = function(ev, section_id, value) { + var desc = ev.target.nextSibling; + if (value === 'mixed') + desc.innerHTML = _('Mixed system TCP stack and gVisor UDP stack.'); + else if (value === 'gvisor') + desc.innerHTML = _('Based on google/gvisor.'); + else if (value === 'system') + desc.innerHTML = _('Less compatibility and sometimes better performance.'); + } + + so = ss.option(form.Value, 'tun_mtu', _('MTU')); + so.datatype = 'uinteger'; + so.placeholder = '9000'; + + so = ss.option(form.Flag, 'tun_gso', _('Generic segmentation offload')); + so.default = so.disabled; + + so = ss.option(form.Value, 'tun_gso_max_size', _('Segment maximum size')); + so.datatype = 'uinteger'; + so.placeholder = '65536'; + + so = ss.option(form.Value, 'tun_udp_timeout', _('UDP NAT expiration time'), + _('In seconds. %s will be used if empty.').format('300')); + so.placeholder = '300'; + so.validate = L.bind(hm.validateTimeDuration, so); + + so = ss.option(form.Flag, 'tun_endpoint_independent_nat', _('Endpoint-Independent NAT'), + _('Performance may degrade slightly, so it is not recommended to enable on when it is not needed.')); + so.default = so.disabled; + /* Inbound END */ + + /* TLS START */ + s.tab('tls', _('TLS')); + + /* TLS settings */ + o = s.taboption('tls', form.SectionValue, '_tls', form.NamedSection, 'tls', 'fchomo', null); + ss = o.subsection; + + so = ss.option(form.ListValue, 'global_client_fingerprint', _('Global client fingerprint')); + so.default = hm.tls_client_fingerprints[0][0]; + hm.tls_client_fingerprints.forEach((res) => { + so.value.apply(so, res); + }) + + so = ss.option(form.Value, 'tls_cert_path', _('API TLS certificate path')); + so.datatype = 'file'; + so.value('/etc/ssl/acme/example.crt'); + + so = ss.option(form.Value, 'tls_key_path', _('API TLS private key path')); + so.datatype = 'file'; + so.value('/etc/ssl/acme/example.key'); + /* TLS END */ + + /* API START */ + s.tab('api', _('API')); + + /* API settings */ + o = s.taboption('api', form.SectionValue, '_api', form.NamedSection, 'api', 'fchomo', null); + ss = o.subsection; + + so = ss.option(form.ListValue, 'dashboard_repo', _('Select Dashboard')); + so.default = hm.dashrepos[0][0]; + so.load = function(section_id) { + delete this.keylist; + delete this.vallist; + + this.value('', _('-- Please choose --')); + hm.dashrepos.forEach((repo) => { + L.resolveDefault(callResVersion('dashboard', repo[0]), {}).then((res) => { + this.value(repo[0], repo[1] + ' - ' + (res.version || _('Not Installed'))); + }); + }); + + return this.super('load', section_id); + } + so.rmempty = false; + + so = ss.option(form.DynamicList, 'external_controller_cors_allow_origins', _('CORS Allow origins'), + _('CORS allowed origins, * will be used if empty.')); + so.placeholder = 'https://yacd.metacubex.one'; + + so = ss.option(form.Flag, 'external_controller_cors_allow_private_network', _('CORS Allow private network'), + _('Allow access from private network.
' + + 'To access the API on a private network from a public website, it must be enabled.')); + so.default = so.enabled; + + so = ss.option(form.Value, 'external_controller_port', _('API HTTP port')); + so.datatype = 'port'; + so.placeholder = '9090'; + + so = ss.option(form.Value, 'external_controller_tls_port', _('API HTTPS port')); + so.datatype = 'port'; + so.placeholder = '9443'; + so.depends({'fchomo.tls.tls_cert_path': /^\/.+/, 'fchomo.tls.tls_key_path': /^\/.+/}); + + so = ss.option(form.Value, 'external_doh_server', _('API DoH service')); + so.placeholder = '/dns-query'; + so.depends({'external_controller_tls_port': /\d+/}); + + so = ss.option(form.Value, 'secret', _('API secret'), + _('Random will be used if empty.')); + so.password = true; + /* API END */ + + /* Sniffer START */ + s.tab('sniffer', _('Sniffer')); + + /* Sniffer settings */ + o = s.taboption('sniffer', form.SectionValue, '_sniffer', form.NamedSection, 'sniffer', 'fchomo', _('Sniffer settings')); + ss = o.subsection; + + so = ss.option(form.Flag, 'override_destination', _('Override destination'), + _('Override the connection destination address with the sniffed domain.')); + so.default = so.enabled; + + so = ss.option(form.DynamicList, 'force_domain', _('Forced sniffing domain')); + so.datatype = 'list(string)'; + + so = ss.option(form.DynamicList, 'skip_domain', _('Skiped sniffing domain')); + so.datatype = 'list(string)'; + + so = ss.option(form.DynamicList, 'skip_src_address', _('Skiped sniffing src address')); + so.datatype = 'list(cidr)'; + + so = ss.option(form.DynamicList, 'skip_dst_address', _('Skiped sniffing dst address')); + so.datatype = 'list(cidr)'; + + /* Sniff protocol settings */ + o = s.taboption('sniffer', form.SectionValue, '_sniffer_sniff', form.GridSection, 'sniff', _('Sniff protocol')); + ss = o.subsection; + ss.anonymous = true; + ss.addremove = false; + ss.rowcolors = true; + ss.sortable = true; + ss.nodescriptions = true; + + so = ss.option(form.Flag, 'enabled', _('Enable')); + so.default = so.enabled; + so.editable = true; + + so = ss.option(form.ListValue, 'protocol', _('Protocol')); + so.value('HTTP'); + so.value('TLS'); + so.value('QUIC'); + so.readonly = true; + + so = ss.option(form.DynamicList, 'ports', _('Ports')); + so.datatype = 'list(or(port, portrange))'; + + so = ss.option(form.Flag, 'override_destination', _('Override destination')); + so.default = so.enabled; + so.editable = true; + /* Sniffer END */ + + /* Experimental START */ + s.tab('experimental', _('Experimental')); + + /* Experimental settings */ + o = s.taboption('experimental', form.SectionValue, '_experimental', form.NamedSection, 'experimental', 'fchomo', null); + ss = o.subsection; + + so = ss.option(form.Flag, 'quic_go_disable_gso', _('Disable GSO of quic-go')); + so.default = so.disabled; + + so = ss.option(form.Flag, 'quic_go_disable_ecn', _('Disable ECN of quic-go')); + so.default = so.disabled; + + so = ss.option(form.Flag, 'dialer_ip4p_convert', _('Enable IP4P conversion for outbound connections') + .format('https://github.com/heiher/natmap/wiki/faq#%E5%9F%9F%E5%90%8D%E8%AE%BF%E9%97%AE%E6%98%AF%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E7%9A%84')); + so.default = so.disabled; + /* Experimental END */ + + /* ACL START */ + s.tab('control', _('Access Control')); + + /* Access Control settings */ + o = s.taboption('control', form.SectionValue, '_control', form.NamedSection, 'routing', 'fchomo', null); + ss = o.subsection; + + /* Interface control */ + ss.tab('interface', _('Interface Control')); + + so = ss.taboption('interface', widgets.DeviceSelect, 'listen_interfaces', _('Listen interfaces'), + _('Only process traffic from specific interfaces. Leave empty for all.')); + so.multiple = true; + so.noaliases = true; + + so = ss.taboption('interface', widgets.DeviceSelect, 'bind_interface', _('Bind interface'), + _('Bind outbound traffic to specific interface. Leave empty to auto detect.
') + + _('Priority: Proxy Node > Proxy Group > Global.')); + so.multiple = false; + so.noaliases = true; + + so = ss.taboption('interface', form.Value, 'route_table_id', _('Routing table ID')); + so.ucisection = 'config'; + so.datatype = 'uinteger'; + so.placeholder = '2022'; + so.rmempty = false; + + so = ss.taboption('interface', form.Value, 'route_rule_pref', _('Routing rule priority')); + so.ucisection = 'config'; + so.datatype = 'uinteger'; + so.placeholder = '9000'; + so.rmempty = false; + + so = ss.taboption('interface', form.Value, 'self_mark', _('Routing mark'), + _('Priority: Proxy Node > Proxy Group > Global.')); + so.ucisection = 'config'; + so.datatype = 'uinteger'; + so.placeholder = '200'; + so.rmempty = false; + + so = ss.taboption('interface', form.Value, 'tproxy_mark', _('Tproxy Fwmark')); + so.ucisection = 'config'; + so.placeholder = '201 or 0xc9/0xff'; + so.rmempty = false; + + so = ss.taboption('interface', form.Value, 'tun_mark', _('Tun Fwmark')); + so.ucisection = 'config'; + so.placeholder = '202 or 0xca/0xff'; + so.rmempty = false; + + /* Access control */ + ss.tab('access_control', _('Access Control')); + + so = ss.taboption('access_control', form.ListValue, 'lan_filter', _('Users filter mode')); + so.value('', _('All allowed')); + so.value('white_list', _('White list')); + so.value('black_list', _('Black list')); + + so = fwtool.addIPOption(ss, 'access_control', 'lan_direct_ipv4_ips', _('Direct IPv4 IP-s'), null, 'ipv4', hosts, true); + so.depends('lan_filter', 'black_list'); + + so = fwtool.addIPOption(ss, 'access_control', 'lan_direct_ipv6_ips', _('Direct IPv6 IP-s'), null, 'ipv6', hosts, true); + so.depends({'lan_filter': 'black_list', 'fchomo.global.ipv6': '1'}); + + so = fwtool.addMACOption(ss, 'access_control', 'lan_direct_mac_addrs', _('Direct MAC-s'), null, hosts); + so.depends('lan_filter', 'black_list'); + + so = fwtool.addIPOption(ss, 'access_control', 'lan_proxy_ipv4_ips', _('Proxy IPv4 IP-s'), null, 'ipv4', hosts, true); + so.depends('lan_filter', 'white_list'); + + so = fwtool.addIPOption(ss, 'access_control', 'lan_proxy_ipv6_ips', _('Proxy IPv6 IP-s'), null, 'ipv6', hosts, true); + so.depends({'lan_filter': 'white_list', 'fchomo.global.ipv6': '1'}); + + so = fwtool.addMACOption(ss, 'access_control', 'lan_proxy_mac_addrs', _('Proxy MAC-s'), null, hosts); + so.depends('lan_filter', 'white_list'); + + so = ss.taboption('access_control', form.Flag, 'proxy_router', _('Proxy routerself')); + so.default = so.enabled; + + /* Routing control */ + ss.tab('routing_control', _('Routing Control')); + + so = ss.taboption('routing_control', form.Value, 'routing_tcpport', _('Routing ports') + ' (TCP)', + _('Specify target ports to be proxied. Multiple ports must be separated by commas.')); + so.value('', _('All ports')); + so.value('common', _('Common ports only (bypass P2P traffic)')); + so.value('common_stun', _('Common and STUN ports')); + so.validate = L.bind(hm.validateCommonPort, so); + + so = ss.taboption('routing_control', form.Value, 'routing_udpport', _('Routing ports') + ' (UDP)', + _('Specify target ports to be proxied. Multiple ports must be separated by commas.')); + so.value('', _('All ports')); + so.value('common', _('Common ports only (bypass P2P traffic)')); + so.value('common_stun', _('Common and STUN ports')); + so.validate = L.bind(hm.validateCommonPort, so); + + so = ss.taboption('routing_control', form.ListValue, 'routing_mode', _('Routing mode'), + _('Routing mode of the traffic enters mihomo via firewall rules.')); + so.value('', _('All allowed')); + so.value('bypass_cn', _('Bypass CN')); + so.value('routing_gfw', _('Routing GFW')); + + so = ss.taboption('routing_control', form.Flag, 'routing_domain', _('Handle domain'), + _('Routing mode will be handle domain.')); + so.default = so.disabled; + if (!features.hm_has_dnsmasq_full) { + so.description = _('To enable, you need to install dnsmasq-full.'); + so.readonly = true; + uci.set(data[0], so.section.section, so.option, ''); + uci.save(); + } + so.depends('routing_mode', 'bypass_cn'); + so.depends('routing_mode', 'routing_gfw'); + + /* Custom Direct list */ + ss.tab('direct_list', _('Custom Direct List')); + + so = ss.taboption('direct_list', form.TextValue, 'direct_list.yaml', null); + so.renderWidget = function(/* ... */) { + var frameEl = form.TextValue.prototype.renderWidget.apply(this, arguments); + + frameEl.querySelector('textarea').style.fontFamily = hm.monospacefonts.join(','); + + return frameEl; + } + so.rows = 20; + so.default = 'FQDN:\nIPCIDR:\nIPCIDR6:\n'; + so.placeholder = "FQDN:\n- mask.icloud.com\n- mask-h2.icloud.com\n- mask.apple-dns.net\nIPCIDR:\n- '223.0.0.0/12'\nIPCIDR6:\n- '2400:3200::/32'\n"; + so.load = function(section_id) { + return L.resolveDefault(hm.readFile('resources', this.option), ''); + } + so.write = function(section_id, formvalue) { + return hm.writeFile('resources', this.option, formvalue); + } + so.remove = function(section_id) { + return hm.writeFile('resources', this.option); + } + so.rmempty = false; + + /* Custom Proxy list */ + ss.tab('proxy_list', _('Custom Proxy List')); + + so = ss.taboption('proxy_list', form.TextValue, 'proxy_list.yaml', null); + so.renderWidget = function(/* ... */) { + var frameEl = form.TextValue.prototype.renderWidget.apply(this, arguments); + + frameEl.querySelector('textarea').style.fontFamily = hm.monospacefonts.join(','); + + return frameEl; + } + so.rows = 20; + so.default = 'FQDN:\nIPCIDR:\nIPCIDR6:\n'; + so.placeholder = "FQDN:\n- www.google.com\nIPCIDR:\n- '91.105.192.0/23'\nIPCIDR6:\n- '2001:67c:4e8::/48'\n"; + so.load = function(section_id) { + return L.resolveDefault(hm.readFile('resources', this.option), ''); + } + so.write = function(section_id, formvalue) { + return hm.writeFile('resources', this.option, formvalue); + } + so.remove = function(section_id) { + return hm.writeFile('resources', this.option); + } + so.rmempty = false; + /* ACL END */ + + return m.render(); + } +}); diff --git a/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/hosts.js b/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/hosts.js new file mode 100644 index 0000000000..69500565be --- /dev/null +++ b/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/hosts.js @@ -0,0 +1,41 @@ +'use strict'; +'require view'; +'require ui'; + +'require fchomo as hm'; + +var isReadonlyView = !L.hasViewPermission() || null; + +return view.extend({ + load: function() { + return L.resolveDefault(hm.readFile('templates', 'hosts.yaml'), ''); + }, + + handleSave: function(ev) { + var value = (document.querySelector('textarea').value || '').trim().replace(/\r\n/g, '\n') + '\n'; + + return hm.writeFile('templates', 'hosts.yaml', value).then(function(rc) { + document.querySelector('textarea').value = value; + ui.addNotification(null, E('p', _('Contents have been saved.')), 'info'); + }).catch(function(e) { + ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e))); + }); + }, + + render: function(content) { + return E([ + E('h2', _('Hosts')), + E('p', { 'class': 'cbi-section-descr' }, _('Custom internal hosts. Support yaml or json format.')), + E('p', {}, E('textarea', { + 'class': 'cbi-input-textarea', + 'placeholder': "hosts:\n '*.clash.dev': 127.0.0.1\n 'alpha.clash.dev': '::1'\n test.com: [1.1.1.1, 2.2.2.2]\n baidu.com: google.com", + 'style': 'width:100%;font-family:' + hm.monospacefonts.join(','), + 'rows': 25, + 'disabled': isReadonlyView + }, [ content ? content : 'hosts:\n' ])) + ]); + }, + + handleSaveApply: null, + handleReset: null +}); diff --git a/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/log.js b/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/log.js new file mode 100644 index 0000000000..a2f181e28b --- /dev/null +++ b/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/log.js @@ -0,0 +1,129 @@ +/* Thanks to homeproxy */ +'use strict'; +'require dom'; +'require form'; +'require fs'; +'require poll'; +'require rpc'; +'require ui'; +'require view'; + +/* Thanks to luci-app-aria2 */ +var css = ' \ +#log_textarea { \ + padding: 10px; \ + text-align: left; \ +} \ +#log_textarea pre { \ + padding: .5rem; \ + word-break: break-all; \ + margin: 0; \ +} \ +.description { \ + background-color: #33ccff; \ +}'; + +var hm_dir = '/var/run/fchomo'; + +function getRuntimeLog(name, filename) { + const callLogClean = rpc.declare({ + object: 'luci.fchomo', + method: 'log_clean', + params: ['type'], + expect: { '': {} } + }); + + var log_textarea = E('div', { 'id': 'log_textarea' }, + E('pre', { + 'class': 'spinning' + }, _('Collecting data...')) + ); + + var log; + poll.add(L.bind(function() { + return fs.read_direct(String.format('%s/%s.log', hm_dir, filename), 'text') + .then(function(res) { + log = E('pre', { 'wrap': 'pre' }, [ + res.trim() || _('Log is empty.') + ]); + + dom.content(log_textarea, log); + }).catch(function(err) { + if (err.toString().includes('NotFoundError')) + log = E('pre', { 'wrap': 'pre' }, [ + _('Log file does not exist.') + ]); + else + log = E('pre', { 'wrap': 'pre' }, [ + _('Unknown error: %s').format(err) + ]); + + dom.content(log_textarea, log); + }); + })); + + return E([ + E('style', [ css ]), + E('div', {'class': 'cbi-map'}, [ + E('h3', {'name': 'content'}, [ + _('%s log').format(name), + ' ', + E('button', { + 'class': 'btn cbi-button cbi-button-action', + 'click': ui.createHandlerFn(this, function() { + return L.resolveDefault(callLogClean(filename), {}); + }) + }, [ _('Clean log') ]) + ]), + E('div', {'class': 'cbi-section'}, [ + log_textarea, + E('div', {'style': 'text-align:right'}, + E('small', {}, _('Refresh every %s seconds.').format(L.env.pollinterval)) + ) + ]) + ]) + ]); +} + +return view.extend({ + render: function(data) { + let m, s, o, ss, so; + + m = new form.Map('fchomo'); + + s = m.section(form.NamedSection, 'config', 'fchomo'); + + /* FullCombo Mihomo START */ + s.tab('fchomo', _('FullCombo Mihomo')); + o = s.taboption('fchomo', form.SectionValue, '_fchomo', form.NamedSection, 'config', null); + ss = o.subsection; + + so = ss.option(form.DummyValue, '_fchomo_logview'); + so.render = L.bind(getRuntimeLog, so, _('FullCombo Mihomo'), 'fchomo'); + /* FullCombo Mihomo END */ + + /* Mihomo client START */ + s.tab('mihomo_c', _('Mihomo client')); + o = s.taboption('mihomo_c', form.SectionValue, '_mihomo_c', form.NamedSection, 'config', null); + ss = o.subsection; + + so = ss.option(form.DummyValue, '_mihomo-c_logview'); + so.render = L.bind(getRuntimeLog, so, _('Mihomo client'), 'mihomo-c'); + /* Mihomo client END */ + + /* Mihomo server START */ + s.tab('mihomo_s', _('Mihomo server')); + o = s.taboption('mihomo_s', form.SectionValue, '_mihomo_s', form.NamedSection, 'config', null); + ss = o.subsection; + + so = ss.option(form.DummyValue, '_mihomo-s_logview'); + so.render = L.bind(getRuntimeLog, so, _('Mihomo server'), 'mihomo-s'); + /* Mihomo server END */ + + return m.render(); + }, + + handleSaveApply: null, + handleSave: null, + handleReset: null +}); diff --git a/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js b/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js new file mode 100644 index 0000000000..b74c438605 --- /dev/null +++ b/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/node.js @@ -0,0 +1,1210 @@ +'use strict'; +'require form'; +'require uci'; +'require ui'; +'require view'; + +'require fchomo as hm'; +'require tools.widgets as widgets'; + +return view.extend({ + load: function() { + return Promise.all([ + uci.load('fchomo') + ]); + }, + + render: function(data) { + let m, s, o, ss, so; + + m = new form.Map('fchomo', _('Edit node')); + + s = m.section(form.NamedSection, 'global', 'fchomo'); + + /* Proxy Node START */ + s.tab('node', _('Proxy Node')); + + /* Proxy Node */ + o = s.taboption('node', form.SectionValue, '_node', form.GridSection, 'node', null); + ss = o.subsection; + var prefmt = { 'prefix': 'node_', 'suffix': '' }; + ss.addremove = true; + ss.rowcolors = true; + ss.sortable = true; + ss.nodescriptions = true; + ss.modaltitle = L.bind(hm.loadModalTitle, ss, _('Node'), _('Add a Node')); + ss.sectiontitle = L.bind(hm.loadDefaultLabel, ss); + ss.renderSectionAdd = L.bind(hm.renderSectionAdd, ss, prefmt, true); + ss.handleAdd = L.bind(hm.handleAdd, ss, prefmt); + + ss.tab('field_general', _('General fields')); + ss.tab('field_tls', _('TLS fields')); + ss.tab('field_transport', _('Transport fields')); + ss.tab('field_multiplex', _('Multiplex fields')); + ss.tab('field_dial', _('Dial fields')); + + so = ss.taboption('field_general', form.Value, 'label', _('Label')); + so.load = L.bind(hm.loadDefaultLabel, so); + so.validate = L.bind(hm.validateUniqueValue, so); + so.modalonly = true; + + so = ss.taboption('field_general', form.Flag, 'enabled', _('Enable')); + so.default = so.enabled; + so.editable = true; + + so = ss.taboption('field_general', form.ListValue, 'type', _('Type')); + so.default = hm.outbound_type[0][0]; + hm.outbound_type.forEach((res) => { + so.value.apply(so, res); + }) + + so = ss.taboption('field_general', form.Value, 'server', _('Server address')); + so.datatype = 'host'; + so.rmempty = false; + so.depends({type: 'direct', '!reverse': true}); + + so = ss.taboption('field_general', form.Value, 'port', _('Port')); + so.datatype = 'port'; + so.rmempty = false; + so.depends({type: /^(direct|mieru)$/, '!reverse': true}); + + /* HTTP / SOCKS fields */ + /* hm.validateAuth */ + so = ss.taboption('field_general', form.Value, 'username', _('Username')); + so.validate = L.bind(hm.validateAuthUsername, so); + so.depends({type: /^(http|socks5|mieru|ssh)$/}); + so.modalonly = true; + + so = ss.taboption('field_general', form.Value, 'password', _('Password')); + so.password = true; + so.validate = L.bind(hm.validateAuthPassword, so); + so.depends({type: /^(http|socks5|mieru|trojan|hysteria2|tuic|ssh)$/}); + so.modalonly = true; + + so = ss.taboption('field_general', form.TextValue, 'headers', _('HTTP header')); + so.renderWidget = function(/* ... */) { + var frameEl = form.TextValue.prototype.renderWidget.apply(this, arguments); + + frameEl.querySelector('textarea').style.fontFamily = hm.monospacefonts.join(','); + + return frameEl; + } + so.placeholder = '{\n "User-Agent": [\n "Clash/v1.18.0",\n "mihomo/1.18.3"\n ],\n "Authorization": [\n //"token 1231231"\n ]\n}'; + so.validate = L.bind(hm.validateJson, so); + so.depends('type', 'http'); + so.modalonly = true; + + /* Hysteria / Hysteria2 fields */ + so = ss.taboption('field_general', form.DynamicList, 'hysteria_ports', _('Ports pool')); + so.datatype = 'or(port, portrange)'; + so.depends({type: /^(hysteria|hysteria2)$/}); + so.modalonly = true; + + so = ss.taboption('field_general', form.Value, 'hysteria_up_mbps', _('Max upload speed'), + _('In Mbps.')); + so.datatype = 'uinteger'; + so.depends({type: /^(hysteria|hysteria2)$/}); + so.modalonly = true; + + so = ss.taboption('field_general', form.Value, 'hysteria_down_mbps', _('Max download speed'), + _('In Mbps.')); + so.datatype = 'uinteger'; + so.depends({type: /^(hysteria|hysteria2)$/}); + so.modalonly = true; + + so = ss.taboption('field_general', form.ListValue, 'hysteria_obfs_type', _('Obfuscate type')); + so.value('', _('Disable')); + so.value('salamander', _('Salamander')); + so.depends('type', 'hysteria2'); + so.modalonly = true; + + so = ss.taboption('field_general', form.Value, 'hysteria_obfs_password', _('Obfuscate password'), + _('Enabling obfuscation will make the server incompatible with standard QUIC connections, losing the ability to masquerade with HTTP/3.')); + so.password = true; + so.rmempty = false; + so.depends('type', 'hysteria'); + so.depends({type: 'hysteria2', hysteria_obfs_type: /.+/}); + so.modalonly = true; + + /* SSH fields */ + so = ss.taboption('field_general', form.TextValue, 'ssh_priv_key', _('Priv-key')); + so.depends('type', 'ssh'); + so.modalonly = true; + + so = ss.taboption('field_general', form.Value, 'ssh_priv_key_passphrase', _('Priv-key passphrase')); + so.password = true; + so.depends({type: 'ssh', ssh_priv_key: /.+/}); + so.modalonly = true; + + so = ss.taboption('field_general', form.DynamicList, 'ssh_host_key_algorithms', _('Host-key algorithms')); + so.placeholder = 'rsa'; + so.depends('type', 'ssh'); + so.modalonly = true; + + so = ss.taboption('field_general', form.DynamicList, 'ssh_host_key', _('Host-key')); + so.placeholder = 'ssh-rsa AAAAB3NzaC1yc2EAA...'; + so.depends({type: 'ssh', ssh_host_key_algorithms: /.+/}); + so.modalonly = true; + + /* Shadowsocks fields */ + so = ss.taboption('field_general', form.ListValue, 'shadowsocks_chipher', _('Chipher')); + so.default = hm.shadowsocks_cipher_methods[1][0]; + hm.shadowsocks_cipher_methods.forEach((res) => { + so.value.apply(so, res); + }) + so.depends('type', 'ss'); + so.modalonly = true; + + so = ss.taboption('field_general', form.Value, 'shadowsocks_password', _('Password')); + so.password = true; + so.validate = function(section_id, value) { + var encmode = this.section.getOption('shadowsocks_chipher').formvalue(section_id); + return hm.validateShadowsocksPassword.call(this, hm, encmode, section_id, value); + } + so.depends({type: 'ss', shadowsocks_chipher: /.+/}); + so.modalonly = true; + + /* Mieru fields */ + so = ss.taboption('field_general', form.Value, 'mieru_port_range', _('Port range')); + so.datatype = 'portrange'; + so.depends('type', 'mieru'); + so.modalonly = true; + + so = ss.taboption('field_general', form.ListValue, 'mieru_transport', _('Transport')); + so.value('TCP'); + so.default = 'TCP'; + so.depends('type', 'mieru'); + so.modalonly = true; + + so = ss.taboption('field_general', form.ListValue, 'mieru_multiplexing', _('Multiplexing')); + so.value('MULTIPLEXING_OFF'); + so.value('MULTIPLEXING_LOW'); + so.value('MULTIPLEXING_MIDDLE'); + so.value('MULTIPLEXING_HIGH'); + so.default = 'MULTIPLEXING_LOW'; + so.depends('type', 'mieru'); + so.modalonly = true; + + /* Snell fields */ + so = ss.taboption('field_general', form.Value, 'snell_psk', _('Pre-shared key')); + so.password = true; + so.rmempty = false; + so.validate = L.bind(hm.validateAuthPassword, so); + so.depends('type', 'snell'); + so.modalonly = true; + + so = ss.taboption('field_general', form.ListValue, 'snell_version', _('Version')); + so.value('1', _('v1')); + so.value('2', _('v2')); + so.value('3', _('v3')); + so.default = '3'; + so.depends('type', 'snell'); + so.modalonly = true; + + /* TUIC fields */ + so = ss.taboption('field_general', form.Value, 'uuid', _('UUID')); + so.rmempty = false; + so.validate = L.bind(hm.validateUUID, so); + so.depends('type', 'tuic'); + so.modalonly = true; + + so = ss.taboption('field_general', form.Value, 'tuic_ip', _('IP override'), + _('Override the IP address of the server that DNS response.')); + so.datatype = 'ipaddr(1)'; + so.depends('type', 'tuic'); + so.modalonly = true; + + so = ss.taboption('field_general', form.ListValue, 'tuic_congestion_controller', _('Congestion controller'), + _('QUIC congestion controller.')); + so.default = 'cubic'; + so.value('cubic', _('cubic')); + so.value('new_reno', _('new_reno')); + so.value('bbr', _('bbr')); + so.depends('type', 'tuic'); + so.modalonly = true; + + so = ss.taboption('field_general', form.ListValue, 'tuic_udp_relay_mode', _('UDP relay mode'), + _('UDP packet relay mode.')); + so.value('', _('Default')); + so.value('native', _('Native')); + so.value('quic', _('QUIC')); + so.depends({type: 'tuic', tuic_udp_over_stream: '0'}); + so.modalonly = true; + + so = ss.taboption('field_general', form.Flag, 'tuic_udp_over_stream', _('UDP over stream'), + _('This is the TUIC port of the SUoT protocol, designed to provide a QUIC stream based UDP relay mode that TUIC does not provide.')); + so.default = so.disabled; + so.depends('type', 'tuic'); + so.modalonly = true; + + so = ss.taboption('field_general', form.ListValue, 'tuic_udp_over_stream_version', _('UDP over stream version')); + so.value('1', _('v1')); + so.depends({type: 'tuic', tuic_udp_over_stream: '1'}); + so.modalonly = true; + + so = ss.taboption('field_general', form.Value, 'tuic_max_udp_relay_packet_size', _('Max UDP relay packet size')); + so.datatype = 'uinteger'; + so.placeholder = '1500'; + so.depends('type', 'tuic'); + so.modalonly = true; + + so = ss.taboption('field_general', form.Flag, 'tuic_reduce_rtt', _('Enable 0-RTT handshake'), + _('Enable 0-RTT QUIC connection handshake on the client side. This is not impacting much on the performance, as the protocol is fully multiplexed.
' + + 'Disabling this is highly recommended, as it is vulnerable to replay attacks.')); + so.default = so.disabled; + so.depends('type', 'tuic'); + so.modalonly = true; + + so = ss.taboption('field_general', form.Value, 'tuic_heartbeat', _('Heartbeat interval'), + _('In millisecond.')); + so.datatype = 'uinteger'; + so.placeholder = '10000'; + so.depends('type', 'tuic'); + so.modalonly = true; + + so = ss.taboption('field_general', form.Value, 'tuic_request_timeout', _('Request timeout'), + _('In millisecond.')); + so.datatype = 'uinteger'; + so.placeholder = '8000'; + so.depends('type', 'tuic'); + so.modalonly = true; + + /* Trojan fields */ + so = ss.taboption('field_general', form.Flag, 'trojan_ss_enabled', _('Shadowsocks encrypt')); + so.default = so.disabled; + so.depends('type', 'trojan'); + so.modalonly = true; + + so = ss.taboption('field_general', form.ListValue, 'trojan_ss_chipher', _('Shadowsocks chipher')); + so.value('aes-128-gcm', _('aes-128-gcm')); + so.value('aes-256-gcm', _('aes-256-gcm')); + so.value('chacha20-ietf-poly1305', _('chacha20-ietf-poly1305')); + so.depends({type: 'trojan', trojan_ss_enabled: '1'}); + so.modalonly = true; + + so = ss.taboption('field_general', form.Value, 'trojan_ss_password', _('Shadowsocks password')); + so.password = true; + so.validate = function(section_id, value) { + var encmode = this.section.getOption('trojan_ss_chipher').formvalue(section_id); + return hm.validateShadowsocksPassword.call(this, hm, encmode, section_id, value); + } + so.depends({type: 'trojan', trojan_ss_enabled: '1'}); + so.modalonly = true; + + /* VMess / VLESS fields */ + so = ss.taboption('field_general', form.Value, 'vmess_uuid', _('UUID')); + so.rmempty = false; + so.validate = L.bind(hm.validateUUID, so); + so.depends({type: /^(vmess|vless)$/}); + so.modalonly = true; + + so = ss.taboption('field_general', form.ListValue, 'vless_flow', _('Flow')); + so.value('', _('None')); + so.value('xtls-rprx-vision'); + so.depends('type', 'vless'); + so.modalonly = true; + + so = ss.taboption('field_general', form.Value, 'vmess_alterid', _('Alter ID')); + so.datatype = 'uinteger'; + so.default = '0'; + so.depends('type', 'vmess'); + so.modalonly = true; + + so = ss.taboption('field_general', form.ListValue, 'vmess_chipher', _('Chipher')); + so.default = 'auto'; + so.value('auto', _('auto')); + so.value('none', _('none')); + so.value('zero', _('zero')); + so.value('aes-128-gcm', _('aes-128-gcm')); + so.value('chacha20-poly1305', _('chacha20-poly1305')); + so.depends('type', 'vmess'); + so.modalonly = true; + + so = ss.taboption('field_general', form.Flag, 'vmess_global_padding', _('Global padding'), + _('Protocol parameter. Will waste traffic randomly if enabled (enabled by default in v2ray and cannot be disabled).')); + so.default = so.enabled; + so.depends('type', 'vmess'); + so.modalonly = true; + + so = ss.taboption('field_general', form.Flag, 'vmess_authenticated_length', _('Authenticated length'), + _('Protocol parameter. Enable length block encryption.')); + so.default = so.disabled; + so.depends('type', 'vmess'); + so.modalonly = true; + + so = ss.taboption('field_general', form.ListValue, 'vmess_packet_encoding', _('Packet encoding')); + so.value('', _('none')); + so.value('packetaddr', _('packet addr (v2ray-core v5+)')); + so.value('xudp', _('Xudp (Xray-core)')); + so.depends({type: /^(vmess|vless)$/}); + so.modalonly = true; + + /* WireGuard fields */ + so = ss.taboption('field_general', form.Value, 'wireguard_ip', _('Local address'), + _('The %s address used by local machine in the Wireguard network.').format('IPv4')); + so.datatype = 'ip4addr(1)'; + so.rmempty = false; + so.depends('type', 'wireguard'); + so.modalonly = true; + + so = ss.taboption('field_general', form.Value, 'wireguard_ipv6', _('Local IPv6 address'), + _('The %s address used by local machine in the Wireguard network.').format('IPv6')); + so.datatype = 'ip6addr(1)'; + so.depends('type', 'wireguard'); + so.modalonly = true; + + so = ss.taboption('field_general', form.Value, 'wireguard_private_key', _('Private key'), + _('WireGuard requires base64-encoded private keys.')); + so.password = true; + so.validate = L.bind(hm.validateBase64Key, so, 44); + so.rmempty = false; + so.depends('type', 'wireguard'); + so.modalonly = true; + + so = ss.taboption('field_general', form.Value, 'wireguard_peer_public_key', _('Peer pubkic key'), + _('WireGuard peer public key.')); + so.validate = L.bind(hm.validateBase64Key, so, 44); + so.rmempty = false; + so.depends('type', 'wireguard'); + so.modalonly = true; + + so = ss.taboption('field_general', form.Value, 'wireguard_pre_shared_key', _('Pre-shared key'), + _('WireGuard pre-shared key.')); + so.password = true; + so.validate = L.bind(hm.validateBase64Key, so, 44); + so.depends('type', 'wireguard'); + so.modalonly = true; + + so = ss.taboption('field_general', form.DynamicList, 'wireguard_allowed_ips', _('Allowed IPs'), + _('Destination addresses allowed to be forwarded via Wireguard.')); + so.datatype = 'cidr'; + so.placeholder = '0.0.0.0/0'; + so.depends('type', 'wireguard'); + so.modalonly = true; + + so = ss.taboption('field_general', form.DynamicList, 'wireguard_reserved', _('Reserved field bytes')); + so.datatype = 'integer'; + so.depends('type', 'wireguard'); + so.modalonly = true; + + so = ss.taboption('field_general', form.Value, 'wireguard_mtu', _('MTU')); + so.datatype = 'range(0,9000)'; + so.placeholder = '1408'; + so.depends('type', 'wireguard'); + so.modalonly = true; + + so = ss.taboption('field_general', form.Flag, 'wireguard_remote_dns_resolve', _('Remote DNS resolve'), + _('Force DNS remote resolution.')); + so.default = so.disabled; + so.depends('type', 'wireguard'); + so.modalonly = true; + + so = ss.taboption('field_general', form.DynamicList, 'wireguard_dns', _('DNS server')); + so.datatype = 'or(host, hostport)'; + so.depends('wireguard_remote_dns_resolve', '1'); + so.modalonly = true; + + /* Plugin fields */ + so = ss.taboption('field_general', form.ListValue, 'plugin', _('Plugin')); + so.value('', _('none')); + so.value('obfs', _('obfs-simple')); + //so.value('v2ray-plugin', _('v2ray-plugin')); + so.value('shadow-tls', _('shadow-tls')); + so.value('restls', _('restls')); + so.depends('type', 'ss'); + so.modalonly = true; + + so = ss.taboption('field_general', form.ListValue, 'plugin_opts_obfsmode', _('Plugin: ') + _('Obfs Mode')); + so.value('http', _('HTTP')); + so.value('tls', _('TLS')); + so.depends('plugin', 'obfs'); + so.depends('type', 'snell'); + so.modalonly = true; + + so = ss.taboption('field_general', form.Value, 'plugin_opts_host', _('Plugin: ') + _('Host that supports TLS 1.3')); + so.placeholder = 'cloud.tencent.com'; + so.rmempty = false; + so.depends({plugin: /^(obfs|v2ray-plugin|shadow-tls|restls)$/}); + so.depends('type', 'snell'); + so.modalonly = true; + + so = ss.taboption('field_general', form.Value, 'plugin_opts_thetlspassword', _('Plugin: ') + _('Password')); + so.password = true; + so.rmempty = false; + so.depends({plugin: /^(shadow-tls|restls)$/}); + so.modalonly = true; + + so = ss.taboption('field_general', form.ListValue, 'plugin_opts_shadowtls_version', _('Plugin: ') + _('Version')); + so.value('1', _('v1')); + so.value('2', _('v2')); + so.value('3', _('v3')); + so.default = '2'; + so.depends({plugin: 'shadow-tls'}); + so.modalonly = true; + + so = ss.taboption('field_general', form.Value, 'plugin_opts_restls_versionhint', _('Plugin: ') + _('Version hint')); + so.default = 'tls13'; + so.rmempty = false; + so.depends({plugin: 'restls'}); + so.modalonly = true; + + so = ss.taboption('field_general', form.Value, 'plugin_opts_restls_script', _('Plugin: ') + _('Restls script')); + so.default = '300?100<1,400~100,350~100,600~100,300~200,300~100'; + so.rmempty = false; + so.depends({plugin: 'restls'}); + so.modalonly = true; + + /* Extra fields */ + so = ss.taboption('field_general', form.Flag, 'udp', _('UDP')); + so.default = so.disabled; + so.depends({type: /^(direct|socks5|ss|vmess|vless|trojan|wireguard)$/}); + so.modalonly = true; + + so = ss.taboption('field_general', form.Flag, 'uot', _('UoT'), + _('Enable the SUoT protocol, requires server support. Conflict with Multiplex.')); + so.default = so.disabled; + so.depends('type', 'ss'); + so.modalonly = true; + + so = ss.taboption('field_general', form.ListValue, 'uot_version', _('SUoT version')); + so.value('1', _('v1')); + so.value('2', _('v2')); + so.default = '2'; + so.depends('uot', '1'); + so.modalonly = true; + + /* TLS fields */ + so = ss.taboption('field_general', form.Flag, 'tls', _('TLS')); + so.default = so.disabled; + so.validate = function(section_id, value) { + var type = this.section.getOption('type').formvalue(section_id); + var tls = this.section.getUIElement(section_id, 'tls').node.querySelector('input'); + var tls_alpn = this.section.getUIElement(section_id, 'tls_alpn'); + + // Force enabled + if (['trojan', 'hysteria', 'hysteria2', 'tuic'].includes(type)) { + tls.checked = true; + tls.disabled = true; + } else { + tls.disabled = null; + } + + // Default alpn + if (!`${tls_alpn.getValue()}`) { + let def_alpn; + + switch (type) { + case 'hysteria': + case 'hysteria2': + case 'tuic': + def_alpn = ['h3']; + break; + case 'vmess': + case 'vless': + case 'trojan': + def_alpn = ['h2', 'http/1.1']; + break; + default: + def_alpn = []; + } + + tls_alpn.setValue(def_alpn); + } + + return true; + } + so.depends({type: /^(http|socks5|vmess|vless|trojan|hysteria|hysteria2|tuic)$/}); + so.modalonly = true; + + so = ss.taboption('field_tls', form.Flag, 'tls_disable_sni', _('Disable SNI'), + _('Donot send server name in ClientHello.')); + so.default = so.disabled; + so.depends({tls: '1', type: /^(tuic)$/}); + so.modalonly = true; + + so = ss.taboption('field_tls', form.Value, 'tls_sni', _('TLS SNI'), + _('Used to verify the hostname on the returned certificates.')); + so.depends({tls: '1', type: /^(http|vmess|vless|trojan|hysteria|hysteria2)$/}); + so.depends({tls: '1', tls_disable_sni: '0', type: /^(tuic)$/}); + so.modalonly = true; + + so = ss.taboption('field_tls', form.DynamicList, 'tls_alpn', _('TLS ALPN'), + _('List of supported application level protocols, in order of preference.')); + so.depends({tls: '1', type: /^(vmess|vless|trojan|hysteria|hysteria2|tuic)$/}); + so.modalonly = true; + + so = ss.taboption('field_tls', form.Value, 'tls_fingerprint', _('Cert fingerprint'), + _('Certificate fingerprint. Used to implement SSL Pinning and prevent MitM.')); + so.validate = function(section_id, value) { + if (!value) + return true; + if (!((value.length === 64) && (value.match(/^[0-9a-fA-F]+$/)))) + return _('Expecting: %s').format(_('valid SHA256 string with %d characters').format(64)); + + return true; + } + so.depends({tls: '1', type: /^(http|socks5|vmess|vless|trojan|hysteria|hysteria2)$/}); + so.modalonly = true; + + so = ss.taboption('field_tls', form.Flag, 'tls_skip_cert_verify', _('Skip cert verify'), + _('Donot verifying server certificate.') + + '
' + + _('This is DANGEROUS, your traffic is almost like PLAIN TEXT! Use at your own risk!')); + so.default = so.disabled; + so.depends({tls: '1', type: /^(http|socks5|vmess|vless|trojan|hysteria|hysteria2|tuic)$/}); + so.modalonly = true; + + // uTLS fields + so = ss.taboption('field_tls', form.ListValue, 'tls_client_fingerprint', _('Client fingerprint')); + so.default = hm.tls_client_fingerprints[0][0]; + hm.tls_client_fingerprints.forEach((res) => { + so.value.apply(so, res); + }) + so.depends({tls: '1', type: /^(vmess|vless|trojan)$/}); + so.depends({type: 'ss', plugin: /^(shadow-tls|restls)$/}); + so.modalonly = true; + + so = ss.taboption('field_tls', form.Flag, 'tls_reality', _('REALITY')); + so.default = so.disabled; + so.depends({tls: '1', type: /^(vmess|vless|trojan)$/}); + so.modalonly = true; + + so = ss.taboption('field_tls', form.Value, 'tls_reality_public_key', _('REALITY public key')); + so.rmempty = false; + so.depends('tls_reality', '1'); + so.modalonly = true; + + so = ss.taboption('field_tls', form.Value, 'tls_reality_short_id', _('REALITY short ID')); + so.rmempty = false; + so.depends('tls_reality', '1'); + so.modalonly = true; + + /* Transport fields */ + so = ss.taboption('field_general', form.Flag, 'transport_enabled', _('Transport')); + so.default = so.disabled; + so.depends({type: /^(vmess|vless|trojan)$/}); + so.modalonly = true; + + so = ss.taboption('field_transport', form.ListValue, 'transport_type', _('Transport type')); + so.default = 'http'; + so.value('http', _('HTTP')); + so.value('h2', _('HTTPUpgrade')); + so.value('grpc', _('gRPC')); + so.value('ws', _('WebSocket')); + so.validate = function(section_id, value) { + var type = this.section.getOption('type').formvalue(section_id); + + switch (type) { + case 'vmess': + case 'vless': + if (!['http', 'h2', 'grpc', 'ws'].includes(value)) + return _('Expecting: only support %s.').format(_('HTTP') + + ' / ' + _('HTTPUpgrade') + + ' / ' + _('gRPC') + + ' / ' + _('WebSocket')); + break; + case 'trojan': + if (!['grpc', 'ws'].includes(value)) + return _('Expecting: only support %s.').format(_('gRPC') + + ' / ' + _('WebSocket')); + break; + default: + break; + } + + return true; + } + so.depends('transport_enabled', '1'); + so.modalonly = true; + + so = ss.taboption('field_transport', form.DynamicList, 'transport_hosts', _('Server hostname')); + so.datatype = 'list(hostname)'; + so.placeholder = 'example.com'; + so.depends({transport_enabled: '1', transport_type: 'h2'}); + so.modalonly = true; + + so = ss.taboption('field_transport', form.Value, 'transport_http_method', _('HTTP request method')); + so.value('GET', _('GET')); + so.value('POST', _('POST')); + so.value('PUT', _('PUT')); + so.default = 'GET'; + so.rmempty = false; + so.depends({transport_enabled: '1', transport_type: 'http'}); + so.modalonly = true; + + so = ss.taboption('field_transport', form.DynamicList, 'transport_paths', _('Request path')); + so.placeholder = '/video'; + so.default = '/'; + so.rmempty = false; + so.depends({transport_enabled: '1', transport_type: 'http'}); + so.modalonly = true; + + so = ss.taboption('field_transport', form.Value, 'transport_path', _('Request path')); + so.placeholder = '/'; + so.default = '/'; + so.rmempty = false; + so.depends({transport_enabled: '1', transport_type: /^(h2|ws)$/}); + so.modalonly = true; + + so = ss.taboption('field_transport', form.TextValue, 'transport_http_headers', _('HTTP header')); + so.renderWidget = function(/* ... */) { + var frameEl = form.TextValue.prototype.renderWidget.apply(this, arguments); + + frameEl.querySelector('textarea').style.fontFamily = hm.monospacefonts.join(','); + + return frameEl; + } + so.placeholder = '{\n "Host": "example.com",\n "Connection": [\n "keep-alive"\n ]\n}'; + so.validate = L.bind(hm.validateJson, so); + so.depends({transport_enabled: '1', transport_type: /^(http|ws)$/}); + so.modalonly = true; + + so = ss.taboption('field_transport', form.Value, 'transport_grpc_servicename', _('gRPC service name')); + so.depends({transport_enabled: '1', transport_type: 'grpc'}); + so.modalonly = true; + + so = ss.taboption('field_transport', form.Value, 'transport_ws_max_early_data', _('Max Early Data'), + _('Early Data first packet length limit.')); + so.datatype = 'uinteger'; + so.value('2048'); + so.depends({transport_enabled: '1', transport_type: 'ws'}); + so.modalonly = true; + + so = ss.taboption('field_transport', form.Value, 'transport_ws_early_data_header', _('Early Data header name')); + so.value('Sec-WebSocket-Protocol'); + so.depends({transport_enabled: '1', transport_type: 'ws'}); + so.modalonly = true; + + so = ss.taboption('field_transport', form.Flag, 'transport_ws_v2ray_http_upgrade', _('V2ray HTTPUpgrade')); + so.default = so.disabled; + so.depends({transport_enabled: '1', transport_type: 'ws'}); + so.modalonly = true; + + so = ss.taboption('field_transport', form.Flag, 'transport_ws_v2ray_http_upgrade_fast_open', _('V2ray HTTPUpgrade fast open')); + so.default = so.disabled; + so.depends({transport_enabled: '1', transport_type: 'ws', transport_ws_v2ray_http_upgrade: '1'}); + so.modalonly = true; + + /* Multiplex fields */ // TCP protocol only + so = ss.taboption('field_general', form.Flag, 'smux_enabled', _('Multiplex')); + so.default = so.disabled; + so.depends({type: /^(vmess|vless|trojan)$/}); + so.depends({type: 'ss', uot: '0'}); + so.modalonly = true; + + so = ss.taboption('field_multiplex', form.ListValue, 'smux_protocol', _('Protocol')); + so.default = 'h2mux'; + so.value('smux', _('smux')); + so.value('yamux', _('yamux')); + so.value('h2mux', _('h2mux')); + so.depends('smux_enabled', '1'); + so.modalonly = true; + + so = ss.taboption('field_multiplex', form.Value, 'smux_max_connections', _('Maximum connections')); + so.datatype = 'uinteger'; + so.placeholder = '4'; + so.depends('smux_enabled', '1'); + so.modalonly = true; + + so = ss.taboption('field_multiplex', form.Value, 'smux_min_streams', _('Minimum streams'), + _('Minimum multiplexed streams in a connection before opening a new connection.')); + so.datatype = 'uinteger'; + so.placeholder = '4'; + so.depends('smux_enabled', '1'); + so.modalonly = true; + + so = ss.taboption('field_multiplex', form.Value, 'smux_max_streams', _('Maximum streams'), + _('Maximum multiplexed streams in a connection before opening a new connection.
' + + 'Conflict with %s and %s.') + .format(_('Maximum connections'), _('Minimum streams'))); + so.datatype = 'uinteger'; + so.placeholder = '0'; + so.depends({smux_enabled: '1', smux_max_connections: '', smux_min_streams: ''}); + so.modalonly = true; + + so = ss.taboption('field_multiplex', form.Flag, 'smux_padding', _('Enable padding')); + so.default = so.disabled; + so.depends('smux_enabled', '1'); + so.modalonly = true; + + so = ss.taboption('field_multiplex', form.Flag, 'smux_only_tcp', _('TCP only'), + _('Enable multiplexing only for TCP.')); + so.default = so.disabled; + so.depends('smux_enabled', '1'); + so.modalonly = true; + + so = ss.taboption('field_multiplex', form.Flag, 'smux_statistic', _('Enable statistic'), + _('Show connections in the dashboard for breaking connections easier.')); + so.default = so.disabled; + so.depends('smux_enabled', '1'); + so.modalonly = true; + + so = ss.taboption('field_multiplex', form.Flag, 'smux_brutal', _('Enable TCP Brutal'), + _('Enable TCP Brutal congestion control algorithm')); + so.default = so.disabled; + so.depends('smux_enabled', '1'); + so.modalonly = true; + + so = ss.taboption('field_multiplex', form.Value, 'smux_brutal_up', _('Upload bandwidth'), + _('Upload bandwidth in Mbps.')); + so.datatype = 'uinteger'; + so.depends('smux_brutal', '1'); + so.modalonly = true; + + so = ss.taboption('field_multiplex', form.Value, 'smux_brutal_down', _('Download bandwidth'), + _('Download bandwidth in Mbps.')); + so.datatype = 'uinteger'; + so.depends('smux_brutal', '1'); + so.modalonly = true; + + /* Dial fields */ + so = ss.taboption('field_dial', form.Flag, 'tfo', _('TFO')); + so.default = so.disabled; + so.modalonly = true; + + so = ss.taboption('field_dial', form.Flag, 'mptcp', _('mpTCP')); + so.default = so.disabled; + so.modalonly = true; + + /* Features are implemented in proxy chain + so = ss.taboption('field_dial', form.Value, 'dialer_proxy', _('dialer-proxy')); + so.readonly = true; + so.modalonly = true; + */ + + so = ss.taboption('field_dial', widgets.DeviceSelect, 'interface_name', _('Bind interface'), + _('Bind outbound interface.
') + + _('Priority: Proxy Node > Proxy Group > Global.')); + so.multiple = false; + so.noaliases = true; + so.modalonly = true; + + so = ss.taboption('field_dial', form.Value, 'routing_mark', _('Routing mark'), + _('Priority: Proxy Node > Proxy Group > Global.')); + so.datatype = 'uinteger'; + so.modalonly = true; + + so = ss.taboption('field_dial', form.ListValue, 'ip_version', _('IP version')); + so.default = hm.ip_version[0][0]; + hm.ip_version.forEach((res) => { + so.value.apply(so, res); + }) + so.modalonly = true; + /* Proxy Node END */ + + /* Provider START */ + s.tab('provider', _('Provider')); + + /* Provider */ + o = s.taboption('provider', form.SectionValue, '_provider', form.GridSection, 'provider', null); + ss = o.subsection; + var prefmt = { 'prefix': 'sub_', 'suffix': '' }; + ss.addremove = true; + ss.rowcolors = true; + ss.sortable = true; + ss.nodescriptions = true; + ss.modaltitle = L.bind(hm.loadModalTitle, ss, _('Provider'), _('Add a provider')); + ss.sectiontitle = L.bind(hm.loadDefaultLabel, ss); + /* Remove idle files start */ + ss.renderSectionAdd = function(/* ... */) { + var el = hm.renderSectionAdd.apply(this, [prefmt, false].concat(Array.prototype.slice.call(arguments))); + + el.appendChild(E('button', { + 'class': 'cbi-button cbi-button-add', + 'title': _('Remove idles'), + 'click': ui.createHandlerFn(this, hm.handleRemoveIdles, hm) + }, [ _('Remove idles') ])); + + return el; + } + ss.handleAdd = L.bind(hm.handleAdd, ss, prefmt); + /* Remove idle files end */ + + ss.tab('field_general', _('General fields')); + ss.tab('field_override', _('Override fields')); + ss.tab('field_health', _('Health fields')); + + /* General fields */ + so = ss.taboption('field_general', form.Value, 'label', _('Label')); + so.load = L.bind(hm.loadDefaultLabel, so); + so.validate = L.bind(hm.validateUniqueValue, so); + so.modalonly = true; + + so = ss.taboption('field_general', form.Flag, 'enabled', _('Enable')); + so.default = so.enabled; + so.editable = true; + + so = ss.taboption('field_general', form.ListValue, 'type', _('Type')); + so.value('file', _('Local')); + so.value('http', _('Remote')); + so.value('inline', _('Inline')); + so.default = 'http'; + + so = ss.option(form.DummyValue, '_value', _('Value')); + so.load = function(section_id) { + var option = uci.get(data[0], section_id, 'type'); + + switch (option) { + case 'file': + return uci.get(data[0], section_id, '.name'); + case 'http': + return uci.get(data[0], section_id, 'url'); + case 'inline': + return uci.get(data[0], section_id, '.name'); + default: + return null; + } + } + so.modalonly = false; + + so = ss.taboption('field_general', form.TextValue, '_editer', _('Editer'), + _('Please type %s.') + .format('https://wiki.metacubex.one/config/proxy-providers/content/', _('Contents'))); + so.renderWidget = function(/* ... */) { + var frameEl = form.TextValue.prototype.renderWidget.apply(this, arguments); + + frameEl.querySelector('textarea').style.fontFamily = hm.monospacefonts.join(','); + + return frameEl; + } + so.placeholder = _('Content will not be verified, Please make sure you enter it correctly.'); + so.load = function(section_id) { + return L.resolveDefault(hm.readFile('provider', section_id), ''); + } + so.write = L.bind(hm.writeFile, so, 'provider'); + so.remove = L.bind(hm.writeFile, so, 'provider'); + so.rmempty = false; + so.retain = true; + so.depends('type', 'file'); + so.modalonly = true; + + so = ss.taboption('field_general', form.TextValue, 'payload', 'payload:', + _('Please type %s.') + .format('https://wiki.metacubex.one/config/proxy-providers/content/', _('Payload'))); + so.renderWidget = function(/* ... */) { + var frameEl = form.TextValue.prototype.renderWidget.apply(this, arguments); + + frameEl.querySelector('textarea').style.fontFamily = hm.monospacefonts.join(','); + + return frameEl; + } + so.placeholder = '- name: "ss1"\n type: ss\n server: server\n port: 443\n cipher: chacha20-ietf-poly1305\n password: "password"\n# ' + _('Content will not be verified, Please make sure you enter it correctly.'); + so.rmempty = false; + so.depends('type', 'inline'); + so.modalonly = true; + + so = ss.taboption('field_general', form.Value, 'url', _('Provider URL')); + so.validate = L.bind(hm.validateUrl, so); + so.rmempty = false; + so.depends('type', 'http'); + so.modalonly = true; + + so = ss.taboption('field_general', form.Value, 'size_limit', _('Size limit'), + _('In bytes. %s will be used if empty.').format('0')); + so.placeholder = '0'; + so.validate = L.bind(hm.validateBytesize, so); + so.depends('type', 'http'); + + so = ss.taboption('field_general', form.Value, 'interval', _('Update interval'), + _('In seconds. %s will be used if empty.').format('86400')); + so.placeholder = '86400'; + so.validate = L.bind(hm.validateTimeDuration, so); + so.depends('type', 'http'); + + so = ss.taboption('field_general', form.ListValue, 'proxy', _('Proxy group'), + _('Name of the Proxy group to download provider.')); + so.default = hm.preset_outbound.direct[0][0]; + hm.preset_outbound.direct.forEach((res) => { + so.value.apply(so, res); + }) + so.load = L.bind(hm.loadProxyGroupLabel, so, hm.preset_outbound.direct); + so.textvalue = L.bind(hm.textvalue2Value, so); + //so.editable = true; + so.depends('type', 'http'); + + so = ss.taboption('field_general', form.TextValue, 'header', _('HTTP header'), + _('Custom HTTP header.')); + so.renderWidget = function(/* ... */) { + var frameEl = form.TextValue.prototype.renderWidget.apply(this, arguments); + + frameEl.querySelector('textarea').style.fontFamily = hm.monospacefonts.join(','); + + return frameEl; + } + so.placeholder = '{\n "User-Agent": [\n "Clash/v1.18.0",\n "mihomo/1.18.3"\n ],\n "Accept": [\n //"application/vnd.github.v3.raw"\n ],\n "Authorization": [\n //"token 1231231"\n ]\n}'; + so.validate = L.bind(hm.validateJson, so); + so.depends('type', 'http'); + so.modalonly = true; + + /* Override fields */ + // https://github.com/muink/mihomo/blob/43f21c0b412b7a8701fe7a2ea6510c5b985a53d6/adapter/provider/parser.go#L30 + + so = ss.taboption('field_override', form.Value, 'override_prefix', _('Add prefix')); + so.modalonly = true; + + so = ss.taboption('field_override', form.Value, 'override_suffix', _('Add suffix')); + so.modalonly = true; + + so = ss.taboption('field_override', form.DynamicList, 'override_replace', _('Replace name'), + _('Replace node name. ') + + _('For format see %s.') + .format('https://wiki.metacubex.one/config/proxy-providers/#overrideproxy-name', _('override.proxy-name'))); + so.placeholder = '{"pattern": "IPLC-(.*?)倍", "target": "iplc x $1"}'; + so.validate = L.bind(hm.validateJson, so); + so.modalonly = true; + + so = ss.taboption('field_override', form.DummyValue, '_config_items', null); + so.load = function() { + return '%s' + .format('https://wiki.metacubex.one/config/proxy-providers/#_2', _('Configuration Items')); + } + so.rawhtml = true; + so.modalonly = true; + + so = ss.taboption('field_override', form.Flag, 'override_tfo', _('TFO')); + so.default = so.disabled; + so.modalonly = true; + + so = ss.taboption('field_override', form.Flag, 'override_mptcp', _('mpTCP')); + so.default = so.disabled; + so.modalonly = true; + + so = ss.taboption('field_override', form.Flag, 'override_udp', _('UDP')); + so.default = so.enabled; + so.modalonly = true; + + so = ss.taboption('field_override', form.Flag, 'override_uot', _('UoT'), + _('Enable the SUoT protocol, requires server support. Conflict with Multiplex.')); + so.default = so.disabled; + so.modalonly = true; + + so = ss.taboption('field_override', form.Value, 'override_up', _('up'), + _('In Mbps.')); + so.datatype = 'uinteger'; + so.modalonly = true; + + so = ss.taboption('field_override', form.Value, 'override_down', _('down'), + _('In Mbps.')); + so.datatype = 'uinteger'; + so.modalonly = true; + + so = ss.taboption('field_override', form.Flag, 'override_skip_cert_verify', _('Skip cert verify'), + _('Donot verifying server certificate.') + + '
' + + _('This is DANGEROUS, your traffic is almost like PLAIN TEXT! Use at your own risk!')); + so.default = so.disabled; + so.modalonly = true; + + /* Features are implemented in proxy chain + so = ss.taboption('field_override', form.Value, 'override_dialer_proxy', _('dialer-proxy')); + so.readonly = true; + so.modalonly = true; + */ + + so = ss.taboption('field_override', widgets.DeviceSelect, 'override_interface_name', _('Bind interface'), + _('Bind outbound interface.
') + + _('Priority: Proxy Node > Proxy Group > Global.')); + so.multiple = false; + so.noaliases = true; + so.modalonly = true; + + so = ss.taboption('field_override', form.Value, 'override_routing_mark', _('Routing mark'), + _('Priority: Proxy Node > Proxy Group > Global.')); + so.datatype = 'uinteger'; + so.modalonly = true; + + so = ss.taboption('field_override', form.ListValue, 'override_ip_version', _('IP version')); + so.default = hm.ip_version[0][0]; + hm.ip_version.forEach((res) => { + so.value.apply(so, res); + }) + so.modalonly = true; + + /* Health fields */ + so = ss.taboption('field_health', form.Flag, 'health_enable', _('Enable')); + so.default = so.enabled; + so.modalonly = true; + + so = ss.taboption('field_health', form.Value, 'health_url', _('Health check URL')); + so.default = hm.health_checkurls[0][0]; + hm.health_checkurls.forEach((res) => { + so.value.apply(so, res); + }) + so.validate = L.bind(hm.validateUrl, so); + so.retain = true; + so.modalonly = true; + + so = ss.taboption('field_health', form.Value, 'health_interval', _('Health check interval'), + _('In seconds. %s will be used if empty.').format('600')); + so.placeholder = '600'; + so.validate = L.bind(hm.validateTimeDuration, so); + so.modalonly = true; + + so = ss.taboption('field_health', form.Value, 'health_timeout', _('Health check timeout'), + _('In millisecond. %s will be used if empty.').format('5000')); + so.datatype = 'uinteger'; + so.placeholder = '5000'; + so.modalonly = true; + + so = ss.taboption('field_health', form.Flag, 'health_lazy', _('Lazy'), + _('No testing is performed when this provider node is not in use.')); + so.default = so.enabled; + so.modalonly = true; + + so = ss.taboption('field_health', form.Value, 'health_expected_status', _('Health check expected status'), + _('Expected HTTP code. 204 will be used if empty. ') + + _('For format see %s.') + .format('https://wiki.metacubex.one/config/proxy-groups/#expected-status', _('Expected status'))); + so.placeholder = '200/302/400-503'; + so.modalonly = true; + + /* General fields */ + so = ss.taboption('field_general', form.DynamicList, 'filter', _('Node filter'), + _('Filter nodes that meet keywords or regexps.')); + so.placeholder = '(?i)港|hk|hongkong|hong kong'; + so.modalonly = true; + + so = ss.taboption('field_general', form.DynamicList, 'exclude_filter', _('Node exclude filter'), + _('Exclude nodes that meet keywords or regexps.')); + so.default = '重置|到期|过期|剩余|套餐 海外用户|回国' + so.placeholder = 'xxx'; + so.modalonly = true; + + so = ss.taboption('field_general', form.DynamicList, 'exclude_type', _('Node exclude type'), + _('Exclude matched node types.')); + so.placeholder = 'ss|http'; + so.modalonly = true; + + so = ss.option(form.DummyValue, '_update'); + so.cfgvalue = L.bind(hm.renderResDownload, so, hm); + so.editable = true; + so.modalonly = false; + /* Provider END */ + + /* Proxy chain START */ + s.tab('dialer_proxy', _('Proxy chain')); + + /* Proxy chain */ + o = s.taboption('dialer_proxy', form.SectionValue, '_dialer_proxy', form.GridSection, 'dialer_proxy', null); + ss = o.subsection; + var prefmt = { 'prefix': 'chain_', 'suffix': '' }; + ss.addremove = true; + ss.rowcolors = true; + ss.sortable = true; + ss.nodescriptions = true; + ss.modaltitle = L.bind(hm.loadModalTitle, ss, _('Proxy chain'), _('Add a proxy chain')); + ss.sectiontitle = L.bind(hm.loadDefaultLabel, ss); + ss.renderSectionAdd = L.bind(hm.renderSectionAdd, ss, prefmt, true); + ss.handleAdd = L.bind(hm.handleAdd, ss, prefmt); + + so = ss.option(form.Value, 'label', _('Label')); + so.load = L.bind(hm.loadDefaultLabel, so); + so.validate = L.bind(hm.validateUniqueValue, so); + so.modalonly = true; + + so = ss.option(form.Flag, 'enabled', _('Enable')); + so.default = so.enabled; + so.editable = true; + + so = ss.option(form.ListValue, 'type', _('Type')); + so.value('node', _('Proxy Node')); + so.value('provider', _('Provider')); + so.default = 'node'; + so.textvalue = L.bind(hm.textvalue2Value, so); + + so = ss.option(form.DummyValue, '_value', _('Value')); + so.load = function(section_id) { + var type = uci.get(data[0], section_id, 'type'); + var detour = uci.get(data[0], section_id, 'chain_tail_group') || uci.get(data[0], section_id, 'chain_tail'); + + switch (type) { + case 'node': + return '%s » %s'.format( + uci.get(data[0], section_id, 'chain_head'), + detour + ); + case 'provider': + return '%s » %s'.format( + uci.get(data[0], section_id, 'chain_head_sub'), + detour + ); + default: + return null; + } + } + so.modalonly = false; + + so = ss.option(form.ListValue, 'chain_head_sub', _('Chain head')); + so.load = L.bind(hm.loadProviderLabel, so); + so.rmempty = false; + so.depends('type', 'provider'); + so.modalonly = true; + + so = ss.option(form.ListValue, 'chain_head', _('Chain head'), + _('Recommended to use UoT node.
such as %s.') + .format('ss|ssr|vmess|vless|trojan|tuic')); + so.load = L.bind(hm.loadNodeLabel, so); + so.rmempty = false; + so.validate = function(section_id, value) { + var chain_tail = this.section.getUIElement(section_id, 'chain_tail').getValue(); + + if (value === chain_tail) + return _('Expecting: %s').format(_('Different chain head/tail')); + + return true; + } + so.depends('type', 'node'); + so.modalonly = true; + + /* + so = ss.option(hm.CBIStaticList, 'chain_body', _('Chain body')); + so.value('', _('-- Please choose --')); + so.load = L.bind(hm.loadNodeLabel, so); + so.validate = function(section_id, value) { + var chain_head = this.section.getUIElement(section_id, 'chain_head').getValue(); + var chain_tail = this.section.getUIElement(section_id, 'chain_tail').getValue(); + var value = this.getUIElement(section_id).getValue(); + + if (value.includes(chain_head) || value.includes(chain_tail)) + return _('Expecting: %s').format(_('Different with chain head/tail')); + + return true; + } + so.textvalue = function(section_id) { + var cvals = this.cfgvalue(section_id); + //alert(Array.prototype.join.call(cvals, ':')); + return cvals ? '» ' + cvals.map((cval) => { + var i = this.keylist.indexOf(cval); + + return this.vallist[i]; + }).join(' » ') + ' »' : '»'; + } + */ + + so = ss.option(form.ListValue, 'chain_tail_group', _('Chain tail')); + so.value('', _('-- Please choose --')); + so.load = L.bind(hm.loadProxyGroupLabel, so, [['', _('-- Please choose --')]]); + so.rmempty = false; + so.depends({chain_tail: /.+/, '!reverse': true}); + so.modalonly = true; + + so = ss.option(form.ListValue, 'chain_tail', _('Chain tail'), + _('Recommended to use UoT node.
such as %s.') + .format('ss|ssr|vmess|vless|trojan|tuic')); + so.load = L.bind(hm.loadNodeLabel, so); + so.rmempty = false; + so.validate = function(section_id, value) { + var chain_head = this.section.getUIElement(section_id, 'chain_head').getValue(); + + if (value === chain_head) + return _('Expecting: %s').format(_('Different chain head/tail')); + + return true; + } + so.depends({chain_tail_group: /.+/, '!reverse': true}); + so.modalonly = true; + /* Proxy chain END */ + + return m.render(); + } +}); diff --git a/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/ruleset.js b/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/ruleset.js new file mode 100644 index 0000000000..949f65e98d --- /dev/null +++ b/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/ruleset.js @@ -0,0 +1,336 @@ +'use strict'; +'require form'; +'require uci'; +'require ui'; +'require view'; + +'require fchomo as hm'; + +function parseRulesetLink(uri) { + var config, + filefmt = new RegExp(/^(text|yaml|mrs)$/), + filebehav = new RegExp(/^(domain|ipcidr|classical)$/), + unuciname = new RegExp(/[^a-zA-Z0-9_]+/, "g"); + + uri = uri.split('://'); + if (uri[0] && uri[1]) { + switch (uri[0]) { + case 'http': + case 'https': + var url = new URL('http://' + uri[1]); + var format = url.searchParams.get('fmt'); + var behavior = url.searchParams.get('behav'); + var interval = url.searchParams.get('sec'); + var rawquery = url.searchParams.get('rawq'); + var name = decodeURI(url.pathname).split('/').pop() + .replace(/[\s\.-]/g, '_').replace(unuciname, ''); + + if (filefmt.test(format) && filebehav.test(behavior)) { + var fullpath = (url.username ? url.username + '@' : '') + url.host + url.pathname + (rawquery ? '?' + decodeURIComponent(rawquery) : ''); + config = { + label: url.hash ? decodeURIComponent(url.hash.slice(1)) : name ? name : null, + type: 'http', + format: format, + behavior: behavior, + url: String.format('%s://%s', uri[0], fullpath), + interval: interval, + id: hm.calcStringMD5(String.format('http://%s', fullpath)) + }; + } + + break; + case 'file': + var url = new URL('file://' + uri[1]); + var format = url.searchParams.get('fmt'); + var behavior = url.searchParams.get('behav'); + var filler = url.searchParams.get('fill'); + var path = decodeURI(url.pathname); + var name = path.split('/').pop() + .replace(/[\s\.-]/g, '_').replace(unuciname, ''); + + if (filefmt.test(format) && filebehav.test(behavior)) { + config = { + label: url.hash ? decodeURIComponent(url.hash.slice(1)) : name ? name : null, + type: 'file', + format: format, + behavior: behavior, + id: hm.calcStringMD5(String.format('file://%s%s', url.host, url.pathname)) + }; + hm.writeFile('ruleset', config.id, hm.decodeBase64Str(filler)); + } + + break; + case 'inline': + var url = new URL('inline:' + uri[1]); + var behavior = url.searchParams.get('behav'); + var payload = hm.decodeBase64Str(url.pathname).trim(); + + if (filebehav.test(behavior) && payload && payload.length) { + config = { + label: url.hash ? decodeURIComponent(url.hash.slice(1)) : null, + type: 'inline', + behavior: behavior, + payload: payload, + id: hm.calcStringMD5(String.format('inline:%s', btoa(payload))) + }; + } + + break; + } + } + + if (config) { + if (!config.type || !config.id) + return null; + else if (!config.label) + config.label = config.id; + } + + return config; +} + +return view.extend({ + load: function() { + return Promise.all([ + uci.load('fchomo') + ]); + }, + + render: function(data) { + let m, s, o; + + m = new form.Map('fchomo', _('Edit ruleset')); + + /* Rule set START */ + /* Rule set settings */ + var prefmt = { 'prefix': 'rule_', 'suffix': '' }; + s = m.section(form.GridSection, 'ruleset'); + s.addremove = true; + s.rowcolors = true; + s.sortable = true; + s.nodescriptions = true; + s.modaltitle = L.bind(hm.loadModalTitle, s, _('Rule set'), _('Add a rule set')); + s.sectiontitle = L.bind(hm.loadDefaultLabel, s); + /* Import rule-set links and Remove idle files start */ + s.handleLinkImport = function() { + var textarea = new ui.Textarea('', { + 'placeholder': 'http(s)://github.com/ACL4SSR/ACL4SSR/raw/refs/heads/master/Clash/Providers/BanAD.yaml?fmt=yaml&behav=classical&rawq=good%3Djob#BanAD\n' + + 'file:///example.txt?fmt=text&behav=domain&fill=LmNuCg#CN%20TLD\n' + + 'inline://LSAnLmhrJwoK?behav=domain#HK%20TLD\n' + }); + ui.showModal(_('Import rule-set links'), [ + E('p', _('Supports rule-set links of type: %s and format: %s.
') + .format('file, http, inline', 'text, yaml, mrs') + + _('Please refer to %s for link format standards.') + .format(hm.rulesetdoc, _('Ruleset-URI-Scheme'))), + textarea.render(), + E('div', { class: 'right' }, [ + E('button', { + class: 'btn', + click: ui.hideModal + }, [ _('Cancel') ]), + ' ', + E('button', { + class: 'btn cbi-button-action', + click: ui.createHandlerFn(this, function() { + var input_links = textarea.getValue().trim().split('\n'); + if (input_links && input_links[0]) { + /* Remove duplicate lines */ + input_links = input_links.reduce((pre, cur) => + (!pre.includes(cur) && pre.push(cur), pre), []); + + var imported_ruleset = 0; + input_links.forEach((l) => { + var config = parseRulesetLink(l); + if (config) { + var sid = uci.add(data[0], 'ruleset', config.id); + config.id = null; + Object.keys(config).forEach((k) => { + uci.set(data[0], sid, k, config[k] || ''); + }); + imported_ruleset++; + } + }); + + if (imported_ruleset === 0) + ui.addNotification(null, E('p', _('No valid rule-set link found.'))); + else + ui.addNotification(null, E('p', _('Successfully imported %s rule-set of total %s.').format( + imported_ruleset, input_links.length))); + + return uci.save() + .then(L.bind(this.map.load, this.map)) + .then(L.bind(this.map.reset, this.map)) + .then(L.ui.hideModal) + .catch(function() {}); + } else { + return ui.hideModal(); + } + }) + }, [ _('Import') ]) + ]) + ]) + } + s.renderSectionAdd = function(/* ... */) { + var el = hm.renderSectionAdd.apply(this, [prefmt, false].concat(Array.prototype.slice.call(arguments))); + + el.appendChild(E('button', { + 'class': 'cbi-button cbi-button-add', + 'title': _('Import rule-set links'), + 'click': ui.createHandlerFn(this, 'handleLinkImport') + }, [ _('Import rule-set links') ])); + + el.appendChild(E('button', { + 'class': 'cbi-button cbi-button-add', + 'title': _('Remove idles'), + 'click': ui.createHandlerFn(this, hm.handleRemoveIdles, hm) + }, [ _('Remove idles') ])); + + return el; + } + s.handleAdd = L.bind(hm.handleAdd, s, prefmt); + /* Import rule-set links and Remove idle files end */ + + o = s.option(form.Value, 'label', _('Label')); + o.load = L.bind(hm.loadDefaultLabel, o); + o.validate = L.bind(hm.validateUniqueValue, o); + o.modalonly = true; + + o = s.option(form.Flag, 'enabled', _('Enable')); + o.default = o.enabled; + o.editable = true; + + o = s.option(form.ListValue, 'type', _('Type')); + o.value('file', _('Local')); + o.value('http', _('Remote')); + o.value('inline', _('Inline')); + o.default = 'http'; + + o = s.option(form.ListValue, 'format', _('Format')); + o.value('text', _('Plain text')); + o.value('yaml', _('Yaml text')); + o.value('mrs', _('Binary file')); + o.default = 'yaml'; + o.validate = function(section_id, value) { + var behavior = this.section.getUIElement(section_id, 'behavior').getValue(); + + if (value === 'mrs' && behavior === 'classical') + return _('Expecting: %s').format(_('Binary format only supports domain / ipcidr')); + + return true; + } + o.textvalue = function(section_id) { + var cval = this.cfgvalue(section_id) || this.default; + var inline = L.bind(function() { + let cval = this.cfgvalue(section_id) || this.default; + return (cval === 'inline') ? true : false; + }, s.getOption('type')); + return inline() ? _('none') : cval; + }; + o.depends({'type': 'inline', '!reverse': true}); + + o = s.option(form.ListValue, 'behavior', _('Behavior')); + o.value('classical'); + o.value('domain'); + o.value('ipcidr'); + o.default = 'classical'; + o.validate = function(section_id, value) { + var format = this.section.getUIElement(section_id, 'format').getValue(); + + if (value === 'classical' && format === 'mrs') + return _('Expecting: %s').format(_('Binary format only supports domain / ipcidr')); + + return true; + } + + o = s.option(form.DummyValue, '_value', _('Value')); + o.load = function(section_id) { + var option = uci.get(data[0], section_id, 'type'); + + switch (option) { + case 'file': + return uci.get(data[0], section_id, '.name'); + case 'http': + return uci.get(data[0], section_id, 'url'); + case 'inline': + return uci.get(data[0], section_id, '.name'); + default: + return null; + } + } + o.modalonly = false; + + o = s.option(form.TextValue, '_editer', _('Editer'), + _('Please type %s.') + .format('https://wiki.metacubex.one/config/rule-providers/content/', _('Contents'))); + o.renderWidget = function(/* ... */) { + var frameEl = form.TextValue.prototype.renderWidget.apply(this, arguments); + + frameEl.querySelector('textarea').style.fontFamily = hm.monospacefonts.join(','); + + return frameEl; + } + o.placeholder = _('Content will not be verified, Please make sure you enter it correctly.'); + o.load = function(section_id) { + return L.resolveDefault(hm.readFile('ruleset', section_id), ''); + } + o.write = L.bind(hm.writeFile, o, 'ruleset'); + o.remove = L.bind(hm.writeFile, o, 'ruleset'); + o.rmempty = false; + o.retain = true; + o.depends({'type': 'file', 'format': /^(text|yaml)$/}); + o.modalonly = true; + + o = s.option(form.TextValue, 'payload', 'payload:', + _('Please type %s.') + .format('https://wiki.metacubex.one/config/rule-providers/content/', _('Payload'))); + o.renderWidget = function(/* ... */) { + var frameEl = form.TextValue.prototype.renderWidget.apply(this, arguments); + + frameEl.querySelector('textarea').style.fontFamily = hm.monospacefonts.join(','); + + return frameEl; + } + o.placeholder = '- DOMAIN-SUFFIX,google.com\n# ' + _('Content will not be verified, Please make sure you enter it correctly.'); + o.rmempty = false; + o.depends('type', 'inline'); + o.modalonly = true; + + o = s.option(form.Value, 'url', _('Rule set URL')); + o.validate = L.bind(hm.validateUrl, o); + o.rmempty = false; + o.depends('type', 'http'); + o.modalonly = true; + + o = s.option(form.Value, 'size_limit', _('Size limit'), + _('In bytes. %s will be used if empty.').format('0')); + o.placeholder = '0'; + o.validate = L.bind(hm.validateBytesize, o); + o.depends('type', 'http'); + + o = s.option(form.Value, 'interval', _('Update interval'), + _('In seconds. %s will be used if empty.').format('259200')); + o.placeholder = '259200'; + o.validate = L.bind(hm.validateTimeDuration, o); + o.depends('type', 'http'); + + o = s.option(form.ListValue, 'proxy', _('Proxy group'), + _('Name of the Proxy group to download rule set.')); + o.default = hm.preset_outbound.direct[0][0]; + hm.preset_outbound.direct.forEach((res) => { + o.value.apply(o, res); + }) + o.load = L.bind(hm.loadProxyGroupLabel, o, hm.preset_outbound.direct); + o.textvalue = L.bind(hm.textvalue2Value, o); + //o.editable = true; + o.depends('type', 'http'); + + o = s.option(form.DummyValue, '_update'); + o.cfgvalue = L.bind(hm.renderResDownload, o, hm); + o.editable = true; + o.modalonly = false; + /* Rule set END */ + + return m.render(); + } +}); diff --git a/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/server.js b/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/server.js new file mode 100644 index 0000000000..a15e49ebf0 --- /dev/null +++ b/luci-app-fchomo/htdocs/luci-static/resources/view/fchomo/server.js @@ -0,0 +1,375 @@ +'use strict'; +'require form'; +'require poll'; +'require uci'; +'require ui'; +'require view'; + +'require fchomo as hm'; + +function handleGenKey(option) { + var section_id = this.section.section; + var type = this.section.getOption('type').formvalue(section_id); + var widget = this.map.findElement('id', 'widget.cbid.fchomo.%s.%s'.format(section_id, option)); + var password, required_method; + + if (option === 'uuid' || option.match(/_uuid/)) + required_method = 'uuid'; + else if (type === 'shadowsocks') + required_method = this.section.getOption('shadowsocks_chipher')?.formvalue(section_id); + + switch (required_method) { + /* NONE */ + case 'none': + password = ''; + break; + /* UUID */ + case 'uuid': + password = hm.generateRand('uuid'); + break; + /* DEFAULT */ + default: + password = hm.generateRand('hex', 16); + break; + } + /* AEAD */ + (function(length) { + if (length && length > 0) + password = hm.generateRand('base64', length); + }(hm.shadowsocks_cipher_length[required_method])); + + return widget.value = password; +} + +return view.extend({ + load: function() { + return Promise.all([ + uci.load('fchomo'), + hm.getFeatures() + ]); + }, + + render: function(data) { + var dashboard_repo = uci.get(data[0], 'api', 'dashboard_repo'), + features = data[1]; + + let m, s, o; + + m = new form.Map('fchomo', _('Mihomo server'), + _('When used as a server, HomeProxy is a better choice.')); + + s = m.section(form.TypedSection); + s.render = function () { + poll.add(function () { + return hm.getServiceStatus('mihomo-s').then((isRunning) => { + hm.updateStatus(hm, document.getElementById('_server_bar'), isRunning ? { dashboard_repo: dashboard_repo } : false, 'mihomo-s', true); + }); + }); + + return E('div', { class: 'cbi-section' }, [ + E('p', [ + hm.renderStatus(hm, '_server_bar', false, 'mihomo-s', true) + ]) + ]); + } + + s = m.section(form.NamedSection, 'routing', 'fchomo', null); + + /* Server switch */ + o = s.option(form.Button, '_reload_server', _('Quick Reload')); + o.inputtitle = _('Reload'); + o.inputstyle = 'apply'; + o.onclick = L.bind(hm.handleReload, o, 'mihomo-s'); + + o = s.option(form.Flag, 'server_enabled', _('Enable')); + o.default = o.disabled; + + o = s.option(form.Flag, 'server_auto_firewall', _('Auto configure firewall')); + o.default = o.disabled; + + /* Server settings START */ + s = m.section(form.GridSection, 'server', null); + var prefmt = { 'prefix': 'server_', 'suffix': '' }; + s.addremove = true; + s.rowcolors = true; + s.sortable = true; + s.nodescriptions = true; + s.modaltitle = L.bind(hm.loadModalTitle, s, _('Server'), _('Add a server')); + s.sectiontitle = L.bind(hm.loadDefaultLabel, s); + s.renderSectionAdd = L.bind(hm.renderSectionAdd, s, prefmt, false); + s.handleAdd = L.bind(hm.handleAdd, s, prefmt); + + /* General fields */ + o = s.option(form.Value, 'label', _('Label')); + o.load = L.bind(hm.loadDefaultLabel, o); + o.validate = L.bind(hm.validateUniqueValue, o); + o.modalonly = true; + + o = s.option(form.Flag, 'enabled', _('Enable')); + o.default = o.enabled; + o.editable = true; + + o = s.option(form.ListValue, 'type', _('Type')); + o.default = hm.inbound_type[0][0]; + hm.inbound_type.forEach((res) => { + o.value.apply(o, res); + }) + + o = s.option(form.Value, 'listen', _('Listen address')); + o.datatype = 'ipaddr'; + o.placeholder = '::'; + o.modalonly = true; + + o = s.option(form.Value, 'port', _('Listen port')); + o.datatype = 'port'; + o.rmempty = false; + + // dev: Features under development + // rule + // proxy + + /* HTTP / SOCKS fields */ + /* hm.validateAuth */ + o = s.option(form.Value, 'username', _('Username')); + o.validate = L.bind(hm.validateAuthUsername, o); + o.depends({type: /^(http|socks|mixed|hysteria2)$/}); + o.modalonly = true; + + o = s.option(form.Value, 'password', _('Password')); + o.password = true; + o.renderWidget = function() { + var node = form.Value.prototype.renderWidget.apply(this, arguments); + + (node.querySelector('.control-group') || node).appendChild(E('button', { + 'class': 'cbi-button cbi-button-add', + 'title': _('Generate'), + 'click': ui.createHandlerFn(this, handleGenKey, this.option) + }, [ _('Generate') ])); + + return node; + } + o.validate = L.bind(hm.validateAuthPassword, o); + o.rmempty = false; + o.depends({type: /^(http|socks|mixed|hysteria2)$/, username: /.+/}); + o.depends({type: /^(tuic)$/, uuid: /.+/}); + o.modalonly = true; + + /* Hysteria2 fields */ + o = s.option(form.Value, 'hysteria_up_mbps', _('Max upload speed'), + _('In Mbps.')); + o.datatype = 'uinteger'; + o.depends('type', 'hysteria2'); + o.modalonly = true; + + o = s.option(form.Value, 'hysteria_down_mbps', _('Max download speed'), + _('In Mbps.')); + o.datatype = 'uinteger'; + o.depends('type', 'hysteria2'); + o.modalonly = true; + + o = s.option(form.Flag, 'hysteria_ignore_client_bandwidth', _('Ignore client bandwidth'), + _('Tell the client to use the BBR flow control algorithm instead of Hysteria CC.')); + o.default = o.disabled; + o.depends({type: 'hysteria2', hysteria_up_mbps: '', hysteria_down_mbps: ''}); + o.modalonly = true; + + o = s.option(form.ListValue, 'hysteria_obfs_type', _('Obfuscate type')); + o.value('', _('Disable')); + o.value('salamander', _('Salamander')); + o.depends('type', 'hysteria2'); + o.modalonly = true; + + o = s.option(form.Value, 'hysteria_obfs_password', _('Obfuscate password'), + _('Enabling obfuscation will make the server incompatible with standard QUIC connections, losing the ability to masquerade with HTTP/3.')); + o.password = true; + o.renderWidget = function() { + var node = form.Value.prototype.renderWidget.apply(this, arguments); + + (node.querySelector('.control-group') || node).appendChild(E('button', { + 'class': 'cbi-button cbi-button-add', + 'title': _('Generate'), + 'click': ui.createHandlerFn(this, handleGenKey, this.option) + }, [ _('Generate') ])); + + return node; + } + o.rmempty = false; + o.depends('type', 'hysteria'); + o.depends({type: 'hysteria2', hysteria_obfs_type: /.+/}); + o.modalonly = true; + + o = s.option(form.Value, 'hysteria_masquerade', _('Masquerade'), + _('HTTP3 server behavior when authentication fails.
A 404 page will be returned if empty.')); + o.placeholder = 'file:///var/www or http://127.0.0.1:8080' + o.depends('type', 'hysteria2'); + o.modalonly = true; + + /* Shadowsocks fields */ + o = s.option(form.ListValue, 'shadowsocks_chipher', _('Chipher')); + o.default = hm.shadowsocks_cipher_methods[1][0]; + hm.shadowsocks_cipher_methods.forEach((res) => { + o.value.apply(o, res); + }) + o.depends('type', 'shadowsocks'); + o.modalonly = true; + + o = s.option(form.Value, 'shadowsocks_password', _('Password')); + o.password = true; + o.renderWidget = function() { + var node = form.Value.prototype.renderWidget.apply(this, arguments); + + (node.querySelector('.control-group') || node).appendChild(E('button', { + 'class': 'cbi-button cbi-button-add', + 'title': _('Generate'), + 'click': ui.createHandlerFn(this, handleGenKey, this.option) + }, [ _('Generate') ])); + + return node; + } + o.validate = function(section_id, value) { + var encmode = this.section.getOption('shadowsocks_chipher').formvalue(section_id); + return hm.validateShadowsocksPassword.call(this, hm, encmode, section_id, value); + } + o.depends({type: 'shadowsocks', shadowsocks_chipher: /.+/}); + o.modalonly = true; + + /* Tuic fields */ + o = s.option(form.Value, 'uuid', _('UUID')); + o.renderWidget = function() { + var node = form.Value.prototype.renderWidget.apply(this, arguments); + + (node.querySelector('.control-group') || node).appendChild(E('button', { + 'class': 'cbi-button cbi-button-add', + 'title': _('Generate'), + 'click': ui.createHandlerFn(this, handleGenKey, this.option) + }, [ _('Generate') ])); + + return node; + } + o.rmempty = false; + o.validate = L.bind(hm.validateUUID, o); + o.depends('type', 'tuic'); + o.modalonly = true; + + o = s.option(form.ListValue, 'tuic_congestion_controller', _('Congestion controller'), + _('QUIC congestion controller.')); + o.default = 'cubic'; + o.value('cubic', _('cubic')); + o.value('new_reno', _('new_reno')); + o.value('bbr', _('bbr')); + o.depends('type', 'tuic'); + o.modalonly = true; + + o = s.option(form.Value, 'tuic_max_udp_relay_packet_size', _('Max UDP relay packet size')); + o.datatype = 'uinteger'; + o.default = '1500'; + o.depends('type', 'tuic'); + o.modalonly = true; + + o = s.option(form.Value, 'tuic_max_idle_time', _('Idle timeout'), + _('In seconds.')); + o.default = '15000'; + o.validate = L.bind(hm.validateTimeDuration, o); + o.depends('type', 'tuic'); + o.modalonly = true; + + o = s.option(form.Value, 'tuic_authentication_timeout', _('Auth timeout'), + _('In seconds.')); + o.default = '1000'; + o.validate = L.bind(hm.validateTimeDuration, o); + o.depends('type', 'tuic'); + o.modalonly = true; + + /* VMess fields */ + o = s.option(form.Value, 'vmess_uuid', _('UUID')); + o.renderWidget = function() { + var node = form.Value.prototype.renderWidget.apply(this, arguments); + + (node.querySelector('.control-group') || node).appendChild(E('button', { + 'class': 'cbi-button cbi-button-add', + 'title': _('Generate'), + 'click': ui.createHandlerFn(this, handleGenKey, this.option) + }, [ _('Generate') ])); + + return node; + } + o.rmempty = false; + o.validate = L.bind(hm.validateUUID, o); + o.depends('type', 'vmess'); + o.modalonly = true; + + o = s.option(form.Value, 'vmess_alterid', _('Alter ID'), + _('Legacy protocol support (VMess MD5 Authentication) is provided for compatibility purposes only, use of alterId > 1 is not recommended.')); + o.datatype = 'uinteger'; + o.placeholder = '0'; + o.depends('type', 'vmess'); + o.modalonly = true; + + /* TLS fields */ + o = s.option(form.Flag, 'tls', _('TLS')); + o.default = o.disabled; + o.validate = function(section_id, value) { + var type = this.section.getOption('type').formvalue(section_id); + var tls = this.section.getUIElement(section_id, 'tls').node.querySelector('input'); + var tls_alpn = this.section.getUIElement(section_id, 'tls_alpn'); + + // Force enabled + if (['tuic', 'hysteria2'].includes(type)) { + tls.checked = true; + tls.disabled = true; + if (!`${tls_alpn.getValue()}`) + tls_alpn.setValue('h3'); + } else { + tls.disabled = null; + } + + return true; + } + o.depends({type: /^(vmess|tuic|hysteria2)$/}); + o.modalonly = true; + + o = s.option(form.DynamicList, 'tls_alpn', _('TLS ALPN'), + _('List of supported application level protocols, in order of preference.')); + o.depends('tls', '1'); + o.modalonly = true; + + o = s.option(form.Value, 'tls_cert_path', _('Certificate path'), + _('The server public key, in PEM format.')); + o.value('/etc/fchomo/certs/server_publickey.pem'); + o.depends('tls', '1'); + o.rmempty = false; + o.modalonly = true; + + o = s.option(form.Button, '_upload_cert', _('Upload certificate'), + _('Save your configuration before uploading files!')); + o.inputstyle = 'action'; + o.inputtitle = _('Upload...'); + o.depends({tls: '1', tls_cert_path: '/etc/fchomo/certs/server_publickey.pem'}); + o.onclick = L.bind(hm.uploadCertificate, o, _('certificate'), 'server_publickey'); + o.modalonly = true; + + o = s.option(form.Value, 'tls_key_path', _('Key path'), + _('The server private key, in PEM format.')); + o.value('/etc/fchomo/certs/server_privatekey.pem'); + o.rmempty = false; + o.depends({tls: '1', tls_cert_path: /.+/}); + o.modalonly = true; + + o = s.option(form.Button, '_upload_key', _('Upload key'), + _('Save your configuration before uploading files!')); + o.inputstyle = 'action'; + o.inputtitle = _('Upload...'); + o.depends({tls: '1', tls_key_path: '/etc/fchomo/certs/server_privatekey.pem'}); + o.onclick = L.bind(hm.uploadCertificate, o, _('private key'), 'server_privatekey'); + o.modalonly = true; + + /* Extra fields */ + o = s.option(form.Flag, 'udp', _('UDP')); + o.default = o.disabled; + o.depends({type: /^(socks|mixed|shadowsocks)$/}); + o.modalonly = true; + /* Server settings END */ + + return m.render(); + } +}); diff --git a/luci-app-fchomo/po/templates/fchomo.pot b/luci-app-fchomo/po/templates/fchomo.pot new file mode 100644 index 0000000000..5212f5ce0e --- /dev/null +++ b/luci-app-fchomo/po/templates/fchomo.pot @@ -0,0 +1,2635 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +#: htdocs/luci-static/resources/view/fchomo/log.js:69 +msgid "%s log" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:431 +#: htdocs/luci-static/resources/fchomo.js:444 +#: htdocs/luci-static/resources/fchomo.js:457 +#: htdocs/luci-static/resources/fchomo.js:471 +#: htdocs/luci-static/resources/view/fchomo/client.js:289 +#: htdocs/luci-static/resources/view/fchomo/client.js:503 +#: htdocs/luci-static/resources/view/fchomo/client.js:517 +#: htdocs/luci-static/resources/view/fchomo/client.js:986 +#: htdocs/luci-static/resources/view/fchomo/global.js:470 +#: htdocs/luci-static/resources/view/fchomo/node.js:1167 +#: htdocs/luci-static/resources/view/fchomo/node.js:1168 +msgid "-- Please choose --" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:48 +msgid "163Music" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:193 +msgid "2022-blake3-aes-128-gcm" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:194 +msgid "2022-blake3-aes-256-gcm" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:195 +msgid "2022-blake3-chacha20-poly1305" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:344 +#: htdocs/luci-static/resources/view/fchomo/server.js:359 +msgid "Save your configuration before uploading files!" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:458 +msgid "API" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:499 +msgid "API DoH service" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:490 +msgid "API HTTP port" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:494 +msgid "API HTTPS port" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:448 +msgid "API TLS certificate path" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:452 +msgid "API TLS private key path" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:503 +msgid "API secret" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:273 +msgid "ASN version" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:577 +#: htdocs/luci-static/resources/view/fchomo/global.js:627 +msgid "Access Control" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:954 +msgid "Add a DNS policy" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:813 +msgid "Add a DNS server" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:35 +msgid "Add a Node" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:806 +msgid "Add a provider" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1077 +msgid "Add a proxy chain" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:465 +msgid "Add a proxy group" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:656 +msgid "Add a routing rule" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:112 +msgid "Add a rule set" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:97 +msgid "Add a server" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:726 +msgid "Add a sub rule" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:924 +msgid "Add prefix" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:927 +msgid "Add suffix" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:827 +#: htdocs/luci-static/resources/view/fchomo/client.js:832 +msgid "Address" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:630 +#: htdocs/luci-static/resources/view/fchomo/global.js:674 +msgid "All allowed" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:660 +#: htdocs/luci-static/resources/view/fchomo/global.js:667 +msgid "All ports" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:486 +msgid "" +"Allow access from private network.
To access the API on a private " +"network from a public website, it must be enabled." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:378 +msgid "Allowed IPs" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:58 +msgid "Already at the latest version." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:55 +msgid "Already in updating." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:307 +#: htdocs/luci-static/resources/view/fchomo/server.js:301 +msgid "Alter ID" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:144 +msgid "Application version" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:276 +msgid "Auth timeout" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:329 +msgid "Authenticated length" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:304 +msgid "Auto" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:87 +msgid "Auto configure firewall" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:212 +msgid "Auto update" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:213 +msgid "Auto update resources." +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:47 +msgid "Baidu" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:409 +msgid "Based on google/gvisor." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:232 +msgid "Behavior" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:212 +msgid "Binary file" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:218 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:241 +msgid "Binary format only supports domain / ipcidr" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:550 +#: htdocs/luci-static/resources/view/fchomo/global.js:591 +#: htdocs/luci-static/resources/view/fchomo/node.js:775 +#: htdocs/luci-static/resources/view/fchomo/node.js:986 +msgid "Bind interface" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:551 +#: htdocs/luci-static/resources/view/fchomo/node.js:776 +#: htdocs/luci-static/resources/view/fchomo/node.js:987 +msgid "Bind outbound interface.
" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:592 +msgid "" +"Bind outbound traffic to specific interface. Leave empty to auto detect.
" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:632 +msgid "Black list" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:16 +msgid "Block DNS queries" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:764 +msgid "Boot DNS server" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:771 +msgid "Boot DNS server (Node)" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:675 +msgid "Bypass CN" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:481 +msgid "CORS Allow origins" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:485 +msgid "CORS Allow private network" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:482 +msgid "CORS allowed origins, * will be used if empty." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:131 +msgid "Cancel" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:536 +msgid "Cert fingerprint" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:537 +msgid "" +"Certificate fingerprint. Used to implement SSL Pinning and prevent MitM." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:336 +msgid "Certificate path" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1119 +#: htdocs/luci-static/resources/view/fchomo/node.js:1125 +msgid "Chain head" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1166 +#: htdocs/luci-static/resources/view/fchomo/node.js:1173 +msgid "Chain tail" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:194 +msgid "Check" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:75 +msgid "Check update" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:276 +msgid "China IPv4 list version" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:279 +msgid "China IPv6 list version" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:285 +msgid "China list version" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:150 +#: htdocs/luci-static/resources/view/fchomo/node.js:313 +#: htdocs/luci-static/resources/view/fchomo/server.js:208 +msgid "Chipher" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/log.js:76 +msgid "Clean log" +msgstr "" + +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:22 +msgid "Client" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:558 +msgid "Client fingerprint" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:149 +msgid "Client status" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/log.js:39 +msgid "Collecting data..." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:662 +#: htdocs/luci-static/resources/view/fchomo/global.js:669 +msgid "Common and STUN ports" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:661 +#: htdocs/luci-static/resources/view/fchomo/global.js:668 +msgid "Common ports only (bypass P2P traffic)" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:640 +msgid "Complete" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:941 +msgid "Configuration Items" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:217 +#: htdocs/luci-static/resources/view/fchomo/server.js:254 +msgid "Congestion controller" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:170 +msgid "Connection check" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:867 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:273 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:294 +msgid "Content will not be verified, Please make sure you enter it correctly." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:859 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:265 +msgid "Contents" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/hosts.js:19 +msgid "Contents have been saved." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:139 +msgid "Core version" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:225 +msgid "Cron expression" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:691 +msgid "Custom Direct List" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:908 +msgid "Custom HTTP header." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:716 +msgid "Custom Proxy List" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/hosts.js:28 +msgid "" +"Custom internal hosts. Support yaml or json format." +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:86 +msgid "DIRECT" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:944 +#: htdocs/luci-static/resources/view/fchomo/client.js:954 +msgid "DNS policy" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:374 +msgid "DNS port" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:803 +#: htdocs/luci-static/resources/view/fchomo/client.js:813 +#: htdocs/luci-static/resources/view/fchomo/client.js:999 +#: htdocs/luci-static/resources/view/fchomo/node.js:402 +msgid "DNS server" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:750 +msgid "DNS settings" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:243 +msgid "Dashboard version" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:313 +msgid "Debug" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:228 +msgid "Default" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:14 +msgid "Default DNS (issued by WAN)" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:778 +msgid "Default DNS server" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:379 +msgid "Destination addresses allowed to be forwarded via Wireguard." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:44 +msgid "Dial fields" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1134 +#: htdocs/luci-static/resources/view/fchomo/node.js:1182 +msgid "Different chain head/tail" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:297 +msgid "Direct" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:634 +msgid "Direct IPv4 IP-s" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:637 +msgid "Direct IPv6 IP-s" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:640 +msgid "Direct MAC-s" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:305 +#: htdocs/luci-static/resources/view/fchomo/node.js:116 +#: htdocs/luci-static/resources/view/fchomo/server.js:177 +msgid "Disable" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:568 +msgid "Disable ECN of quic-go" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:565 +msgid "Disable GSO of quic-go" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:519 +msgid "Disable SNI" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:546 +msgid "Disable UDP" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:969 +#: htdocs/luci-static/resources/view/fchomo/client.js:974 +#: htdocs/luci-static/resources/view/fchomo/client.js:1039 +#: htdocs/luci-static/resources/view/fchomo/client.js:1046 +#: htdocs/luci-static/resources/view/fchomo/client.js:1048 +msgid "Domain" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:520 +msgid "Donot send server name in ClientHello." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:550 +#: htdocs/luci-static/resources/view/fchomo/node.js:974 +msgid "Donot verifying server certificate." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:754 +msgid "Download bandwidth" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:755 +msgid "Download bandwidth in Mbps." +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:546 +msgid "Download failed: %s" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:544 +msgid "Download successful." +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:72 +msgid "Dual stack" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:924 +msgid "ECS override" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:908 +msgid "EDNS Client Subnet" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:316 +msgid "ETag support" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:667 +msgid "Early Data first packet length limit." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:673 +msgid "Early Data header name" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:20 +msgid "Edit node" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:102 +msgid "Edit ruleset" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:857 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:263 +msgid "Editer" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:454 +#: htdocs/luci-static/resources/view/fchomo/client.js:485 +#: htdocs/luci-static/resources/view/fchomo/client.js:666 +#: htdocs/luci-static/resources/view/fchomo/client.js:736 +#: htdocs/luci-static/resources/view/fchomo/client.js:823 +#: htdocs/luci-static/resources/view/fchomo/client.js:964 +#: htdocs/luci-static/resources/view/fchomo/global.js:303 +#: htdocs/luci-static/resources/view/fchomo/global.js:540 +#: htdocs/luci-static/resources/view/fchomo/node.js:51 +#: htdocs/luci-static/resources/view/fchomo/node.js:833 +#: htdocs/luci-static/resources/view/fchomo/node.js:1006 +#: htdocs/luci-static/resources/view/fchomo/node.js:1087 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:199 +#: htdocs/luci-static/resources/view/fchomo/server.js:84 +#: htdocs/luci-static/resources/view/fchomo/server.js:108 +msgid "Enable" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:252 +msgid "" +"Enable 0-RTT QUIC connection handshake on the client side. This is not " +"impacting much on the performance, as the protocol is fully multiplexed.
Disabling this is highly recommended, as it is vulnerable to replay attacks." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:251 +msgid "Enable 0-RTT handshake" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:571 +msgid "" +"Enable IP4P " +"conversion for outbound connections" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:742 +msgid "Enable TCP Brutal" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:743 +msgid "Enable TCP Brutal congestion control algorithm" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:731 +msgid "Enable multiplexing only for TCP." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:725 +msgid "Enable padding" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:736 +msgid "Enable statistic" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:464 +#: htdocs/luci-static/resources/view/fchomo/node.js:959 +msgid "" +"Enable the SUoT protocol, requires server support. Conflict with Multiplex." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:122 +#: htdocs/luci-static/resources/view/fchomo/server.js:183 +msgid "" +"Enabling obfuscation will make the server incompatible with standard QUIC " +"connections, losing the ability to masquerade with HTTP/3." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:430 +msgid "Endpoint-Independent NAT" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:305 +#: htdocs/luci-static/resources/view/fchomo/client.js:991 +msgid "Entry" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:310 +msgid "Error" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:602 +msgid "" +"Exceeding this triggers a forced health check. 5 will be used " +"if empty." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1056 +msgid "Exclude matched node types." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:639 +msgid "" +"Exclude matched node types. Available types see here." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:634 +#: htdocs/luci-static/resources/view/fchomo/node.js:1050 +msgid "Exclude nodes that meet keywords or regexps." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:594 +#: htdocs/luci-static/resources/view/fchomo/node.js:1037 +msgid "Expected HTTP code. 204 will be used if empty." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:596 +#: htdocs/luci-static/resources/view/fchomo/node.js:1039 +msgid "Expected status" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:570 +#: htdocs/luci-static/resources/fchomo.js:573 +#: htdocs/luci-static/resources/fchomo.js:576 +#: htdocs/luci-static/resources/fchomo.js:657 +#: htdocs/luci-static/resources/fchomo.js:665 +#: htdocs/luci-static/resources/fchomo.js:673 +#: htdocs/luci-static/resources/fchomo.js:697 +#: htdocs/luci-static/resources/fchomo.js:714 +#: htdocs/luci-static/resources/fchomo.js:717 +#: htdocs/luci-static/resources/fchomo.js:727 +#: htdocs/luci-static/resources/fchomo.js:740 +#: htdocs/luci-static/resources/fchomo.js:742 +#: htdocs/luci-static/resources/fchomo.js:755 +#: htdocs/luci-static/resources/fchomo.js:764 +#: htdocs/luci-static/resources/fchomo.js:771 +#: htdocs/luci-static/resources/fchomo.js:780 +#: htdocs/luci-static/resources/fchomo.js:792 +#: htdocs/luci-static/resources/fchomo.js:795 +#: htdocs/luci-static/resources/fchomo.js:805 +#: htdocs/luci-static/resources/view/fchomo/client.js:27 +#: htdocs/luci-static/resources/view/fchomo/client.js:479 +#: htdocs/luci-static/resources/view/fchomo/client.js:838 +#: htdocs/luci-static/resources/view/fchomo/node.js:542 +#: htdocs/luci-static/resources/view/fchomo/node.js:1134 +#: htdocs/luci-static/resources/view/fchomo/node.js:1182 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:218 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:241 +msgid "Expecting: %s" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:601 +#: htdocs/luci-static/resources/view/fchomo/node.js:608 +msgid "Expecting: only support %s." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:559 +msgid "Experimental" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:243 +#: htdocs/luci-static/resources/view/fchomo/client.js:256 +#: htdocs/luci-static/resources/view/fchomo/client.js:266 +#: htdocs/luci-static/resources/view/fchomo/client.js:273 +#: htdocs/luci-static/resources/view/fchomo/client.js:281 +#: htdocs/luci-static/resources/view/fchomo/client.js:288 +msgid "Factor" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:598 +msgid "Failed to execute \"/etc/init.d/fchomo %s %s\" reason: %s" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:905 +msgid "Failed to upload %s, error: %s." +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:924 +msgid "Failed to upload, error: %s." +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:123 +msgid "Fallback" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:785 +#: htdocs/luci-static/resources/view/fchomo/client.js:786 +#: htdocs/luci-static/resources/view/fchomo/client.js:797 +msgid "Fallback DNS server" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:1018 +msgid "Fallback filter" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:629 +#: htdocs/luci-static/resources/view/fchomo/node.js:1045 +msgid "Filter nodes that meet keywords or regexps." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:779 +#: htdocs/luci-static/resources/view/fchomo/client.js:796 +msgid "Final DNS server" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:779 +#: htdocs/luci-static/resources/view/fchomo/client.js:793 +msgid "Final DNS server (Used to Domestic-IP response)" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:786 +#: htdocs/luci-static/resources/view/fchomo/client.js:794 +msgid "Final DNS server (Used to Overseas-IP response)" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:301 +msgid "Flow" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:618 +msgid "" +"For details, see %s." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:595 +#: htdocs/luci-static/resources/view/fchomo/node.js:932 +#: htdocs/luci-static/resources/view/fchomo/node.js:1038 +msgid "" +"For format see %s." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:397 +msgid "Force DNS remote resolution." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:519 +msgid "Forced sniffing domain" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:209 +msgid "Format" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:127 +#: htdocs/luci-static/resources/view/fchomo/log.js:97 +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:3 +msgid "FullCombo Mihomo" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:627 +msgid "GET" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:282 +msgid "GFW list version" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:290 +msgid "General" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:470 +#: htdocs/luci-static/resources/view/fchomo/node.js:40 +#: htdocs/luci-static/resources/view/fchomo/node.js:823 +msgid "General fields" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:293 +msgid "General settings" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:145 +#: htdocs/luci-static/resources/view/fchomo/server.js:147 +#: htdocs/luci-static/resources/view/fchomo/server.js:190 +#: htdocs/luci-static/resources/view/fchomo/server.js:192 +#: htdocs/luci-static/resources/view/fchomo/server.js:223 +#: htdocs/luci-static/resources/view/fchomo/server.js:225 +#: htdocs/luci-static/resources/view/fchomo/server.js:243 +#: htdocs/luci-static/resources/view/fchomo/server.js:245 +#: htdocs/luci-static/resources/view/fchomo/server.js:290 +#: htdocs/luci-static/resources/view/fchomo/server.js:292 +msgid "Generate" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:418 +msgid "Generic segmentation offload" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:267 +msgid "GeoIP version" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:270 +msgid "GeoSite version" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:1028 +msgid "Geoip code" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:1025 +msgid "Geoip enable" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:970 +#: htdocs/luci-static/resources/view/fchomo/client.js:979 +#: htdocs/luci-static/resources/view/fchomo/client.js:1037 +msgid "Geosite" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:50 +msgid "GitHub" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:299 +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:14 +msgid "Global" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:339 +msgid "Global Authentication" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:442 +msgid "Global client fingerprint" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:323 +msgid "Global padding" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:49 +msgid "Google" +msgstr "" + +#: root/usr/share/rpcd/acl.d/luci-app-fchomo.json:3 +msgid "Grant access to fchomo configuration" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:495 +msgid "Group" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:60 +#: htdocs/luci-static/resources/fchomo.js:87 +#: htdocs/luci-static/resources/view/fchomo/node.js:418 +#: htdocs/luci-static/resources/view/fchomo/node.js:590 +#: htdocs/luci-static/resources/view/fchomo/node.js:601 +msgid "HTTP" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:84 +#: htdocs/luci-static/resources/view/fchomo/node.js:649 +#: htdocs/luci-static/resources/view/fchomo/node.js:907 +msgid "HTTP header" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:626 +msgid "HTTP request method" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:892 +msgid "HTTP/3" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:202 +msgid "" +"HTTP3 server behavior when authentication fails.
A 404 page will be " +"returned if empty." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:591 +#: htdocs/luci-static/resources/view/fchomo/node.js:602 +msgid "HTTPUpgrade" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:678 +msgid "Handle domain" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:564 +#: htdocs/luci-static/resources/view/fchomo/node.js:1010 +msgid "Health check URL" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:593 +#: htdocs/luci-static/resources/view/fchomo/node.js:1036 +msgid "Health check expected status" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:573 +#: htdocs/luci-static/resources/view/fchomo/node.js:1019 +msgid "Health check interval" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:580 +#: htdocs/luci-static/resources/view/fchomo/node.js:1025 +msgid "Health check timeout" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:472 +#: htdocs/luci-static/resources/view/fchomo/node.js:825 +msgid "Health fields" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:258 +msgid "Heartbeat interval" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:424 +msgid "Host that supports TLS 1.3" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:144 +msgid "Host-key" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:139 +msgid "Host-key algorithms" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/hosts.js:27 +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:30 +msgid "Hosts" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:66 +#: htdocs/luci-static/resources/fchomo.js:97 +msgid "Hysteria2" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:1030 +#: htdocs/luci-static/resources/view/fchomo/client.js:1043 +msgid "IP" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:1041 +msgid "IP CIDR" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:211 +msgid "IP override" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:787 +#: htdocs/luci-static/resources/view/fchomo/node.js:998 +msgid "IP version" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:73 +msgid "IPv4 only" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:74 +msgid "IPv6 only" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:761 +#: htdocs/luci-static/resources/view/fchomo/global.js:319 +msgid "IPv6 support" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:269 +msgid "Idle timeout" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:27 +msgid "If Block is selected, uncheck others" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:170 +msgid "Ignore client bandwidth" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:170 +msgid "Import" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:121 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:179 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:181 +msgid "Import rule-set links" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:104 +#: htdocs/luci-static/resources/view/fchomo/node.js:110 +#: htdocs/luci-static/resources/view/fchomo/node.js:964 +#: htdocs/luci-static/resources/view/fchomo/node.js:969 +#: htdocs/luci-static/resources/view/fchomo/server.js:159 +#: htdocs/luci-static/resources/view/fchomo/server.js:165 +msgid "In Mbps." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:885 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:306 +msgid "In bytes. %s will be used if empty." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:259 +#: htdocs/luci-static/resources/view/fchomo/node.js:266 +msgid "In millisecond." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:581 +#: htdocs/luci-static/resources/view/fchomo/client.js:610 +#: htdocs/luci-static/resources/view/fchomo/node.js:1026 +msgid "In millisecond. %s will be used if empty." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:270 +#: htdocs/luci-static/resources/view/fchomo/server.js:277 +msgid "In seconds." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:574 +#: htdocs/luci-static/resources/view/fchomo/global.js:329 +#: htdocs/luci-static/resources/view/fchomo/global.js:334 +#: htdocs/luci-static/resources/view/fchomo/global.js:426 +#: htdocs/luci-static/resources/view/fchomo/node.js:891 +#: htdocs/luci-static/resources/view/fchomo/node.js:1020 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:312 +msgid "In seconds. %s will be used if empty." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:353 +msgid "Inbound" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:530 +msgid "Include all" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:535 +msgid "Include all node" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:540 +msgid "Include all provider" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:541 +msgid "Includes all Provider." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:531 +msgid "Includes all Proxy Node and Provider." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:536 +msgid "Includes all Proxy Node." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:312 +msgid "Info" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:206 +msgid "Inline" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:584 +msgid "Interface Control" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:71 +msgid "Keep default" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:351 +msgid "Key path" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:475 +#: htdocs/luci-static/resources/view/fchomo/client.js:661 +#: htdocs/luci-static/resources/view/fchomo/client.js:731 +#: htdocs/luci-static/resources/view/fchomo/client.js:818 +#: htdocs/luci-static/resources/view/fchomo/client.js:959 +#: htdocs/luci-static/resources/view/fchomo/node.js:46 +#: htdocs/luci-static/resources/view/fchomo/node.js:828 +#: htdocs/luci-static/resources/view/fchomo/node.js:1082 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:194 +#: htdocs/luci-static/resources/view/fchomo/server.js:103 +msgid "Label" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:587 +#: htdocs/luci-static/resources/view/fchomo/node.js:1031 +msgid "Lazy" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:302 +msgid "" +"Legacy protocol support (VMess MD5 Authentication) is provided for " +"compatibility purposes only, use of alterId > 1 is not recommended." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:411 +msgid "Less compatibility and sometimes better performance." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:532 +#: htdocs/luci-static/resources/view/fchomo/server.js:332 +msgid "List of supported application level protocols, in order of preference." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:118 +msgid "Listen address" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:586 +msgid "Listen interfaces" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:756 +#: htdocs/luci-static/resources/view/fchomo/server.js:123 +msgid "Listen port" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:356 +msgid "Listen ports" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:125 +msgid "Load balance" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:838 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:204 +msgid "Local" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:350 +msgid "Local IPv6 address" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:343 +msgid "Local address" +msgstr "" + +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:62 +msgid "Log" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/log.js:54 +msgid "Log file does not exist." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/log.js:47 +msgid "Log is empty." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:308 +msgid "Log level" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:570 +msgid "Lowercase only" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:414 +#: htdocs/luci-static/resources/view/fchomo/node.js:390 +msgid "MTU" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:201 +msgid "Masquerade" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:975 +msgid "Match domain. Support wildcards." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:1047 +msgid "Match domain. Support wildcards.
" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:980 +msgid "Match geosite." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:1038 +msgid "Match geosite.
" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:1029 +msgid "Match response with geoip.
" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:1042 +msgid "Match response with ipcidr.
" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:985 +msgid "Match rule set." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:666 +msgid "Max Early Data" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:245 +#: htdocs/luci-static/resources/view/fchomo/server.js:263 +msgid "Max UDP relay packet size" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:601 +msgid "Max count of failures" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:109 +#: htdocs/luci-static/resources/view/fchomo/server.js:164 +msgid "Max download speed" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:103 +#: htdocs/luci-static/resources/view/fchomo/server.js:158 +msgid "Max upload speed" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:703 +#: htdocs/luci-static/resources/view/fchomo/node.js:719 +msgid "Maximum connections" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:717 +msgid "" +"Maximum multiplexed streams in a connection before opening a new connection." +"
Conflict with %s and %s." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:716 +msgid "Maximum streams" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:91 +msgid "Mieru" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:426 +#: htdocs/luci-static/resources/view/fchomo/log.js:100 +msgid "Mihomo client" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/log.js:103 +#: htdocs/luci-static/resources/view/fchomo/server.js:58 +msgid "Mihomo server" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:710 +msgid "" +"Minimum multiplexed streams in a connection before opening a new connection." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:709 +#: htdocs/luci-static/resources/view/fchomo/node.js:719 +msgid "Minimum streams" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:62 +#: htdocs/luci-static/resources/view/fchomo/global.js:400 +msgid "Mixed" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:407 +msgid "Mixed system TCP stack and gVisor UDP stack." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:359 +msgid "Mixed port" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:689 +msgid "Multiplex" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:43 +msgid "Multiplex fields" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:179 +msgid "Multiplexing" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:897 +msgid "Name of the Proxy group to download provider." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:318 +msgid "Name of the Proxy group to download rule set." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:229 +msgid "Native" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:347 +msgid "No Authentication IP ranges" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:838 +msgid "No add'l params" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:588 +#: htdocs/luci-static/resources/view/fchomo/node.js:1032 +msgid "No testing is performed when this provider node is not in use." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:156 +msgid "No valid rule-set link found." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:502 +#: htdocs/luci-static/resources/view/fchomo/node.js:35 +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:38 +msgid "Node" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:633 +#: htdocs/luci-static/resources/view/fchomo/node.js:1049 +msgid "Node exclude filter" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:638 +#: htdocs/luci-static/resources/view/fchomo/node.js:1055 +msgid "Node exclude type" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:628 +#: htdocs/luci-static/resources/view/fchomo/node.js:1044 +msgid "Node filter" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:609 +msgid "Node switch tolerance" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:302 +msgid "None" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:473 +msgid "Not Installed" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:504 +msgid "Not Running" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:417 +msgid "Obfs Mode" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:121 +#: htdocs/luci-static/resources/view/fchomo/server.js:182 +msgid "Obfuscate password" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:115 +#: htdocs/luci-static/resources/view/fchomo/server.js:176 +msgid "Obfuscate type" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:587 +msgid "Only process traffic from specific interfaces. Leave empty for all." +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:498 +msgid "Open Dashboard" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:296 +msgid "Operation mode" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:925 +msgid "Override ECS in original request." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:515 +#: htdocs/luci-static/resources/view/fchomo/global.js:553 +msgid "Override destination" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:471 +#: htdocs/luci-static/resources/view/fchomo/node.js:824 +msgid "Override fields" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:212 +msgid "Override the IP address of the server that DNS response." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:1008 +msgid "Override the Proxy group of DNS server." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:516 +msgid "Override the connection destination address with the sniffed domain." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:133 +msgid "Overview" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:628 +msgid "POST" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:629 +msgid "PUT" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:335 +msgid "Packet encoding" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:78 +#: htdocs/luci-static/resources/view/fchomo/node.js:158 +#: htdocs/luci-static/resources/view/fchomo/node.js:431 +#: htdocs/luci-static/resources/view/fchomo/server.js:138 +#: htdocs/luci-static/resources/view/fchomo/server.js:216 +msgid "Password" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:286 +msgid "Payload" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:364 +msgid "Peer pubkic key" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:431 +msgid "" +"Performance may degrade slightly, so it is not recommended to enable on when " +"it is not needed." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:210 +msgid "Plain text" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:124 +msgid "" +"Please refer to %s for link format " +"standards." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:858 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:264 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:285 +msgid "" +"Please type %s." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:408 +msgid "Plugin" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:417 +#: htdocs/luci-static/resources/view/fchomo/node.js:424 +#: htdocs/luci-static/resources/view/fchomo/node.js:431 +#: htdocs/luci-static/resources/view/fchomo/node.js:437 +#: htdocs/luci-static/resources/view/fchomo/node.js:445 +#: htdocs/luci-static/resources/view/fchomo/node.js:451 +msgid "Plugin:" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:66 +msgid "Port" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:699 +msgid "Port %s alrealy exists!" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:168 +msgid "Port range" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:550 +msgid "Ports" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:98 +msgid "Ports pool" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:189 +#: htdocs/luci-static/resources/view/fchomo/node.js:371 +msgid "Pre-shared key" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:75 +msgid "Prefer IPv4" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:76 +msgid "Prefer IPv6" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:552 +#: htdocs/luci-static/resources/view/fchomo/client.js:558 +#: htdocs/luci-static/resources/view/fchomo/global.js:593 +#: htdocs/luci-static/resources/view/fchomo/global.js:610 +#: htdocs/luci-static/resources/view/fchomo/node.js:777 +#: htdocs/luci-static/resources/view/fchomo/node.js:783 +#: htdocs/luci-static/resources/view/fchomo/node.js:988 +#: htdocs/luci-static/resources/view/fchomo/node.js:994 +msgid "Priority: Proxy Node > Proxy Group > Global." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:130 +msgid "Priv-key" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:134 +msgid "Priv-key passphrase" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:356 +msgid "Private key" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:302 +msgid "Process matching mode" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:544 +#: htdocs/luci-static/resources/view/fchomo/node.js:695 +msgid "Protocol" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:330 +msgid "Protocol parameter. Enable length block encryption." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:324 +msgid "" +"Protocol parameter. Will waste traffic randomly if enabled (enabled by " +"default in v2ray and cannot be disabled)." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:516 +#: htdocs/luci-static/resources/view/fchomo/node.js:796 +#: htdocs/luci-static/resources/view/fchomo/node.js:806 +#: htdocs/luci-static/resources/view/fchomo/node.js:1093 +msgid "Provider" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:878 +msgid "Provider URL" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:446 +#: htdocs/luci-static/resources/view/fchomo/client.js:465 +msgid "Proxy Group" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:643 +msgid "Proxy IPv4 IP-s" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:646 +msgid "Proxy IPv6 IP-s" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:649 +msgid "Proxy MAC-s" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:25 +#: htdocs/luci-static/resources/view/fchomo/node.js:1092 +msgid "Proxy Node" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1067 +#: htdocs/luci-static/resources/view/fchomo/node.js:1077 +msgid "Proxy chain" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:361 +#: htdocs/luci-static/resources/view/fchomo/client.js:875 +#: htdocs/luci-static/resources/view/fchomo/client.js:1007 +#: htdocs/luci-static/resources/view/fchomo/node.js:896 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:317 +msgid "Proxy group" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:379 +msgid "Proxy mode" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:652 +msgid "Proxy routerself" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:230 +msgid "QUIC" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:218 +#: htdocs/luci-static/resources/view/fchomo/server.js:255 +msgid "QUIC congestion controller." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:449 +#: htdocs/luci-static/resources/view/fchomo/server.js:79 +msgid "Quick Reload" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:567 +msgid "REALITY" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:572 +msgid "REALITY public key" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:577 +msgid "REALITY short ID" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:504 +msgid "Random will be used if empty." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1126 +#: htdocs/luci-static/resources/view/fchomo/node.js:1174 +msgid "Recommended to use UoT node.
such as %s." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:364 +msgid "Redir port" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:380 +msgid "Redirect TCP" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:382 +msgid "Redirect TCP + TProxy UDP" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:384 +msgid "Redirect TCP + Tun UDP" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/log.js:81 +msgid "Refresh every %s seconds." +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:491 +#: htdocs/luci-static/resources/view/fchomo/client.js:450 +#: htdocs/luci-static/resources/view/fchomo/global.js:166 +#: htdocs/luci-static/resources/view/fchomo/server.js:80 +msgid "Reload" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:165 +msgid "Reload All" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:839 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:205 +msgid "Remote" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:396 +msgid "Remote DNS resolve" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:629 +msgid "Remove" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:634 +#: htdocs/luci-static/resources/view/fchomo/node.js:814 +#: htdocs/luci-static/resources/view/fchomo/node.js:816 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:185 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:187 +msgid "Remove idles" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:930 +msgid "Replace name" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:931 +msgid "Replace node name." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:635 +#: htdocs/luci-static/resources/view/fchomo/node.js:642 +msgid "Request path" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:265 +msgid "Request timeout" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:385 +msgid "Reserved field bytes" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:202 +msgid "Resources management" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:451 +msgid "Restls script" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:656 +msgid "Routing Control" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:676 +msgid "Routing GFW" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:557 +#: htdocs/luci-static/resources/view/fchomo/global.js:609 +#: htdocs/luci-static/resources/view/fchomo/node.js:782 +#: htdocs/luci-static/resources/view/fchomo/node.js:993 +msgid "Routing mark" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:672 +msgid "Routing mode" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:673 +msgid "Routing mode of the traffic enters mihomo via firewall rules." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:679 +msgid "Routing mode will be handle domain." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:658 +#: htdocs/luci-static/resources/view/fchomo/global.js:665 +msgid "Routing ports" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:646 +#: htdocs/luci-static/resources/view/fchomo/client.js:656 +msgid "Routing rule" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:603 +msgid "Routing rule priority" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:597 +msgid "Routing table ID" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:298 +msgid "Rule" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:971 +#: htdocs/luci-static/resources/view/fchomo/client.js:984 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:112 +msgid "Rule set" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:299 +msgid "Rule set URL" +msgstr "" + +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:46 +msgid "Ruleset" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:125 +msgid "Ruleset-URI-Scheme" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:504 +msgid "Running" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:61 +msgid "SOCKS" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:88 +msgid "SOCKS5" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:100 +msgid "SSH" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:672 +msgid "SUB-RULE" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:469 +msgid "SUoT version" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:117 +#: htdocs/luci-static/resources/view/fchomo/server.js:178 +msgid "Salamander" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:81 +msgid "Same dstaddr requests. Same node" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:82 +msgid "Same srcaddr and dstaddr requests. Same node" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:421 +msgid "Segment maximum size" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:122 +msgid "Select" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:464 +msgid "Select Dashboard" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:97 +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:54 +msgid "Server" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:61 +msgid "Server address" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:620 +msgid "Server hostname" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:157 +msgid "Server status" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:136 +msgid "Service status" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:63 +#: htdocs/luci-static/resources/fchomo.js:89 +msgid "Shadowsocks" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:278 +msgid "Shadowsocks chipher" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:273 +msgid "Shadowsocks encrypt" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:285 +msgid "Shadowsocks password" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:737 +msgid "Show connections in the dashboard for breaking connections easier." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:309 +msgid "Silent" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:80 +msgid "Simple round-robin all nodes" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:884 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:305 +msgid "Size limit" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:549 +#: htdocs/luci-static/resources/view/fchomo/node.js:973 +msgid "Skip cert verify" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:522 +msgid "Skiped sniffing domain" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:528 +msgid "Skiped sniffing dst address" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:525 +msgid "Skiped sniffing src address" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:92 +msgid "Snell" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:532 +msgid "Sniff protocol" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:509 +msgid "Sniffer" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:512 +msgid "Sniffer settings" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:659 +#: htdocs/luci-static/resources/view/fchomo/global.js:666 +msgid "" +"Specify target ports to be proxied. Multiple ports must be separated by " +"commas." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:395 +msgid "Stack" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:617 +#: htdocs/luci-static/resources/view/fchomo/client.js:619 +msgid "Strategy" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:695 +#: htdocs/luci-static/resources/view/fchomo/client.js:716 +#: htdocs/luci-static/resources/view/fchomo/client.js:726 +msgid "Sub rule" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:740 +msgid "Sub rule group" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:158 +msgid "Successfully imported %s rule-set of total %s." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:49 +msgid "Successfully updated." +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:921 +msgid "Successfully uploaded." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:122 +msgid "" +"Supports rule-set links of type: %s and format: %s." +"
" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:397 +msgid "System" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:15 +msgid "System DNS" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:275 +msgid "TCP" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:325 +msgid "TCP concurrency" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:730 +msgid "TCP only" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:333 +msgid "TCP-Keep-Alive idle timeout" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:328 +msgid "TCP-Keep-Alive interval" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:761 +#: htdocs/luci-static/resources/view/fchomo/node.js:946 +msgid "TFO" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:436 +#: htdocs/luci-static/resources/view/fchomo/node.js:419 +#: htdocs/luci-static/resources/view/fchomo/node.js:477 +#: htdocs/luci-static/resources/view/fchomo/server.js:309 +msgid "TLS" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:531 +#: htdocs/luci-static/resources/view/fchomo/server.js:331 +msgid "TLS ALPN" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:525 +msgid "TLS SNI" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:41 +msgid "TLS fields" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:65 +#: htdocs/luci-static/resources/fchomo.js:98 +msgid "TUIC" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:171 +msgid "" +"Tell the client to use the BBR flow control algorithm instead of Hysteria CC." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:344 +#: htdocs/luci-static/resources/view/fchomo/node.js:351 +msgid "The %s address used by local machine in the Wireguard network." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:226 +msgid "The default value is 2:00 every day." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:1030 +msgid "The matching %s will be deemed as not-poisoned." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:1039 +#: htdocs/luci-static/resources/view/fchomo/client.js:1043 +#: htdocs/luci-static/resources/view/fchomo/client.js:1048 +msgid "The matching %s will be deemed as poisoned." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:352 +msgid "The server private key, in PEM format." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:337 +msgid "The server public key, in PEM format." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:552 +#: htdocs/luci-static/resources/view/fchomo/node.js:976 +msgid "" +"This is DANGEROUS, your traffic is almost like " +"PLAIN TEXT! Use at your own risk!" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:235 +msgid "" +"This is the TUIC port of the SUoT protocol, designed to provide a QUIC " +"stream based UDP relay mode that TUIC does not provide." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:387 +msgid "" +"To enable Tun support, you need to install ip-full and " +"kmod-tun" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:682 +msgid "To enable, you need to install dnsmasq-full." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:616 +msgid "Tproxy Fwmark" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:369 +msgid "Tproxy port" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:173 +#: htdocs/luci-static/resources/view/fchomo/node.js:583 +msgid "Transport" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:42 +msgid "Transport fields" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:588 +msgid "Transport type" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:95 +msgid "Trojan" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:621 +msgid "Tun Fwmark" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:385 +msgid "Tun TCP/UDP" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:392 +msgid "Tun settings" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:396 +msgid "Tun stack." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:223 +#: htdocs/luci-static/resources/view/fchomo/client.js:313 +#: htdocs/luci-static/resources/view/fchomo/client.js:489 +#: htdocs/luci-static/resources/view/fchomo/client.js:968 +#: htdocs/luci-static/resources/view/fchomo/node.js:55 +#: htdocs/luci-static/resources/view/fchomo/node.js:837 +#: htdocs/luci-static/resources/view/fchomo/node.js:1091 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:203 +#: htdocs/luci-static/resources/view/fchomo/server.js:112 +msgid "Type" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:274 +#: htdocs/luci-static/resources/view/fchomo/node.js:458 +#: htdocs/luci-static/resources/view/fchomo/node.js:954 +#: htdocs/luci-static/resources/view/fchomo/server.js:367 +msgid "UDP" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:425 +msgid "UDP NAT expiration time" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:234 +msgid "UDP over stream" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:240 +msgid "UDP over stream version" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:227 +msgid "UDP packet relay mode." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:226 +msgid "UDP relay mode" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:124 +msgid "URL test" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:205 +#: htdocs/luci-static/resources/view/fchomo/node.js:295 +#: htdocs/luci-static/resources/view/fchomo/server.js:237 +#: htdocs/luci-static/resources/view/fchomo/server.js:284 +msgid "UUID" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:549 +msgid "Unable to download unsupported type: %s" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/hosts.js:21 +msgid "Unable to save contents: %s" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:322 +msgid "Unified delay" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:141 +#: htdocs/luci-static/resources/view/fchomo/global.js:146 +msgid "Unknown" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:61 +msgid "Unknown error." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/log.js:58 +msgid "Unknown error: %s" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:463 +#: htdocs/luci-static/resources/view/fchomo/node.js:958 +msgid "UoT" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:52 +msgid "Update failed." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:890 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:311 +msgid "Update interval" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:748 +msgid "Upload bandwidth" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:749 +msgid "Upload bandwidth in Mbps." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:343 +msgid "Upload certificate" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:206 +msgid "Upload initial package" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:358 +msgid "Upload key" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:208 +#: htdocs/luci-static/resources/view/fchomo/server.js:346 +#: htdocs/luci-static/resources/view/fchomo/server.js:361 +msgid "Upload..." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:765 +msgid "Used to resolve the domain of the DNS server. Must be IP." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:772 +msgid "Used to resolve the domain of the Proxy node." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:526 +msgid "Used to verify the hostname on the returned certificates." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:342 +msgid "User Authentication" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:73 +#: htdocs/luci-static/resources/view/fchomo/server.js:133 +msgid "Username" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:629 +msgid "Users filter mode" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:678 +msgid "V2ray HTTPUpgrade" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:683 +msgid "V2ray HTTPUpgrade fast open" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:94 +msgid "VLESS" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:64 +#: htdocs/luci-static/resources/fchomo.js:93 +msgid "VMess" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:842 +#: htdocs/luci-static/resources/view/fchomo/node.js:1097 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:246 +msgid "Value" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:196 +#: htdocs/luci-static/resources/view/fchomo/node.js:437 +msgid "Version" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:445 +msgid "Version hint" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:311 +msgid "Warning" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:593 +#: htdocs/luci-static/resources/view/fchomo/node.js:604 +#: htdocs/luci-static/resources/view/fchomo/node.js:609 +msgid "WebSocket" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:59 +msgid "When used as a server, HomeProxy is a better choice." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:631 +msgid "White list" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:99 +msgid "WireGuard" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:365 +msgid "WireGuard peer public key." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:372 +msgid "WireGuard pre-shared key." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:357 +msgid "WireGuard requires base64-encoded private keys." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:338 +msgid "Xudp (Xray-core)" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:211 +msgid "Yaml text" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:51 +msgid "YouTube" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:903 +msgid "Your %s was successfully uploaded. Size: %sB." +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:187 +#: htdocs/luci-static/resources/view/fchomo/node.js:279 +#: htdocs/luci-static/resources/view/fchomo/node.js:318 +msgid "aes-128-gcm" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:188 +msgid "aes-192-gcm" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:189 +#: htdocs/luci-static/resources/view/fchomo/node.js:280 +msgid "aes-256-gcm" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:315 +msgid "auto" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:222 +#: htdocs/luci-static/resources/view/fchomo/server.js:259 +msgid "bbr" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:348 +msgid "certificate" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:190 +#: htdocs/luci-static/resources/view/fchomo/node.js:281 +msgid "chacha20-ietf-poly1305" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:319 +msgid "chacha20-poly1305" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:220 +#: htdocs/luci-static/resources/view/fchomo/server.js:257 +msgid "cubic" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:968 +msgid "down" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:592 +#: htdocs/luci-static/resources/view/fchomo/node.js:603 +#: htdocs/luci-static/resources/view/fchomo/node.js:608 +msgid "gRPC" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:662 +msgid "gRPC service name" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:399 +msgid "gVisor" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:699 +msgid "h2mux" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:34 +msgid "metacubexd" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:765 +#: htdocs/luci-static/resources/view/fchomo/node.js:950 +msgid "mpTCP" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:221 +#: htdocs/luci-static/resources/view/fchomo/server.js:258 +msgid "new_reno" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:396 +msgid "no-resolve" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:740 +#: htdocs/luci-static/resources/fchomo.js:771 +msgid "non-empty value" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:185 +#: htdocs/luci-static/resources/view/fchomo/node.js:316 +#: htdocs/luci-static/resources/view/fchomo/node.js:336 +#: htdocs/luci-static/resources/view/fchomo/node.js:409 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:228 +msgid "none" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:92 +msgid "not found" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:479 +msgid "not included \",\"" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:112 +msgid "null" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:410 +msgid "obfs-simple" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:933 +msgid "override.proxy-name" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:337 +msgid "packet addr (v2ray-core v5+)" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:363 +msgid "private key" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:36 +msgid "razord-meta" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:413 +msgid "restls" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:412 +msgid "shadow-tls" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:697 +msgid "smux" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:379 +msgid "src" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:196 +msgid "unchecked" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:573 +msgid "unique UCI identifier" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:576 +msgid "unique identifier" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:780 +msgid "unique value" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:963 +msgid "up" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:197 +#: htdocs/luci-static/resources/view/fchomo/node.js:241 +#: htdocs/luci-static/resources/view/fchomo/node.js:438 +#: htdocs/luci-static/resources/view/fchomo/node.js:470 +msgid "v1" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:198 +#: htdocs/luci-static/resources/view/fchomo/node.js:439 +#: htdocs/luci-static/resources/view/fchomo/node.js:471 +msgid "v2" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:199 +#: htdocs/luci-static/resources/view/fchomo/node.js:440 +msgid "v3" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:714 +#: htdocs/luci-static/resources/fchomo.js:717 +msgid "valid JSON format" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:542 +msgid "valid SHA256 string with %d characters" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:792 +#: htdocs/luci-static/resources/fchomo.js:795 +msgid "valid URL" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:727 +msgid "valid base64 key with %d characters" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:742 +msgid "valid key length with %d characters" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:697 +msgid "valid port value" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:805 +msgid "valid uuid" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:191 +msgid "xchacha20-ietf-poly1305" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:35 +msgid "yacd-meta" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:698 +msgid "yamux" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:33 +msgid "zashboard" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:317 +msgid "zero" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:551 +msgid "🡇" +msgstr "" diff --git a/luci-app-fchomo/po/zh-cn b/luci-app-fchomo/po/zh-cn new file mode 120000 index 0000000000..8d69574ddd --- /dev/null +++ b/luci-app-fchomo/po/zh-cn @@ -0,0 +1 @@ +zh_Hans \ No newline at end of file diff --git a/luci-app-fchomo/po/zh_Hans/fchomo.po b/luci-app-fchomo/po/zh_Hans/fchomo.po new file mode 100644 index 0000000000..2b9c3c05ab --- /dev/null +++ b/luci-app-fchomo/po/zh_Hans/fchomo.po @@ -0,0 +1,2664 @@ +msgid "" +msgstr "" +"Content-Type: text/plain; charset=UTF-8\n" +"Project-Id-Version: PACKAGE VERSION\n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: zh_Hans\n" +"MIME-Version: 1.0\n" +"Content-Transfer-Encoding: 8bit\n" + +#: htdocs/luci-static/resources/view/fchomo/log.js:69 +msgid "%s log" +msgstr "%s 日志" + +#: htdocs/luci-static/resources/fchomo.js:431 +#: htdocs/luci-static/resources/fchomo.js:444 +#: htdocs/luci-static/resources/fchomo.js:457 +#: htdocs/luci-static/resources/fchomo.js:471 +#: htdocs/luci-static/resources/view/fchomo/client.js:289 +#: htdocs/luci-static/resources/view/fchomo/client.js:503 +#: htdocs/luci-static/resources/view/fchomo/client.js:517 +#: htdocs/luci-static/resources/view/fchomo/client.js:986 +#: htdocs/luci-static/resources/view/fchomo/global.js:470 +#: htdocs/luci-static/resources/view/fchomo/node.js:1167 +#: htdocs/luci-static/resources/view/fchomo/node.js:1168 +msgid "-- Please choose --" +msgstr "-- 请选择 --" + +#: htdocs/luci-static/resources/fchomo.js:48 +msgid "163Music" +msgstr "网抑云" + +#: htdocs/luci-static/resources/fchomo.js:193 +msgid "2022-blake3-aes-128-gcm" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:194 +msgid "2022-blake3-aes-256-gcm" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:195 +msgid "2022-blake3-chacha20-poly1305" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:344 +#: htdocs/luci-static/resources/view/fchomo/server.js:359 +msgid "Save your configuration before uploading files!" +msgstr "上传文件前请先保存配置!" + +#: htdocs/luci-static/resources/view/fchomo/global.js:458 +msgid "API" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:499 +msgid "API DoH service" +msgstr "API DoH 服务器" + +#: htdocs/luci-static/resources/view/fchomo/global.js:490 +msgid "API HTTP port" +msgstr "API HTTP 端口" + +#: htdocs/luci-static/resources/view/fchomo/global.js:494 +msgid "API HTTPS port" +msgstr "API HTTPS 端口" + +#: htdocs/luci-static/resources/view/fchomo/global.js:448 +msgid "API TLS certificate path" +msgstr "API TLS 证书路径" + +#: htdocs/luci-static/resources/view/fchomo/global.js:452 +msgid "API TLS private key path" +msgstr "API TLS 私钥" + +#: htdocs/luci-static/resources/view/fchomo/global.js:503 +msgid "API secret" +msgstr "API 令牌" + +#: htdocs/luci-static/resources/view/fchomo/global.js:273 +msgid "ASN version" +msgstr "ASN 版本" + +#: htdocs/luci-static/resources/view/fchomo/global.js:577 +#: htdocs/luci-static/resources/view/fchomo/global.js:627 +msgid "Access Control" +msgstr "访问控制" + +#: htdocs/luci-static/resources/view/fchomo/client.js:954 +msgid "Add a DNS policy" +msgstr "新增 DNS 策略" + +#: htdocs/luci-static/resources/view/fchomo/client.js:813 +msgid "Add a DNS server" +msgstr "新增 DNS 服务器" + +#: htdocs/luci-static/resources/view/fchomo/node.js:35 +msgid "Add a Node" +msgstr "新增 节点" + +#: htdocs/luci-static/resources/view/fchomo/node.js:806 +msgid "Add a provider" +msgstr "新增 供应商" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1077 +msgid "Add a proxy chain" +msgstr "新增 代理链" + +#: htdocs/luci-static/resources/view/fchomo/client.js:465 +msgid "Add a proxy group" +msgstr "新增 代理组" + +#: htdocs/luci-static/resources/view/fchomo/client.js:656 +msgid "Add a routing rule" +msgstr "新增 路由规则" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:112 +msgid "Add a rule set" +msgstr "新增 规则集" + +#: htdocs/luci-static/resources/view/fchomo/server.js:97 +msgid "Add a server" +msgstr "新增 服务器" + +#: htdocs/luci-static/resources/view/fchomo/client.js:726 +msgid "Add a sub rule" +msgstr "新增 子规则" + +#: htdocs/luci-static/resources/view/fchomo/node.js:924 +msgid "Add prefix" +msgstr "添加前缀" + +#: htdocs/luci-static/resources/view/fchomo/node.js:927 +msgid "Add suffix" +msgstr "添加后缀" + +#: htdocs/luci-static/resources/view/fchomo/client.js:827 +#: htdocs/luci-static/resources/view/fchomo/client.js:832 +msgid "Address" +msgstr "地址" + +#: htdocs/luci-static/resources/view/fchomo/global.js:630 +#: htdocs/luci-static/resources/view/fchomo/global.js:674 +msgid "All allowed" +msgstr "允许所有" + +#: htdocs/luci-static/resources/view/fchomo/global.js:660 +#: htdocs/luci-static/resources/view/fchomo/global.js:667 +msgid "All ports" +msgstr "所有端口" + +#: htdocs/luci-static/resources/view/fchomo/global.js:486 +msgid "" +"Allow access from private network.
To access the API on a private " +"network from a public website, it must be enabled." +msgstr "" +"允许从私有网络访问。
要从公共网站访问私有网络上的 API,则必须启用。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:378 +msgid "Allowed IPs" +msgstr "允许的 IP" + +#: htdocs/luci-static/resources/view/fchomo/global.js:58 +msgid "Already at the latest version." +msgstr "已是最新版本。" + +#: htdocs/luci-static/resources/view/fchomo/global.js:55 +msgid "Already in updating." +msgstr "已在更新中。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:307 +#: htdocs/luci-static/resources/view/fchomo/server.js:301 +msgid "Alter ID" +msgstr "额外 ID" + +#: htdocs/luci-static/resources/view/fchomo/global.js:144 +msgid "Application version" +msgstr "应用版本" + +#: htdocs/luci-static/resources/view/fchomo/server.js:276 +msgid "Auth timeout" +msgstr "认证超时" + +#: htdocs/luci-static/resources/view/fchomo/node.js:329 +msgid "Authenticated length" +msgstr "认证长度" + +#: htdocs/luci-static/resources/view/fchomo/global.js:304 +msgid "Auto" +msgstr "自动" + +#: htdocs/luci-static/resources/view/fchomo/server.js:87 +msgid "Auto configure firewall" +msgstr "自动配置防火墙" + +#: htdocs/luci-static/resources/view/fchomo/global.js:212 +msgid "Auto update" +msgstr "自动更新" + +#: htdocs/luci-static/resources/view/fchomo/global.js:213 +msgid "Auto update resources." +msgstr "自动更新资源文件。" + +#: htdocs/luci-static/resources/fchomo.js:47 +msgid "Baidu" +msgstr "百度" + +#: htdocs/luci-static/resources/view/fchomo/global.js:409 +msgid "Based on google/gvisor." +msgstr "基于 google/gvisor。" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:232 +msgid "Behavior" +msgstr "行为" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:212 +msgid "Binary file" +msgstr "二进制文件" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:218 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:241 +msgid "Binary format only supports domain / ipcidr" +msgstr "二进制格式仅支持 domain/ipcidr" + +#: htdocs/luci-static/resources/view/fchomo/client.js:550 +#: htdocs/luci-static/resources/view/fchomo/global.js:591 +#: htdocs/luci-static/resources/view/fchomo/node.js:775 +#: htdocs/luci-static/resources/view/fchomo/node.js:986 +msgid "Bind interface" +msgstr "绑定接口" + +#: htdocs/luci-static/resources/view/fchomo/client.js:551 +#: htdocs/luci-static/resources/view/fchomo/node.js:776 +#: htdocs/luci-static/resources/view/fchomo/node.js:987 +msgid "Bind outbound interface.
" +msgstr "绑定出站接口。
" + +#: htdocs/luci-static/resources/view/fchomo/global.js:592 +msgid "" +"Bind outbound traffic to specific interface. Leave empty to auto detect.
" +msgstr "绑定出站流量至指定接口。留空自动检测。
" + +#: htdocs/luci-static/resources/view/fchomo/global.js:632 +msgid "Black list" +msgstr "黑名单" + +#: htdocs/luci-static/resources/view/fchomo/client.js:16 +msgid "Block DNS queries" +msgstr "封锁 DNS 请求" + +#: htdocs/luci-static/resources/view/fchomo/client.js:764 +msgid "Boot DNS server" +msgstr "启动 DNS 服务器" + +#: htdocs/luci-static/resources/view/fchomo/client.js:771 +msgid "Boot DNS server (Node)" +msgstr "启动 DNS 服务器 (节点)" + +#: htdocs/luci-static/resources/view/fchomo/global.js:675 +msgid "Bypass CN" +msgstr "绕过 CN 流量" + +#: htdocs/luci-static/resources/view/fchomo/global.js:481 +msgid "CORS Allow origins" +msgstr "CORS 允许的来源" + +#: htdocs/luci-static/resources/view/fchomo/global.js:485 +msgid "CORS Allow private network" +msgstr "CORS 允许私有网络" + +#: htdocs/luci-static/resources/view/fchomo/global.js:482 +msgid "CORS allowed origins, * will be used if empty." +msgstr "CORS 允许的来源,留空则使用 *。" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:131 +msgid "Cancel" +msgstr "取消" + +#: htdocs/luci-static/resources/view/fchomo/node.js:536 +msgid "Cert fingerprint" +msgstr "证书指纹" + +#: htdocs/luci-static/resources/view/fchomo/node.js:537 +msgid "" +"Certificate fingerprint. Used to implement SSL Pinning and prevent MitM." +msgstr "证书指纹。用于实现 SSL证书固定 并防止 MitM。" + +#: htdocs/luci-static/resources/view/fchomo/server.js:336 +msgid "Certificate path" +msgstr "证书路径" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1119 +#: htdocs/luci-static/resources/view/fchomo/node.js:1125 +msgid "Chain head" +msgstr "链头" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1166 +#: htdocs/luci-static/resources/view/fchomo/node.js:1173 +msgid "Chain tail" +msgstr "链尾" + +#: htdocs/luci-static/resources/view/fchomo/global.js:194 +msgid "Check" +msgstr "检查" + +#: htdocs/luci-static/resources/view/fchomo/global.js:75 +msgid "Check update" +msgstr "检查更新" + +#: htdocs/luci-static/resources/view/fchomo/global.js:276 +msgid "China IPv4 list version" +msgstr "大陆 IPv4 库版本" + +#: htdocs/luci-static/resources/view/fchomo/global.js:279 +msgid "China IPv6 list version" +msgstr "大陆 IPv6 库版本" + +#: htdocs/luci-static/resources/view/fchomo/global.js:285 +msgid "China list version" +msgstr "大陆域名列表版本" + +#: htdocs/luci-static/resources/view/fchomo/node.js:150 +#: htdocs/luci-static/resources/view/fchomo/node.js:313 +#: htdocs/luci-static/resources/view/fchomo/server.js:208 +msgid "Chipher" +msgstr "加密方法" + +#: htdocs/luci-static/resources/view/fchomo/log.js:76 +msgid "Clean log" +msgstr "清空日志" + +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:22 +msgid "Client" +msgstr "客户端" + +#: htdocs/luci-static/resources/view/fchomo/node.js:558 +msgid "Client fingerprint" +msgstr "客户端指纹" + +#: htdocs/luci-static/resources/view/fchomo/global.js:149 +msgid "Client status" +msgstr "客户端状态" + +#: htdocs/luci-static/resources/view/fchomo/log.js:39 +msgid "Collecting data..." +msgstr "收集数据中..." + +#: htdocs/luci-static/resources/view/fchomo/global.js:662 +#: htdocs/luci-static/resources/view/fchomo/global.js:669 +msgid "Common and STUN ports" +msgstr "常用端口和 STUN 端口" + +#: htdocs/luci-static/resources/view/fchomo/global.js:661 +#: htdocs/luci-static/resources/view/fchomo/global.js:668 +msgid "Common ports only (bypass P2P traffic)" +msgstr "仅常用端口(绕过 P2P 流量)" + +#: htdocs/luci-static/resources/fchomo.js:640 +msgid "Complete" +msgstr "完成" + +#: htdocs/luci-static/resources/view/fchomo/node.js:941 +msgid "Configuration Items" +msgstr "配置项" + +#: htdocs/luci-static/resources/view/fchomo/node.js:217 +#: htdocs/luci-static/resources/view/fchomo/server.js:254 +msgid "Congestion controller" +msgstr "拥塞控制器" + +#: htdocs/luci-static/resources/view/fchomo/global.js:170 +msgid "Connection check" +msgstr "连接检查" + +#: htdocs/luci-static/resources/view/fchomo/node.js:867 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:273 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:294 +msgid "Content will not be verified, Please make sure you enter it correctly." +msgstr "内容将不会被验证,请确保输入正确。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:859 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:265 +msgid "Contents" +msgstr "内容" + +#: htdocs/luci-static/resources/view/fchomo/hosts.js:19 +msgid "Contents have been saved." +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:139 +msgid "Core version" +msgstr "核心版本" + +#: htdocs/luci-static/resources/view/fchomo/global.js:225 +msgid "Cron expression" +msgstr "Cron 表达式" + +#: htdocs/luci-static/resources/view/fchomo/global.js:691 +msgid "Custom Direct List" +msgstr "自定义直连列表" + +#: htdocs/luci-static/resources/view/fchomo/node.js:908 +msgid "Custom HTTP header." +msgstr "自定义 HTTP header。" + +#: htdocs/luci-static/resources/view/fchomo/global.js:716 +msgid "Custom Proxy List" +msgstr "自定义代理列表" + +#: htdocs/luci-static/resources/view/fchomo/hosts.js:28 +msgid "" +"Custom internal hosts. Support yaml or json format." +msgstr "自定义内部 hosts。支持 yamljson 格式。" + +#: htdocs/luci-static/resources/fchomo.js:86 +msgid "DIRECT" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:944 +#: htdocs/luci-static/resources/view/fchomo/client.js:954 +msgid "DNS policy" +msgstr "DNS 策略" + +#: htdocs/luci-static/resources/view/fchomo/global.js:374 +msgid "DNS port" +msgstr " DNS 端口" + +#: htdocs/luci-static/resources/view/fchomo/client.js:803 +#: htdocs/luci-static/resources/view/fchomo/client.js:813 +#: htdocs/luci-static/resources/view/fchomo/client.js:999 +#: htdocs/luci-static/resources/view/fchomo/node.js:402 +msgid "DNS server" +msgstr "DNS 服务器" + +#: htdocs/luci-static/resources/view/fchomo/client.js:750 +msgid "DNS settings" +msgstr "DNS 设置" + +#: htdocs/luci-static/resources/view/fchomo/global.js:243 +msgid "Dashboard version" +msgstr "面板版本" + +#: htdocs/luci-static/resources/view/fchomo/global.js:313 +msgid "Debug" +msgstr "调试" + +#: htdocs/luci-static/resources/view/fchomo/node.js:228 +msgid "Default" +msgstr "默认" + +#: htdocs/luci-static/resources/view/fchomo/client.js:14 +msgid "Default DNS (issued by WAN)" +msgstr "默认 DNS(由 WAN 下发)" + +#: htdocs/luci-static/resources/view/fchomo/client.js:778 +msgid "Default DNS server" +msgstr "默认 DNS 服务器" + +#: htdocs/luci-static/resources/view/fchomo/node.js:379 +msgid "Destination addresses allowed to be forwarded via Wireguard." +msgstr "允许通过 WireGuard 转发的目的地址" + +#: htdocs/luci-static/resources/view/fchomo/node.js:44 +msgid "Dial fields" +msgstr "拨号字段" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1134 +#: htdocs/luci-static/resources/view/fchomo/node.js:1182 +msgid "Different chain head/tail" +msgstr "不同的链头/链尾" + +#: htdocs/luci-static/resources/view/fchomo/global.js:297 +msgid "Direct" +msgstr "直连" + +#: htdocs/luci-static/resources/view/fchomo/global.js:634 +msgid "Direct IPv4 IP-s" +msgstr "直连 IPv4 地址" + +#: htdocs/luci-static/resources/view/fchomo/global.js:637 +msgid "Direct IPv6 IP-s" +msgstr "直连 IPv6 地址" + +#: htdocs/luci-static/resources/view/fchomo/global.js:640 +msgid "Direct MAC-s" +msgstr "直连 MAC 地址" + +#: htdocs/luci-static/resources/view/fchomo/global.js:305 +#: htdocs/luci-static/resources/view/fchomo/node.js:116 +#: htdocs/luci-static/resources/view/fchomo/server.js:177 +msgid "Disable" +msgstr "禁用" + +#: htdocs/luci-static/resources/view/fchomo/global.js:568 +msgid "Disable ECN of quic-go" +msgstr "禁用 quic-go 的 显式拥塞通知(ECN)" + +#: htdocs/luci-static/resources/view/fchomo/global.js:565 +msgid "Disable GSO of quic-go" +msgstr "禁用 quic-go 的 通用分段卸载(GSO)" + +#: htdocs/luci-static/resources/view/fchomo/node.js:519 +msgid "Disable SNI" +msgstr "禁用 SNI" + +#: htdocs/luci-static/resources/view/fchomo/client.js:546 +msgid "Disable UDP" +msgstr "禁用 UDP" + +#: htdocs/luci-static/resources/view/fchomo/client.js:969 +#: htdocs/luci-static/resources/view/fchomo/client.js:974 +#: htdocs/luci-static/resources/view/fchomo/client.js:1039 +#: htdocs/luci-static/resources/view/fchomo/client.js:1046 +#: htdocs/luci-static/resources/view/fchomo/client.js:1048 +msgid "Domain" +msgstr "域名" + +#: htdocs/luci-static/resources/view/fchomo/node.js:520 +msgid "Donot send server name in ClientHello." +msgstr "不要在 ClientHello 中发送服务器名称。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:550 +#: htdocs/luci-static/resources/view/fchomo/node.js:974 +msgid "Donot verifying server certificate." +msgstr "不验证服务器证书。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:754 +msgid "Download bandwidth" +msgstr "下载带宽" + +#: htdocs/luci-static/resources/view/fchomo/node.js:755 +msgid "Download bandwidth in Mbps." +msgstr "下载带宽(单位:Mbps)。" + +#: htdocs/luci-static/resources/fchomo.js:546 +msgid "Download failed: %s" +msgstr "下载失败: %s" + +#: htdocs/luci-static/resources/fchomo.js:544 +msgid "Download successful." +msgstr "下载成功。" + +#: htdocs/luci-static/resources/fchomo.js:72 +msgid "Dual stack" +msgstr "双栈" + +#: htdocs/luci-static/resources/view/fchomo/client.js:924 +msgid "ECS override" +msgstr "强制覆盖 ECS" + +#: htdocs/luci-static/resources/view/fchomo/client.js:908 +msgid "EDNS Client Subnet" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:316 +msgid "ETag support" +msgstr "ETag 支持" + +#: htdocs/luci-static/resources/view/fchomo/node.js:667 +msgid "Early Data first packet length limit." +msgstr "前置数据长度阈值" + +#: htdocs/luci-static/resources/view/fchomo/node.js:673 +msgid "Early Data header name" +msgstr "前置数据标头" + +#: htdocs/luci-static/resources/view/fchomo/node.js:20 +msgid "Edit node" +msgstr "编辑节点" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:102 +msgid "Edit ruleset" +msgstr "编辑规则集" + +#: htdocs/luci-static/resources/view/fchomo/node.js:857 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:263 +msgid "Editer" +msgstr "编辑器" + +#: htdocs/luci-static/resources/view/fchomo/client.js:454 +#: htdocs/luci-static/resources/view/fchomo/client.js:485 +#: htdocs/luci-static/resources/view/fchomo/client.js:666 +#: htdocs/luci-static/resources/view/fchomo/client.js:736 +#: htdocs/luci-static/resources/view/fchomo/client.js:823 +#: htdocs/luci-static/resources/view/fchomo/client.js:964 +#: htdocs/luci-static/resources/view/fchomo/global.js:303 +#: htdocs/luci-static/resources/view/fchomo/global.js:540 +#: htdocs/luci-static/resources/view/fchomo/node.js:51 +#: htdocs/luci-static/resources/view/fchomo/node.js:833 +#: htdocs/luci-static/resources/view/fchomo/node.js:1006 +#: htdocs/luci-static/resources/view/fchomo/node.js:1087 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:199 +#: htdocs/luci-static/resources/view/fchomo/server.js:84 +#: htdocs/luci-static/resources/view/fchomo/server.js:108 +msgid "Enable" +msgstr "启用" + +#: htdocs/luci-static/resources/view/fchomo/node.js:252 +msgid "" +"Enable 0-RTT QUIC connection handshake on the client side. This is not " +"impacting much on the performance, as the protocol is fully multiplexed.
Disabling this is highly recommended, as it is vulnerable to replay attacks." +msgstr "" +"在客户端启用 0-RTT QUIC 连接握手。由于协议是完全复用的,这对性能影响不大。" +"
强烈建议禁用此功能,因为它容易受到重放攻击。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:251 +msgid "Enable 0-RTT handshake" +msgstr "启用 0-RTT 握手" + +#: htdocs/luci-static/resources/view/fchomo/global.js:571 +msgid "" +"Enable
IP4P " +"conversion for outbound connections" +msgstr "" +"为出站连接启用 IP4P 转换" + +#: htdocs/luci-static/resources/view/fchomo/node.js:742 +msgid "Enable TCP Brutal" +msgstr "启用 TCP Brutal" + +#: htdocs/luci-static/resources/view/fchomo/node.js:743 +msgid "Enable TCP Brutal congestion control algorithm" +msgstr "启用 TCP Brutal 拥塞控制算法。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:731 +msgid "Enable multiplexing only for TCP." +msgstr "仅为 TCP 启用多路复用。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:725 +msgid "Enable padding" +msgstr "启用填充" + +#: htdocs/luci-static/resources/view/fchomo/node.js:736 +msgid "Enable statistic" +msgstr "启用统计" + +#: htdocs/luci-static/resources/view/fchomo/node.js:464 +#: htdocs/luci-static/resources/view/fchomo/node.js:959 +msgid "" +"Enable the SUoT protocol, requires server support. Conflict with Multiplex." +msgstr "启用 SUoT 协议,需要服务端支持。与多路复用冲突。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:122 +#: htdocs/luci-static/resources/view/fchomo/server.js:183 +msgid "" +"Enabling obfuscation will make the server incompatible with standard QUIC " +"connections, losing the ability to masquerade with HTTP/3." +msgstr "启用混淆将使服务器与标准的 QUIC 连接不兼容,失去 HTTP/3 伪装的能力。" + +#: htdocs/luci-static/resources/view/fchomo/global.js:430 +msgid "Endpoint-Independent NAT" +msgstr "端点独立 NAT" + +#: htdocs/luci-static/resources/view/fchomo/client.js:305 +#: htdocs/luci-static/resources/view/fchomo/client.js:991 +msgid "Entry" +msgstr "条目" + +#: htdocs/luci-static/resources/view/fchomo/global.js:310 +msgid "Error" +msgstr "错误" + +#: htdocs/luci-static/resources/view/fchomo/client.js:602 +msgid "" +"Exceeding this triggers a forced health check. 5 will be used " +"if empty." +msgstr "超过此限制将会触发强制健康检查。留空则使用 5。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1056 +msgid "Exclude matched node types." +msgstr "排除匹配的节点类型。" + +#: htdocs/luci-static/resources/view/fchomo/client.js:639 +msgid "" +"Exclude matched node types. Available types see here." +msgstr "" +"排除匹配的节点类型。可用类型请参见此处。" + +#: htdocs/luci-static/resources/view/fchomo/client.js:634 +#: htdocs/luci-static/resources/view/fchomo/node.js:1050 +msgid "Exclude nodes that meet keywords or regexps." +msgstr "排除匹配关键词或表达式的节点。" + +#: htdocs/luci-static/resources/view/fchomo/client.js:594 +#: htdocs/luci-static/resources/view/fchomo/node.js:1037 +msgid "Expected HTTP code. 204 will be used if empty." +msgstr "预期的 HTTP code。留空则使用 204。" + +#: htdocs/luci-static/resources/view/fchomo/client.js:596 +#: htdocs/luci-static/resources/view/fchomo/node.js:1039 +msgid "Expected status" +msgstr "预期状态" + +#: htdocs/luci-static/resources/fchomo.js:570 +#: htdocs/luci-static/resources/fchomo.js:573 +#: htdocs/luci-static/resources/fchomo.js:576 +#: htdocs/luci-static/resources/fchomo.js:657 +#: htdocs/luci-static/resources/fchomo.js:665 +#: htdocs/luci-static/resources/fchomo.js:673 +#: htdocs/luci-static/resources/fchomo.js:697 +#: htdocs/luci-static/resources/fchomo.js:714 +#: htdocs/luci-static/resources/fchomo.js:717 +#: htdocs/luci-static/resources/fchomo.js:727 +#: htdocs/luci-static/resources/fchomo.js:740 +#: htdocs/luci-static/resources/fchomo.js:742 +#: htdocs/luci-static/resources/fchomo.js:755 +#: htdocs/luci-static/resources/fchomo.js:764 +#: htdocs/luci-static/resources/fchomo.js:771 +#: htdocs/luci-static/resources/fchomo.js:780 +#: htdocs/luci-static/resources/fchomo.js:792 +#: htdocs/luci-static/resources/fchomo.js:795 +#: htdocs/luci-static/resources/fchomo.js:805 +#: htdocs/luci-static/resources/view/fchomo/client.js:27 +#: htdocs/luci-static/resources/view/fchomo/client.js:479 +#: htdocs/luci-static/resources/view/fchomo/client.js:838 +#: htdocs/luci-static/resources/view/fchomo/node.js:542 +#: htdocs/luci-static/resources/view/fchomo/node.js:1134 +#: htdocs/luci-static/resources/view/fchomo/node.js:1182 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:218 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:241 +msgid "Expecting: %s" +msgstr "请输入:%s" + +#: htdocs/luci-static/resources/view/fchomo/node.js:601 +#: htdocs/luci-static/resources/view/fchomo/node.js:608 +msgid "Expecting: only support %s." +msgstr "请输入:仅支援 %s." + +#: htdocs/luci-static/resources/view/fchomo/global.js:559 +msgid "Experimental" +msgstr "实验性" + +#: htdocs/luci-static/resources/view/fchomo/client.js:243 +#: htdocs/luci-static/resources/view/fchomo/client.js:256 +#: htdocs/luci-static/resources/view/fchomo/client.js:266 +#: htdocs/luci-static/resources/view/fchomo/client.js:273 +#: htdocs/luci-static/resources/view/fchomo/client.js:281 +#: htdocs/luci-static/resources/view/fchomo/client.js:288 +msgid "Factor" +msgstr "条件" + +#: htdocs/luci-static/resources/fchomo.js:598 +msgid "Failed to execute \"/etc/init.d/fchomo %s %s\" reason: %s" +msgstr "无法执行 \"/etc/init.d/fchomo %s %s\" 原因: %s" + +#: htdocs/luci-static/resources/fchomo.js:905 +msgid "Failed to upload %s, error: %s." +msgstr "上传 %s 失败,错误:%s。" + +#: htdocs/luci-static/resources/fchomo.js:924 +msgid "Failed to upload, error: %s." +msgstr "上传失败,错误:%s。" + +#: htdocs/luci-static/resources/fchomo.js:123 +msgid "Fallback" +msgstr "自动回退" + +#: htdocs/luci-static/resources/view/fchomo/client.js:785 +#: htdocs/luci-static/resources/view/fchomo/client.js:786 +#: htdocs/luci-static/resources/view/fchomo/client.js:797 +msgid "Fallback DNS server" +msgstr "後備 DNS 服务器" + +#: htdocs/luci-static/resources/view/fchomo/client.js:1018 +msgid "Fallback filter" +msgstr "後備过滤器" + +#: htdocs/luci-static/resources/view/fchomo/client.js:629 +#: htdocs/luci-static/resources/view/fchomo/node.js:1045 +msgid "Filter nodes that meet keywords or regexps." +msgstr "过滤匹配关键字或表达式的节点。" + +#: htdocs/luci-static/resources/view/fchomo/client.js:779 +#: htdocs/luci-static/resources/view/fchomo/client.js:796 +msgid "Final DNS server" +msgstr "兜底 DNS 服务器" + +#: htdocs/luci-static/resources/view/fchomo/client.js:779 +#: htdocs/luci-static/resources/view/fchomo/client.js:793 +msgid "Final DNS server (Used to Domestic-IP response)" +msgstr "兜底 DNS 服务器 (用于 境内-IP 响应)" + +#: htdocs/luci-static/resources/view/fchomo/client.js:786 +#: htdocs/luci-static/resources/view/fchomo/client.js:794 +msgid "Final DNS server (Used to Overseas-IP response)" +msgstr "兜底 DNS 服务器 (用于 境外-IP 响应)" + +#: htdocs/luci-static/resources/view/fchomo/node.js:301 +msgid "Flow" +msgstr "流控" + +#: htdocs/luci-static/resources/view/fchomo/client.js:618 +msgid "" +"For details, see %s." +msgstr "" +"实现细节请参阅 %s." + +#: htdocs/luci-static/resources/view/fchomo/client.js:595 +#: htdocs/luci-static/resources/view/fchomo/node.js:932 +#: htdocs/luci-static/resources/view/fchomo/node.js:1038 +msgid "" +"For format see %s." +msgstr "" +"格式请参阅 %s." + +#: htdocs/luci-static/resources/view/fchomo/node.js:397 +msgid "Force DNS remote resolution." +msgstr "强制 DNS 远程解析。" + +#: htdocs/luci-static/resources/view/fchomo/global.js:519 +msgid "Forced sniffing domain" +msgstr "强制嗅探域名" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:209 +msgid "Format" +msgstr "格式" + +#: htdocs/luci-static/resources/view/fchomo/global.js:127 +#: htdocs/luci-static/resources/view/fchomo/log.js:97 +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:3 +msgid "FullCombo Mihomo" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:627 +msgid "GET" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:282 +msgid "GFW list version" +msgstr "GFW 域名列表版本" + +#: htdocs/luci-static/resources/view/fchomo/global.js:290 +msgid "General" +msgstr "常规" + +#: htdocs/luci-static/resources/view/fchomo/client.js:470 +#: htdocs/luci-static/resources/view/fchomo/node.js:40 +#: htdocs/luci-static/resources/view/fchomo/node.js:823 +msgid "General fields" +msgstr "常规字段" + +#: htdocs/luci-static/resources/view/fchomo/global.js:293 +msgid "General settings" +msgstr "常规设置" + +#: htdocs/luci-static/resources/view/fchomo/server.js:145 +#: htdocs/luci-static/resources/view/fchomo/server.js:147 +#: htdocs/luci-static/resources/view/fchomo/server.js:190 +#: htdocs/luci-static/resources/view/fchomo/server.js:192 +#: htdocs/luci-static/resources/view/fchomo/server.js:223 +#: htdocs/luci-static/resources/view/fchomo/server.js:225 +#: htdocs/luci-static/resources/view/fchomo/server.js:243 +#: htdocs/luci-static/resources/view/fchomo/server.js:245 +#: htdocs/luci-static/resources/view/fchomo/server.js:290 +#: htdocs/luci-static/resources/view/fchomo/server.js:292 +msgid "Generate" +msgstr "生成" + +#: htdocs/luci-static/resources/view/fchomo/global.js:418 +msgid "Generic segmentation offload" +msgstr "通用分段卸载(GSO)" + +#: htdocs/luci-static/resources/view/fchomo/global.js:267 +msgid "GeoIP version" +msgstr "GeoIP 版本" + +#: htdocs/luci-static/resources/view/fchomo/global.js:270 +msgid "GeoSite version" +msgstr "GeoSite 版本" + +#: htdocs/luci-static/resources/view/fchomo/client.js:1028 +msgid "Geoip code" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:1025 +msgid "Geoip enable" +msgstr "Geoip 启用" + +#: htdocs/luci-static/resources/view/fchomo/client.js:970 +#: htdocs/luci-static/resources/view/fchomo/client.js:979 +#: htdocs/luci-static/resources/view/fchomo/client.js:1037 +msgid "Geosite" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:50 +msgid "GitHub" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:299 +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:14 +msgid "Global" +msgstr "全局" + +#: htdocs/luci-static/resources/view/fchomo/global.js:339 +msgid "Global Authentication" +msgstr "全局认证" + +#: htdocs/luci-static/resources/view/fchomo/global.js:442 +msgid "Global client fingerprint" +msgstr "全局客户端指纹" + +#: htdocs/luci-static/resources/view/fchomo/node.js:323 +msgid "Global padding" +msgstr "全局填充" + +#: htdocs/luci-static/resources/fchomo.js:49 +msgid "Google" +msgstr "谷歌" + +#: root/usr/share/rpcd/acl.d/luci-app-fchomo.json:3 +msgid "Grant access to fchomo configuration" +msgstr "授予 fchomo 访问 UCI 配置的权限" + +#: htdocs/luci-static/resources/view/fchomo/client.js:495 +msgid "Group" +msgstr "组" + +#: htdocs/luci-static/resources/fchomo.js:60 +#: htdocs/luci-static/resources/fchomo.js:87 +#: htdocs/luci-static/resources/view/fchomo/node.js:418 +#: htdocs/luci-static/resources/view/fchomo/node.js:590 +#: htdocs/luci-static/resources/view/fchomo/node.js:601 +msgid "HTTP" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:84 +#: htdocs/luci-static/resources/view/fchomo/node.js:649 +#: htdocs/luci-static/resources/view/fchomo/node.js:907 +msgid "HTTP header" +msgstr "HTTP header" + +#: htdocs/luci-static/resources/view/fchomo/node.js:626 +msgid "HTTP request method" +msgstr "HTTP 请求方法" + +#: htdocs/luci-static/resources/view/fchomo/client.js:892 +msgid "HTTP/3" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:202 +msgid "" +"HTTP3 server behavior when authentication fails.
A 404 page will be " +"returned if empty." +msgstr "身份验证失败时的 HTTP3 服务器响应。默认返回 404 页面。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:591 +#: htdocs/luci-static/resources/view/fchomo/node.js:602 +msgid "HTTPUpgrade" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:678 +msgid "Handle domain" +msgstr "处理域名" + +#: htdocs/luci-static/resources/view/fchomo/client.js:564 +#: htdocs/luci-static/resources/view/fchomo/node.js:1010 +msgid "Health check URL" +msgstr "健康检查 URL" + +#: htdocs/luci-static/resources/view/fchomo/client.js:593 +#: htdocs/luci-static/resources/view/fchomo/node.js:1036 +msgid "Health check expected status" +msgstr "健康检查预期状态" + +#: htdocs/luci-static/resources/view/fchomo/client.js:573 +#: htdocs/luci-static/resources/view/fchomo/node.js:1019 +msgid "Health check interval" +msgstr "健康检查间隔" + +#: htdocs/luci-static/resources/view/fchomo/client.js:580 +#: htdocs/luci-static/resources/view/fchomo/node.js:1025 +msgid "Health check timeout" +msgstr "健康检查超时" + +#: htdocs/luci-static/resources/view/fchomo/client.js:472 +#: htdocs/luci-static/resources/view/fchomo/node.js:825 +msgid "Health fields" +msgstr "健康字段" + +#: htdocs/luci-static/resources/view/fchomo/node.js:258 +msgid "Heartbeat interval" +msgstr "心跳间隔" + +#: htdocs/luci-static/resources/view/fchomo/node.js:424 +msgid "Host that supports TLS 1.3" +msgstr "主机名称 (支援 TLS 1.3)" + +#: htdocs/luci-static/resources/view/fchomo/node.js:144 +msgid "Host-key" +msgstr "主机密钥" + +#: htdocs/luci-static/resources/view/fchomo/node.js:139 +msgid "Host-key algorithms" +msgstr "主机密钥算法" + +#: htdocs/luci-static/resources/view/fchomo/hosts.js:27 +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:30 +msgid "Hosts" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:66 +#: htdocs/luci-static/resources/fchomo.js:97 +msgid "Hysteria2" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:1030 +#: htdocs/luci-static/resources/view/fchomo/client.js:1043 +msgid "IP" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:1041 +msgid "IP CIDR" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:211 +msgid "IP override" +msgstr "IP 覆写" + +#: htdocs/luci-static/resources/view/fchomo/node.js:787 +#: htdocs/luci-static/resources/view/fchomo/node.js:998 +msgid "IP version" +msgstr "IP 版本" + +#: htdocs/luci-static/resources/fchomo.js:73 +msgid "IPv4 only" +msgstr "仅 IPv4" + +#: htdocs/luci-static/resources/fchomo.js:74 +msgid "IPv6 only" +msgstr "仅 IPv6" + +#: htdocs/luci-static/resources/view/fchomo/client.js:761 +#: htdocs/luci-static/resources/view/fchomo/global.js:319 +msgid "IPv6 support" +msgstr "IPv6 支持" + +#: htdocs/luci-static/resources/view/fchomo/server.js:269 +msgid "Idle timeout" +msgstr "空闲超时" + +#: htdocs/luci-static/resources/view/fchomo/client.js:27 +msgid "If Block is selected, uncheck others" +msgstr "如果选择了“阻止”,则取消选中“其他”" + +#: htdocs/luci-static/resources/view/fchomo/server.js:170 +msgid "Ignore client bandwidth" +msgstr "忽略客户端带宽" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:170 +msgid "Import" +msgstr "导入" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:121 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:179 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:181 +msgid "Import rule-set links" +msgstr "导入 rule-set 链接" + +#: htdocs/luci-static/resources/view/fchomo/node.js:104 +#: htdocs/luci-static/resources/view/fchomo/node.js:110 +#: htdocs/luci-static/resources/view/fchomo/node.js:964 +#: htdocs/luci-static/resources/view/fchomo/node.js:969 +#: htdocs/luci-static/resources/view/fchomo/server.js:159 +#: htdocs/luci-static/resources/view/fchomo/server.js:165 +msgid "In Mbps." +msgstr "单位为 Mbps。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:885 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:306 +msgid "In bytes. %s will be used if empty." +msgstr "单位为字节。留空则使用 %s。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:259 +#: htdocs/luci-static/resources/view/fchomo/node.js:266 +msgid "In millisecond." +msgstr "单位为毫秒。" + +#: htdocs/luci-static/resources/view/fchomo/client.js:581 +#: htdocs/luci-static/resources/view/fchomo/client.js:610 +#: htdocs/luci-static/resources/view/fchomo/node.js:1026 +msgid "In millisecond. %s will be used if empty." +msgstr "单位为毫秒。留空则使用 %s。" + +#: htdocs/luci-static/resources/view/fchomo/server.js:270 +#: htdocs/luci-static/resources/view/fchomo/server.js:277 +msgid "In seconds." +msgstr "单位为秒。" + +#: htdocs/luci-static/resources/view/fchomo/client.js:574 +#: htdocs/luci-static/resources/view/fchomo/global.js:329 +#: htdocs/luci-static/resources/view/fchomo/global.js:334 +#: htdocs/luci-static/resources/view/fchomo/global.js:426 +#: htdocs/luci-static/resources/view/fchomo/node.js:891 +#: htdocs/luci-static/resources/view/fchomo/node.js:1020 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:312 +msgid "In seconds. %s will be used if empty." +msgstr "单位为秒。留空则使用 %s。" + +#: htdocs/luci-static/resources/view/fchomo/global.js:353 +msgid "Inbound" +msgstr "入站" + +#: htdocs/luci-static/resources/view/fchomo/client.js:530 +msgid "Include all" +msgstr "引入所有" + +#: htdocs/luci-static/resources/view/fchomo/client.js:535 +msgid "Include all node" +msgstr "引入所有节点" + +#: htdocs/luci-static/resources/view/fchomo/client.js:540 +msgid "Include all provider" +msgstr "引入所有供应商" + +#: htdocs/luci-static/resources/view/fchomo/client.js:541 +msgid "Includes all Provider." +msgstr "引入所有供应商。" + +#: htdocs/luci-static/resources/view/fchomo/client.js:531 +msgid "Includes all Proxy Node and Provider." +msgstr "引入所有代理节点及供应商。" + +#: htdocs/luci-static/resources/view/fchomo/client.js:536 +msgid "Includes all Proxy Node." +msgstr "引入所有代理节点。" + +#: htdocs/luci-static/resources/view/fchomo/global.js:312 +msgid "Info" +msgstr "信息" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:206 +msgid "Inline" +msgstr "内嵌" + +#: htdocs/luci-static/resources/view/fchomo/global.js:584 +msgid "Interface Control" +msgstr "接口控制" + +#: htdocs/luci-static/resources/fchomo.js:71 +msgid "Keep default" +msgstr "保持缺省" + +#: htdocs/luci-static/resources/view/fchomo/server.js:351 +msgid "Key path" +msgstr "证书路径" + +#: htdocs/luci-static/resources/view/fchomo/client.js:475 +#: htdocs/luci-static/resources/view/fchomo/client.js:661 +#: htdocs/luci-static/resources/view/fchomo/client.js:731 +#: htdocs/luci-static/resources/view/fchomo/client.js:818 +#: htdocs/luci-static/resources/view/fchomo/client.js:959 +#: htdocs/luci-static/resources/view/fchomo/node.js:46 +#: htdocs/luci-static/resources/view/fchomo/node.js:828 +#: htdocs/luci-static/resources/view/fchomo/node.js:1082 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:194 +#: htdocs/luci-static/resources/view/fchomo/server.js:103 +msgid "Label" +msgstr "标签" + +#: htdocs/luci-static/resources/view/fchomo/client.js:587 +#: htdocs/luci-static/resources/view/fchomo/node.js:1031 +msgid "Lazy" +msgstr "懒惰状态" + +#: htdocs/luci-static/resources/view/fchomo/server.js:302 +msgid "" +"Legacy protocol support (VMess MD5 Authentication) is provided for " +"compatibility purposes only, use of alterId > 1 is not recommended." +msgstr "" +"提供旧协议支持(VMess MD5 身份验证)仅出于兼容性目的,不建议使用 alterId > " +"1。" + +#: htdocs/luci-static/resources/view/fchomo/global.js:411 +msgid "Less compatibility and sometimes better performance." +msgstr "有时性能更好。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:532 +#: htdocs/luci-static/resources/view/fchomo/server.js:332 +msgid "List of supported application level protocols, in order of preference." +msgstr "支持的应用层协议协商列表,按顺序排列。" + +#: htdocs/luci-static/resources/view/fchomo/server.js:118 +msgid "Listen address" +msgstr "监听地址" + +#: htdocs/luci-static/resources/view/fchomo/global.js:586 +msgid "Listen interfaces" +msgstr "监听接口" + +#: htdocs/luci-static/resources/view/fchomo/client.js:756 +#: htdocs/luci-static/resources/view/fchomo/server.js:123 +msgid "Listen port" +msgstr "监听端口" + +#: htdocs/luci-static/resources/view/fchomo/global.js:356 +msgid "Listen ports" +msgstr "监听端口" + +#: htdocs/luci-static/resources/fchomo.js:125 +msgid "Load balance" +msgstr "负载均衡" + +#: htdocs/luci-static/resources/view/fchomo/node.js:838 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:204 +msgid "Local" +msgstr "本地" + +#: htdocs/luci-static/resources/view/fchomo/node.js:350 +msgid "Local IPv6 address" +msgstr "本地 IPv6 地址" + +#: htdocs/luci-static/resources/view/fchomo/node.js:343 +msgid "Local address" +msgstr "本地地址" + +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:62 +msgid "Log" +msgstr "日志" + +#: htdocs/luci-static/resources/view/fchomo/log.js:54 +msgid "Log file does not exist." +msgstr "日志文件不存在。" + +#: htdocs/luci-static/resources/view/fchomo/log.js:47 +msgid "Log is empty." +msgstr "日志为空。" + +#: htdocs/luci-static/resources/view/fchomo/global.js:308 +msgid "Log level" +msgstr "日志等级" + +#: htdocs/luci-static/resources/fchomo.js:570 +msgid "Lowercase only" +msgstr "仅限小写" + +#: htdocs/luci-static/resources/view/fchomo/global.js:414 +#: htdocs/luci-static/resources/view/fchomo/node.js:390 +msgid "MTU" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:201 +msgid "Masquerade" +msgstr "伪装" + +#: htdocs/luci-static/resources/view/fchomo/client.js:975 +msgid "Match domain. Support wildcards." +msgstr "匹配域名。支持通配符。" + +#: htdocs/luci-static/resources/view/fchomo/client.js:1047 +msgid "Match domain. Support wildcards.
" +msgstr "匹配域名。支持通配符。
" + +#: htdocs/luci-static/resources/view/fchomo/client.js:980 +msgid "Match geosite." +msgstr "匹配 geosite。" + +#: htdocs/luci-static/resources/view/fchomo/client.js:1038 +msgid "Match geosite.
" +msgstr "匹配 geosite。
" + +#: htdocs/luci-static/resources/view/fchomo/client.js:1029 +msgid "Match response with geoip.
" +msgstr "匹配响应通过 geoip。
" + +#: htdocs/luci-static/resources/view/fchomo/client.js:1042 +msgid "Match response with ipcidr.
" +msgstr "匹配响应通过 ipcidr
" + +#: htdocs/luci-static/resources/view/fchomo/client.js:985 +msgid "Match rule set." +msgstr "匹配规则集。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:666 +msgid "Max Early Data" +msgstr "前置数据最大值" + +#: htdocs/luci-static/resources/view/fchomo/node.js:245 +#: htdocs/luci-static/resources/view/fchomo/server.js:263 +msgid "Max UDP relay packet size" +msgstr "UDP 中继数据包最大尺寸" + +#: htdocs/luci-static/resources/view/fchomo/client.js:601 +msgid "Max count of failures" +msgstr "最大失败次数" + +#: htdocs/luci-static/resources/view/fchomo/node.js:109 +#: htdocs/luci-static/resources/view/fchomo/server.js:164 +msgid "Max download speed" +msgstr "最大下载速度" + +#: htdocs/luci-static/resources/view/fchomo/node.js:103 +#: htdocs/luci-static/resources/view/fchomo/server.js:158 +msgid "Max upload speed" +msgstr "最大上传速度" + +#: htdocs/luci-static/resources/view/fchomo/node.js:703 +#: htdocs/luci-static/resources/view/fchomo/node.js:719 +msgid "Maximum connections" +msgstr "最大连接数" + +#: htdocs/luci-static/resources/view/fchomo/node.js:717 +msgid "" +"Maximum multiplexed streams in a connection before opening a new connection." +"
Conflict with %s and %s." +msgstr "" +"在打开新连接之前,连接中的最大多路复用流数量。
%s 和 " +"%s 冲突。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:716 +msgid "Maximum streams" +msgstr "最大流数量" + +#: htdocs/luci-static/resources/fchomo.js:91 +msgid "Mieru" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:426 +#: htdocs/luci-static/resources/view/fchomo/log.js:100 +msgid "Mihomo client" +msgstr "Mihomo 客户端" + +#: htdocs/luci-static/resources/view/fchomo/log.js:103 +#: htdocs/luci-static/resources/view/fchomo/server.js:58 +msgid "Mihomo server" +msgstr "Mihomo 服务端" + +#: htdocs/luci-static/resources/view/fchomo/node.js:710 +msgid "" +"Minimum multiplexed streams in a connection before opening a new connection." +msgstr "在打开新连接之前,连接中的最小多路复用流数量。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:709 +#: htdocs/luci-static/resources/view/fchomo/node.js:719 +msgid "Minimum streams" +msgstr "最小流数量" + +#: htdocs/luci-static/resources/fchomo.js:62 +#: htdocs/luci-static/resources/view/fchomo/global.js:400 +msgid "Mixed" +msgstr "混合" + +#: htdocs/luci-static/resources/view/fchomo/global.js:407 +msgid "Mixed system TCP stack and gVisor UDP stack." +msgstr "混合 系统 TCP 栈和 gVisor UDP 栈。" + +#: htdocs/luci-static/resources/view/fchomo/global.js:359 +msgid "Mixed port" +msgstr "混合端口" + +#: htdocs/luci-static/resources/view/fchomo/node.js:689 +msgid "Multiplex" +msgstr "多路复用" + +#: htdocs/luci-static/resources/view/fchomo/node.js:43 +msgid "Multiplex fields" +msgstr "多路复用字段" + +#: htdocs/luci-static/resources/view/fchomo/node.js:179 +msgid "Multiplexing" +msgstr "多路复用" + +#: htdocs/luci-static/resources/view/fchomo/node.js:897 +msgid "Name of the Proxy group to download provider." +msgstr "用于下载供应商订阅的代理组名称。" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:318 +msgid "Name of the Proxy group to download rule set." +msgstr "用于下载规则集订阅的代理组名称。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:229 +msgid "Native" +msgstr "原生" + +#: htdocs/luci-static/resources/view/fchomo/global.js:347 +msgid "No Authentication IP ranges" +msgstr "无需认证的 IP 范围" + +#: htdocs/luci-static/resources/view/fchomo/client.js:838 +msgid "No add'l params" +msgstr "无附加参数" + +#: htdocs/luci-static/resources/view/fchomo/client.js:588 +#: htdocs/luci-static/resources/view/fchomo/node.js:1032 +msgid "No testing is performed when this provider node is not in use." +msgstr "当此供应商的节点未使用时,不执行任何测试。" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:156 +msgid "No valid rule-set link found." +msgstr "未找到有效的规则集链接。" + +#: htdocs/luci-static/resources/view/fchomo/client.js:502 +#: htdocs/luci-static/resources/view/fchomo/node.js:35 +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:38 +msgid "Node" +msgstr "节点" + +#: htdocs/luci-static/resources/view/fchomo/client.js:633 +#: htdocs/luci-static/resources/view/fchomo/node.js:1049 +msgid "Node exclude filter" +msgstr "排除节点" + +#: htdocs/luci-static/resources/view/fchomo/client.js:638 +#: htdocs/luci-static/resources/view/fchomo/node.js:1055 +msgid "Node exclude type" +msgstr "排除节点类型" + +#: htdocs/luci-static/resources/view/fchomo/client.js:628 +#: htdocs/luci-static/resources/view/fchomo/node.js:1044 +msgid "Node filter" +msgstr "过滤节点" + +#: htdocs/luci-static/resources/view/fchomo/client.js:609 +msgid "Node switch tolerance" +msgstr "节点切换容差" + +#: htdocs/luci-static/resources/view/fchomo/node.js:302 +msgid "None" +msgstr "无" + +#: htdocs/luci-static/resources/view/fchomo/global.js:473 +msgid "Not Installed" +msgstr "未安装" + +#: htdocs/luci-static/resources/fchomo.js:504 +msgid "Not Running" +msgstr "未在运行" + +#: htdocs/luci-static/resources/view/fchomo/node.js:417 +msgid "Obfs Mode" +msgstr "Obfs 模式" + +#: htdocs/luci-static/resources/view/fchomo/node.js:121 +#: htdocs/luci-static/resources/view/fchomo/server.js:182 +msgid "Obfuscate password" +msgstr "混淆密码" + +#: htdocs/luci-static/resources/view/fchomo/node.js:115 +#: htdocs/luci-static/resources/view/fchomo/server.js:176 +msgid "Obfuscate type" +msgstr "混淆类型" + +#: htdocs/luci-static/resources/view/fchomo/global.js:587 +msgid "Only process traffic from specific interfaces. Leave empty for all." +msgstr "只处理来自指定接口的流量。留空表示全部。" + +#: htdocs/luci-static/resources/fchomo.js:498 +msgid "Open Dashboard" +msgstr "打开面板" + +#: htdocs/luci-static/resources/view/fchomo/global.js:296 +msgid "Operation mode" +msgstr "运行模式" + +#: htdocs/luci-static/resources/view/fchomo/client.js:925 +msgid "Override ECS in original request." +msgstr "覆盖原始请求中的 ECS。" + +#: htdocs/luci-static/resources/view/fchomo/global.js:515 +#: htdocs/luci-static/resources/view/fchomo/global.js:553 +msgid "Override destination" +msgstr "覆盖目标地址" + +#: htdocs/luci-static/resources/view/fchomo/client.js:471 +#: htdocs/luci-static/resources/view/fchomo/node.js:824 +msgid "Override fields" +msgstr "覆盖字段" + +#: htdocs/luci-static/resources/view/fchomo/node.js:212 +msgid "Override the IP address of the server that DNS response." +msgstr "覆盖 DNS 回应的服务器的 IP 地址。" + +#: htdocs/luci-static/resources/view/fchomo/client.js:1008 +msgid "Override the Proxy group of DNS server." +msgstr "覆盖 DNS 服务器所使用的代理组。" + +#: htdocs/luci-static/resources/view/fchomo/global.js:516 +msgid "Override the connection destination address with the sniffed domain." +msgstr "使用嗅探到的域名覆盖连接目标。" + +#: htdocs/luci-static/resources/view/fchomo/global.js:133 +msgid "Overview" +msgstr "概览" + +#: htdocs/luci-static/resources/view/fchomo/node.js:628 +msgid "POST" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:629 +msgid "PUT" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:335 +msgid "Packet encoding" +msgstr "数据包编码" + +#: htdocs/luci-static/resources/view/fchomo/node.js:78 +#: htdocs/luci-static/resources/view/fchomo/node.js:158 +#: htdocs/luci-static/resources/view/fchomo/node.js:431 +#: htdocs/luci-static/resources/view/fchomo/server.js:138 +#: htdocs/luci-static/resources/view/fchomo/server.js:216 +msgid "Password" +msgstr "密码" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:286 +msgid "Payload" +msgstr "Payload" + +#: htdocs/luci-static/resources/view/fchomo/node.js:364 +msgid "Peer pubkic key" +msgstr "对端公钥" + +#: htdocs/luci-static/resources/view/fchomo/global.js:431 +msgid "" +"Performance may degrade slightly, so it is not recommended to enable on when " +"it is not needed." +msgstr "性能可能会略有下降,建议仅在需要时开启。" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:210 +msgid "Plain text" +msgstr "纯文本" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:124 +msgid "" +"Please refer to
%s for link format " +"standards." +msgstr "链接格式标准请参考 %s。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:858 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:264 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:285 +msgid "" +"Please type %s." +msgstr "" +"请输入 %s。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:408 +msgid "Plugin" +msgstr "插件" + +#: htdocs/luci-static/resources/view/fchomo/node.js:417 +#: htdocs/luci-static/resources/view/fchomo/node.js:424 +#: htdocs/luci-static/resources/view/fchomo/node.js:431 +#: htdocs/luci-static/resources/view/fchomo/node.js:437 +#: htdocs/luci-static/resources/view/fchomo/node.js:445 +#: htdocs/luci-static/resources/view/fchomo/node.js:451 +msgid "Plugin:" +msgstr "插件:" + +#: htdocs/luci-static/resources/view/fchomo/node.js:66 +msgid "Port" +msgstr "端口" + +#: htdocs/luci-static/resources/fchomo.js:699 +msgid "Port %s alrealy exists!" +msgstr "端口 %s 已存在!" + +#: htdocs/luci-static/resources/view/fchomo/node.js:168 +msgid "Port range" +msgstr "端口范围" + +#: htdocs/luci-static/resources/view/fchomo/global.js:550 +msgid "Ports" +msgstr "端口" + +#: htdocs/luci-static/resources/view/fchomo/node.js:98 +msgid "Ports pool" +msgstr "端口池" + +#: htdocs/luci-static/resources/view/fchomo/node.js:189 +#: htdocs/luci-static/resources/view/fchomo/node.js:371 +msgid "Pre-shared key" +msgstr "预共享密钥" + +#: htdocs/luci-static/resources/fchomo.js:75 +msgid "Prefer IPv4" +msgstr "优先 IPv4" + +#: htdocs/luci-static/resources/fchomo.js:76 +msgid "Prefer IPv6" +msgstr "优先 IPv6" + +#: htdocs/luci-static/resources/view/fchomo/client.js:552 +#: htdocs/luci-static/resources/view/fchomo/client.js:558 +#: htdocs/luci-static/resources/view/fchomo/global.js:593 +#: htdocs/luci-static/resources/view/fchomo/global.js:610 +#: htdocs/luci-static/resources/view/fchomo/node.js:777 +#: htdocs/luci-static/resources/view/fchomo/node.js:783 +#: htdocs/luci-static/resources/view/fchomo/node.js:988 +#: htdocs/luci-static/resources/view/fchomo/node.js:994 +msgid "Priority: Proxy Node > Proxy Group > Global." +msgstr "优先级: 代理节点 > 代理组 > 全局。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:130 +msgid "Priv-key" +msgstr "密钥" + +#: htdocs/luci-static/resources/view/fchomo/node.js:134 +msgid "Priv-key passphrase" +msgstr "密钥密码" + +#: htdocs/luci-static/resources/view/fchomo/node.js:356 +msgid "Private key" +msgstr "私钥" + +#: htdocs/luci-static/resources/view/fchomo/global.js:302 +msgid "Process matching mode" +msgstr "进程匹配模式" + +#: htdocs/luci-static/resources/view/fchomo/global.js:544 +#: htdocs/luci-static/resources/view/fchomo/node.js:695 +msgid "Protocol" +msgstr "协议" + +#: htdocs/luci-static/resources/view/fchomo/node.js:330 +msgid "Protocol parameter. Enable length block encryption." +msgstr "协议参数。启用长度块加密。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:324 +msgid "" +"Protocol parameter. Will waste traffic randomly if enabled (enabled by " +"default in v2ray and cannot be disabled)." +msgstr "协议参数。 如启用会随机浪费流量(在 v2ray 中默认启用并且无法禁用)。" + +#: htdocs/luci-static/resources/view/fchomo/client.js:516 +#: htdocs/luci-static/resources/view/fchomo/node.js:796 +#: htdocs/luci-static/resources/view/fchomo/node.js:806 +#: htdocs/luci-static/resources/view/fchomo/node.js:1093 +msgid "Provider" +msgstr "供应商" + +#: htdocs/luci-static/resources/view/fchomo/node.js:878 +msgid "Provider URL" +msgstr "供应商订阅 URL" + +#: htdocs/luci-static/resources/view/fchomo/client.js:446 +#: htdocs/luci-static/resources/view/fchomo/client.js:465 +msgid "Proxy Group" +msgstr "代理组" + +#: htdocs/luci-static/resources/view/fchomo/global.js:643 +msgid "Proxy IPv4 IP-s" +msgstr "代理 IPv4 地址" + +#: htdocs/luci-static/resources/view/fchomo/global.js:646 +msgid "Proxy IPv6 IP-s" +msgstr "代理 IPv6 地址" + +#: htdocs/luci-static/resources/view/fchomo/global.js:649 +msgid "Proxy MAC-s" +msgstr "代理 MAC 地址" + +#: htdocs/luci-static/resources/view/fchomo/node.js:25 +#: htdocs/luci-static/resources/view/fchomo/node.js:1092 +msgid "Proxy Node" +msgstr "代理节点" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1067 +#: htdocs/luci-static/resources/view/fchomo/node.js:1077 +msgid "Proxy chain" +msgstr "代理链" + +#: htdocs/luci-static/resources/view/fchomo/client.js:361 +#: htdocs/luci-static/resources/view/fchomo/client.js:875 +#: htdocs/luci-static/resources/view/fchomo/client.js:1007 +#: htdocs/luci-static/resources/view/fchomo/node.js:896 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:317 +msgid "Proxy group" +msgstr "代理组" + +#: htdocs/luci-static/resources/view/fchomo/global.js:379 +msgid "Proxy mode" +msgstr "代理模式" + +#: htdocs/luci-static/resources/view/fchomo/global.js:652 +msgid "Proxy routerself" +msgstr "代理路由器自身" + +#: htdocs/luci-static/resources/view/fchomo/node.js:230 +msgid "QUIC" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:218 +#: htdocs/luci-static/resources/view/fchomo/server.js:255 +msgid "QUIC congestion controller." +msgstr "QUIC 拥塞控制器。" + +#: htdocs/luci-static/resources/view/fchomo/client.js:449 +#: htdocs/luci-static/resources/view/fchomo/server.js:79 +msgid "Quick Reload" +msgstr "快速重载" + +#: htdocs/luci-static/resources/view/fchomo/node.js:567 +msgid "REALITY" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:572 +msgid "REALITY public key" +msgstr "REALITY 公钥" + +#: htdocs/luci-static/resources/view/fchomo/node.js:577 +msgid "REALITY short ID" +msgstr "REALITY 标识符" + +#: htdocs/luci-static/resources/view/fchomo/global.js:504 +msgid "Random will be used if empty." +msgstr "留空将使用随机令牌。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:1126 +#: htdocs/luci-static/resources/view/fchomo/node.js:1174 +msgid "Recommended to use UoT node.
such as %s." +msgstr "建议使用 UoT 节点。
例如%s。" + +#: htdocs/luci-static/resources/view/fchomo/global.js:364 +msgid "Redir port" +msgstr "Redir 端口" + +#: htdocs/luci-static/resources/view/fchomo/global.js:380 +msgid "Redirect TCP" +msgstr "Redirect TCP" + +#: htdocs/luci-static/resources/view/fchomo/global.js:382 +msgid "Redirect TCP + TProxy UDP" +msgstr "Redirect TCP + TProxy UDP" + +#: htdocs/luci-static/resources/view/fchomo/global.js:384 +msgid "Redirect TCP + Tun UDP" +msgstr "Redirect TCP + Tun UDP" + +#: htdocs/luci-static/resources/view/fchomo/log.js:81 +msgid "Refresh every %s seconds." +msgstr "每 %s 秒刷新。" + +#: htdocs/luci-static/resources/fchomo.js:491 +#: htdocs/luci-static/resources/view/fchomo/client.js:450 +#: htdocs/luci-static/resources/view/fchomo/global.js:166 +#: htdocs/luci-static/resources/view/fchomo/server.js:80 +msgid "Reload" +msgstr "重载" + +#: htdocs/luci-static/resources/view/fchomo/global.js:165 +msgid "Reload All" +msgstr "重载所有" + +#: htdocs/luci-static/resources/view/fchomo/node.js:839 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:205 +msgid "Remote" +msgstr "远程" + +#: htdocs/luci-static/resources/view/fchomo/node.js:396 +msgid "Remote DNS resolve" +msgstr "远程 DNS 解析" + +#: htdocs/luci-static/resources/fchomo.js:629 +msgid "Remove" +msgstr "移除" + +#: htdocs/luci-static/resources/fchomo.js:634 +#: htdocs/luci-static/resources/view/fchomo/node.js:814 +#: htdocs/luci-static/resources/view/fchomo/node.js:816 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:185 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:187 +msgid "Remove idles" +msgstr "移除闲置" + +#: htdocs/luci-static/resources/view/fchomo/node.js:930 +msgid "Replace name" +msgstr "名称替换" + +#: htdocs/luci-static/resources/view/fchomo/node.js:931 +msgid "Replace node name." +msgstr "替换节点名称" + +#: htdocs/luci-static/resources/view/fchomo/node.js:635 +#: htdocs/luci-static/resources/view/fchomo/node.js:642 +msgid "Request path" +msgstr "请求路径" + +#: htdocs/luci-static/resources/view/fchomo/node.js:265 +msgid "Request timeout" +msgstr "请求超时" + +#: htdocs/luci-static/resources/view/fchomo/node.js:385 +msgid "Reserved field bytes" +msgstr "保留字段字节" + +#: htdocs/luci-static/resources/view/fchomo/global.js:202 +msgid "Resources management" +msgstr "资源管理" + +#: htdocs/luci-static/resources/view/fchomo/node.js:451 +msgid "Restls script" +msgstr "Restls 剧本" + +#: htdocs/luci-static/resources/view/fchomo/global.js:656 +msgid "Routing Control" +msgstr "路由控制" + +#: htdocs/luci-static/resources/view/fchomo/global.js:676 +msgid "Routing GFW" +msgstr "路由 GFW 流量" + +#: htdocs/luci-static/resources/view/fchomo/client.js:557 +#: htdocs/luci-static/resources/view/fchomo/global.js:609 +#: htdocs/luci-static/resources/view/fchomo/node.js:782 +#: htdocs/luci-static/resources/view/fchomo/node.js:993 +msgid "Routing mark" +msgstr "路由标记" + +#: htdocs/luci-static/resources/view/fchomo/global.js:672 +msgid "Routing mode" +msgstr "路由模式" + +#: htdocs/luci-static/resources/view/fchomo/global.js:673 +msgid "Routing mode of the traffic enters mihomo via firewall rules." +msgstr "流量通过防火墙规则进入 mihomo 的路由模式。" + +#: htdocs/luci-static/resources/view/fchomo/global.js:679 +msgid "Routing mode will be handle domain." +msgstr "路由模式将处理域名。" + +#: htdocs/luci-static/resources/view/fchomo/global.js:658 +#: htdocs/luci-static/resources/view/fchomo/global.js:665 +msgid "Routing ports" +msgstr "路由端口" + +#: htdocs/luci-static/resources/view/fchomo/client.js:646 +#: htdocs/luci-static/resources/view/fchomo/client.js:656 +msgid "Routing rule" +msgstr "路由规则" + +#: htdocs/luci-static/resources/view/fchomo/global.js:603 +msgid "Routing rule priority" +msgstr "路由规则优先权" + +#: htdocs/luci-static/resources/view/fchomo/global.js:597 +msgid "Routing table ID" +msgstr "路由表 ID" + +#: htdocs/luci-static/resources/view/fchomo/global.js:298 +msgid "Rule" +msgstr "规则" + +#: htdocs/luci-static/resources/view/fchomo/client.js:971 +#: htdocs/luci-static/resources/view/fchomo/client.js:984 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:112 +msgid "Rule set" +msgstr "规则集" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:299 +msgid "Rule set URL" +msgstr "规则集订阅 URL" + +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:46 +msgid "Ruleset" +msgstr "规则集" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:125 +msgid "Ruleset-URI-Scheme" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:504 +msgid "Running" +msgstr "正在运行" + +#: htdocs/luci-static/resources/fchomo.js:61 +msgid "SOCKS" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:88 +msgid "SOCKS5" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:100 +msgid "SSH" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:672 +msgid "SUB-RULE" +msgstr "SUB-RULE" + +#: htdocs/luci-static/resources/view/fchomo/node.js:469 +msgid "SUoT version" +msgstr "SUoT 版本" + +#: htdocs/luci-static/resources/view/fchomo/node.js:117 +#: htdocs/luci-static/resources/view/fchomo/server.js:178 +msgid "Salamander" +msgstr "Salamander" + +#: htdocs/luci-static/resources/fchomo.js:81 +msgid "Same dstaddr requests. Same node" +msgstr "相同 目标地址 请求。相同节点" + +#: htdocs/luci-static/resources/fchomo.js:82 +msgid "Same srcaddr and dstaddr requests. Same node" +msgstr "相同 来源地址 和 目标地址 请求。相同节点" + +#: htdocs/luci-static/resources/view/fchomo/global.js:421 +msgid "Segment maximum size" +msgstr "分段最大尺寸" + +#: htdocs/luci-static/resources/fchomo.js:122 +msgid "Select" +msgstr "手动选择" + +#: htdocs/luci-static/resources/view/fchomo/global.js:464 +msgid "Select Dashboard" +msgstr "选择面板" + +#: htdocs/luci-static/resources/view/fchomo/server.js:97 +#: root/usr/share/luci/menu.d/luci-app-fchomo.json:54 +msgid "Server" +msgstr "服务端" + +#: htdocs/luci-static/resources/view/fchomo/node.js:61 +msgid "Server address" +msgstr "服务器地址" + +#: htdocs/luci-static/resources/view/fchomo/node.js:620 +msgid "Server hostname" +msgstr "服务器主机名称" + +#: htdocs/luci-static/resources/view/fchomo/global.js:157 +msgid "Server status" +msgstr "服务端状态" + +#: htdocs/luci-static/resources/view/fchomo/global.js:136 +msgid "Service status" +msgstr "服务状态" + +#: htdocs/luci-static/resources/fchomo.js:63 +#: htdocs/luci-static/resources/fchomo.js:89 +msgid "Shadowsocks" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:278 +msgid "Shadowsocks chipher" +msgstr "Shadowsocks 加密方法" + +#: htdocs/luci-static/resources/view/fchomo/node.js:273 +msgid "Shadowsocks encrypt" +msgstr "Shadowsocks 加密" + +#: htdocs/luci-static/resources/view/fchomo/node.js:285 +msgid "Shadowsocks password" +msgstr "Shadowsocks 密码" + +#: htdocs/luci-static/resources/view/fchomo/node.js:737 +msgid "Show connections in the dashboard for breaking connections easier." +msgstr "在面板中显示连接以便于打断连接。" + +#: htdocs/luci-static/resources/view/fchomo/global.js:309 +msgid "Silent" +msgstr "静音" + +#: htdocs/luci-static/resources/fchomo.js:80 +msgid "Simple round-robin all nodes" +msgstr "简单轮替所有节点" + +#: htdocs/luci-static/resources/view/fchomo/node.js:884 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:305 +msgid "Size limit" +msgstr "大小限制" + +#: htdocs/luci-static/resources/view/fchomo/node.js:549 +#: htdocs/luci-static/resources/view/fchomo/node.js:973 +msgid "Skip cert verify" +msgstr "跳过证书验证" + +#: htdocs/luci-static/resources/view/fchomo/global.js:522 +msgid "Skiped sniffing domain" +msgstr "跳过嗅探域名" + +#: htdocs/luci-static/resources/view/fchomo/global.js:528 +msgid "Skiped sniffing dst address" +msgstr "跳过嗅探目标地址" + +#: htdocs/luci-static/resources/view/fchomo/global.js:525 +msgid "Skiped sniffing src address" +msgstr "跳过嗅探来源地址" + +#: htdocs/luci-static/resources/fchomo.js:92 +msgid "Snell" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:532 +msgid "Sniff protocol" +msgstr "嗅探协议" + +#: htdocs/luci-static/resources/view/fchomo/global.js:509 +msgid "Sniffer" +msgstr "嗅探器" + +#: htdocs/luci-static/resources/view/fchomo/global.js:512 +msgid "Sniffer settings" +msgstr "嗅探器设置" + +#: htdocs/luci-static/resources/view/fchomo/global.js:659 +#: htdocs/luci-static/resources/view/fchomo/global.js:666 +msgid "" +"Specify target ports to be proxied. Multiple ports must be separated by " +"commas." +msgstr "指定需要被代理的目标端口。多个端口必须用逗号隔开。" + +#: htdocs/luci-static/resources/view/fchomo/global.js:395 +msgid "Stack" +msgstr "堆栈" + +#: htdocs/luci-static/resources/view/fchomo/client.js:617 +#: htdocs/luci-static/resources/view/fchomo/client.js:619 +msgid "Strategy" +msgstr "策略" + +#: htdocs/luci-static/resources/view/fchomo/client.js:695 +#: htdocs/luci-static/resources/view/fchomo/client.js:716 +#: htdocs/luci-static/resources/view/fchomo/client.js:726 +msgid "Sub rule" +msgstr "子规则" + +#: htdocs/luci-static/resources/view/fchomo/client.js:740 +msgid "Sub rule group" +msgstr "子规则组" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:158 +msgid "Successfully imported %s rule-set of total %s." +msgstr "已成功导入 %s 个规则集 (共 %s 个)。" + +#: htdocs/luci-static/resources/view/fchomo/global.js:49 +msgid "Successfully updated." +msgstr "更新成功。" + +#: htdocs/luci-static/resources/fchomo.js:921 +msgid "Successfully uploaded." +msgstr "已成功上传。" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:122 +msgid "" +"Supports rule-set links of type: %s and format: %s." +"
" +msgstr "" +"支持规则集类型为: %s 并且格式为: %s 的规则集链接。" +"
" + +#: htdocs/luci-static/resources/view/fchomo/global.js:397 +msgid "System" +msgstr "系统" + +#: htdocs/luci-static/resources/view/fchomo/client.js:15 +msgid "System DNS" +msgstr "系统 DNS" + +#: htdocs/luci-static/resources/view/fchomo/client.js:275 +msgid "TCP" +msgstr "TCP" + +#: htdocs/luci-static/resources/view/fchomo/global.js:325 +msgid "TCP concurrency" +msgstr "TCP 并发" + +#: htdocs/luci-static/resources/view/fchomo/node.js:730 +msgid "TCP only" +msgstr "仅 TCP" + +#: htdocs/luci-static/resources/view/fchomo/global.js:333 +msgid "TCP-Keep-Alive idle timeout" +msgstr "TCP-Keep-Alive 闲置超时" + +#: htdocs/luci-static/resources/view/fchomo/global.js:328 +msgid "TCP-Keep-Alive interval" +msgstr "TCP-Keep-Alive 间隔" + +#: htdocs/luci-static/resources/view/fchomo/node.js:761 +#: htdocs/luci-static/resources/view/fchomo/node.js:946 +msgid "TFO" +msgstr "TCP 快速打开 (TFO)" + +#: htdocs/luci-static/resources/view/fchomo/global.js:436 +#: htdocs/luci-static/resources/view/fchomo/node.js:419 +#: htdocs/luci-static/resources/view/fchomo/node.js:477 +#: htdocs/luci-static/resources/view/fchomo/server.js:309 +msgid "TLS" +msgstr "TLS" + +#: htdocs/luci-static/resources/view/fchomo/node.js:531 +#: htdocs/luci-static/resources/view/fchomo/server.js:331 +msgid "TLS ALPN" +msgstr "TLS ALPN" + +#: htdocs/luci-static/resources/view/fchomo/node.js:525 +msgid "TLS SNI" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:41 +msgid "TLS fields" +msgstr "TLS字段" + +#: htdocs/luci-static/resources/fchomo.js:65 +#: htdocs/luci-static/resources/fchomo.js:98 +msgid "TUIC" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:171 +msgid "" +"Tell the client to use the BBR flow control algorithm instead of Hysteria CC." +msgstr "让客户端使用 BBR 流控算法。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:344 +#: htdocs/luci-static/resources/view/fchomo/node.js:351 +msgid "The %s address used by local machine in the Wireguard network." +msgstr "WireGuard 网络中使用的本机 %s 地址。" + +#: htdocs/luci-static/resources/view/fchomo/global.js:226 +msgid "The default value is 2:00 every day." +msgstr "默认值为每天 2:00。" + +#: htdocs/luci-static/resources/view/fchomo/client.js:1030 +msgid "The matching %s will be deemed as not-poisoned." +msgstr "匹配的 %s 将被视为未被污染。" + +#: htdocs/luci-static/resources/view/fchomo/client.js:1039 +#: htdocs/luci-static/resources/view/fchomo/client.js:1043 +#: htdocs/luci-static/resources/view/fchomo/client.js:1048 +msgid "The matching %s will be deemed as poisoned." +msgstr "匹配的 %s 将被视为已被污染。" + +#: htdocs/luci-static/resources/view/fchomo/server.js:352 +msgid "The server private key, in PEM format." +msgstr "服务端私钥,需要 PEM 格式。" + +#: htdocs/luci-static/resources/view/fchomo/server.js:337 +msgid "The server public key, in PEM format." +msgstr "服务端公钥,需要 PEM 格式。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:552 +#: htdocs/luci-static/resources/view/fchomo/node.js:976 +msgid "" +"This is DANGEROUS, your traffic is almost like " +"PLAIN TEXT! Use at your own risk!" +msgstr "" +"这是危险行为,您的流量将几乎等同于明文!使用风险自负!" + +#: htdocs/luci-static/resources/view/fchomo/node.js:235 +msgid "" +"This is the TUIC port of the SUoT protocol, designed to provide a QUIC " +"stream based UDP relay mode that TUIC does not provide." +msgstr "" +"这是 TUIC 的 SUoT 协议移植, 旨在提供 TUIC 不提供的基于 QUIC 流的 UDP 中继模" +"式。" + +#: htdocs/luci-static/resources/view/fchomo/global.js:387 +msgid "" +"To enable Tun support, you need to install ip-full and " +"kmod-tun" +msgstr "" +"要启用 Tun 支持,您需要安装 ip-fullkmod-tun。" + +#: htdocs/luci-static/resources/view/fchomo/global.js:682 +msgid "To enable, you need to install dnsmasq-full." +msgstr "要启用,您需要安装 dnsmasq-full。" + +#: htdocs/luci-static/resources/view/fchomo/global.js:616 +msgid "Tproxy Fwmark" +msgstr "Tproxy 标记" + +#: htdocs/luci-static/resources/view/fchomo/global.js:369 +msgid "Tproxy port" +msgstr "Tproxy 端口" + +#: htdocs/luci-static/resources/view/fchomo/node.js:173 +#: htdocs/luci-static/resources/view/fchomo/node.js:583 +msgid "Transport" +msgstr "传输层" + +#: htdocs/luci-static/resources/view/fchomo/node.js:42 +msgid "Transport fields" +msgstr "传输层字段" + +#: htdocs/luci-static/resources/view/fchomo/node.js:588 +msgid "Transport type" +msgstr "传输层类型" + +#: htdocs/luci-static/resources/fchomo.js:95 +msgid "Trojan" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:621 +msgid "Tun Fwmark" +msgstr "Tun 标记" + +#: htdocs/luci-static/resources/view/fchomo/global.js:385 +msgid "Tun TCP/UDP" +msgstr "Tun TCP/UDP" + +#: htdocs/luci-static/resources/view/fchomo/global.js:392 +msgid "Tun settings" +msgstr "Tun 设置" + +#: htdocs/luci-static/resources/view/fchomo/global.js:396 +msgid "Tun stack." +msgstr "Tun 堆栈" + +#: htdocs/luci-static/resources/view/fchomo/client.js:223 +#: htdocs/luci-static/resources/view/fchomo/client.js:313 +#: htdocs/luci-static/resources/view/fchomo/client.js:489 +#: htdocs/luci-static/resources/view/fchomo/client.js:968 +#: htdocs/luci-static/resources/view/fchomo/node.js:55 +#: htdocs/luci-static/resources/view/fchomo/node.js:837 +#: htdocs/luci-static/resources/view/fchomo/node.js:1091 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:203 +#: htdocs/luci-static/resources/view/fchomo/server.js:112 +msgid "Type" +msgstr "类型" + +#: htdocs/luci-static/resources/view/fchomo/client.js:274 +#: htdocs/luci-static/resources/view/fchomo/node.js:458 +#: htdocs/luci-static/resources/view/fchomo/node.js:954 +#: htdocs/luci-static/resources/view/fchomo/server.js:367 +msgid "UDP" +msgstr "UDP" + +#: htdocs/luci-static/resources/view/fchomo/global.js:425 +msgid "UDP NAT expiration time" +msgstr "UDP NAT 过期时间" + +#: htdocs/luci-static/resources/view/fchomo/node.js:234 +msgid "UDP over stream" +msgstr "UDP over stream" + +#: htdocs/luci-static/resources/view/fchomo/node.js:240 +msgid "UDP over stream version" +msgstr "UDP over stream 版本" + +#: htdocs/luci-static/resources/view/fchomo/node.js:227 +msgid "UDP packet relay mode." +msgstr "UDP 包中继模式。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:226 +msgid "UDP relay mode" +msgstr "UDP 中继模式" + +#: htdocs/luci-static/resources/fchomo.js:124 +msgid "URL test" +msgstr "自动选择" + +#: htdocs/luci-static/resources/view/fchomo/node.js:205 +#: htdocs/luci-static/resources/view/fchomo/node.js:295 +#: htdocs/luci-static/resources/view/fchomo/server.js:237 +#: htdocs/luci-static/resources/view/fchomo/server.js:284 +msgid "UUID" +msgstr "UUID" + +#: htdocs/luci-static/resources/fchomo.js:549 +msgid "Unable to download unsupported type: %s" +msgstr "无法下载不支持的类型: %s" + +#: htdocs/luci-static/resources/view/fchomo/hosts.js:21 +msgid "Unable to save contents: %s" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:322 +msgid "Unified delay" +msgstr "统一延迟" + +#: htdocs/luci-static/resources/view/fchomo/global.js:141 +#: htdocs/luci-static/resources/view/fchomo/global.js:146 +msgid "Unknown" +msgstr "未知" + +#: htdocs/luci-static/resources/view/fchomo/global.js:61 +msgid "Unknown error." +msgstr "未知错误。" + +#: htdocs/luci-static/resources/view/fchomo/log.js:58 +msgid "Unknown error: %s" +msgstr "未知错误:%s" + +#: htdocs/luci-static/resources/view/fchomo/node.js:463 +#: htdocs/luci-static/resources/view/fchomo/node.js:958 +msgid "UoT" +msgstr "UDP over TCP (UoT)" + +#: htdocs/luci-static/resources/view/fchomo/global.js:52 +msgid "Update failed." +msgstr "更新失败。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:890 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:311 +msgid "Update interval" +msgstr "更新间隔" + +#: htdocs/luci-static/resources/view/fchomo/node.js:748 +msgid "Upload bandwidth" +msgstr "上传带宽" + +#: htdocs/luci-static/resources/view/fchomo/node.js:749 +msgid "Upload bandwidth in Mbps." +msgstr "上传带宽(单位:Mbps)。" + +#: htdocs/luci-static/resources/view/fchomo/server.js:343 +msgid "Upload certificate" +msgstr "上传证书" + +#: htdocs/luci-static/resources/view/fchomo/global.js:206 +msgid "Upload initial package" +msgstr "上传初始资源包" + +#: htdocs/luci-static/resources/view/fchomo/server.js:358 +msgid "Upload key" +msgstr "上传密钥" + +#: htdocs/luci-static/resources/view/fchomo/global.js:208 +#: htdocs/luci-static/resources/view/fchomo/server.js:346 +#: htdocs/luci-static/resources/view/fchomo/server.js:361 +msgid "Upload..." +msgstr "上传..." + +#: htdocs/luci-static/resources/view/fchomo/client.js:765 +msgid "Used to resolve the domain of the DNS server. Must be IP." +msgstr "用于解析 DNS 服务器的域名。必须是 IP。" + +#: htdocs/luci-static/resources/view/fchomo/client.js:772 +msgid "Used to resolve the domain of the Proxy node." +msgstr "用于解析代理节点的域名。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:526 +msgid "Used to verify the hostname on the returned certificates." +msgstr "用于验证返回的证书上的主机名。" + +#: htdocs/luci-static/resources/view/fchomo/global.js:342 +msgid "User Authentication" +msgstr "用户认证" + +#: htdocs/luci-static/resources/view/fchomo/node.js:73 +#: htdocs/luci-static/resources/view/fchomo/server.js:133 +msgid "Username" +msgstr "用户名" + +#: htdocs/luci-static/resources/view/fchomo/global.js:629 +msgid "Users filter mode" +msgstr "使用者过滤模式" + +#: htdocs/luci-static/resources/view/fchomo/node.js:678 +msgid "V2ray HTTPUpgrade" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:683 +msgid "V2ray HTTPUpgrade fast open" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:94 +msgid "VLESS" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:64 +#: htdocs/luci-static/resources/fchomo.js:93 +msgid "VMess" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:842 +#: htdocs/luci-static/resources/view/fchomo/node.js:1097 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:246 +msgid "Value" +msgstr "可视化值" + +#: htdocs/luci-static/resources/view/fchomo/node.js:196 +#: htdocs/luci-static/resources/view/fchomo/node.js:437 +msgid "Version" +msgstr "版本" + +#: htdocs/luci-static/resources/view/fchomo/node.js:445 +msgid "Version hint" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/global.js:311 +msgid "Warning" +msgstr "警告" + +#: htdocs/luci-static/resources/view/fchomo/node.js:593 +#: htdocs/luci-static/resources/view/fchomo/node.js:604 +#: htdocs/luci-static/resources/view/fchomo/node.js:609 +msgid "WebSocket" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:59 +msgid "When used as a server, HomeProxy is a better choice." +msgstr "用作服务端时,HomeProxy 是更好的选择。" + +#: htdocs/luci-static/resources/view/fchomo/global.js:631 +msgid "White list" +msgstr "白名单" + +#: htdocs/luci-static/resources/fchomo.js:99 +msgid "WireGuard" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:365 +msgid "WireGuard peer public key." +msgstr "WireGuard 对端公钥。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:372 +msgid "WireGuard pre-shared key." +msgstr "WireGuard 预共享密钥。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:357 +msgid "WireGuard requires base64-encoded private keys." +msgstr "WireGuard 要求 base64 编码的私钥。" + +#: htdocs/luci-static/resources/view/fchomo/node.js:338 +msgid "Xudp (Xray-core)" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:211 +msgid "Yaml text" +msgstr "Yaml 格式文本" + +#: htdocs/luci-static/resources/fchomo.js:51 +msgid "YouTube" +msgstr "油管" + +#: htdocs/luci-static/resources/fchomo.js:903 +msgid "Your %s was successfully uploaded. Size: %sB." +msgstr "您的 %s 已成功上传。大小:%sB。" + +#: htdocs/luci-static/resources/fchomo.js:187 +#: htdocs/luci-static/resources/view/fchomo/node.js:279 +#: htdocs/luci-static/resources/view/fchomo/node.js:318 +msgid "aes-128-gcm" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:188 +msgid "aes-192-gcm" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:189 +#: htdocs/luci-static/resources/view/fchomo/node.js:280 +msgid "aes-256-gcm" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:315 +msgid "auto" +msgstr "自动" + +#: htdocs/luci-static/resources/view/fchomo/node.js:222 +#: htdocs/luci-static/resources/view/fchomo/server.js:259 +msgid "bbr" +msgstr "bbr" + +#: htdocs/luci-static/resources/view/fchomo/server.js:348 +msgid "certificate" +msgstr "证书" + +#: htdocs/luci-static/resources/fchomo.js:190 +#: htdocs/luci-static/resources/view/fchomo/node.js:281 +msgid "chacha20-ietf-poly1305" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:319 +msgid "chacha20-poly1305" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:220 +#: htdocs/luci-static/resources/view/fchomo/server.js:257 +msgid "cubic" +msgstr "cubic" + +#: htdocs/luci-static/resources/view/fchomo/node.js:968 +msgid "down" +msgstr "Hysteria 下载速率" + +#: htdocs/luci-static/resources/view/fchomo/node.js:592 +#: htdocs/luci-static/resources/view/fchomo/node.js:603 +#: htdocs/luci-static/resources/view/fchomo/node.js:608 +msgid "gRPC" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:662 +msgid "gRPC service name" +msgstr "gRPC 服务名称" + +#: htdocs/luci-static/resources/view/fchomo/global.js:399 +msgid "gVisor" +msgstr "gVisor" + +#: htdocs/luci-static/resources/view/fchomo/node.js:699 +msgid "h2mux" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:34 +msgid "metacubexd" +msgstr "metacubexd" + +#: htdocs/luci-static/resources/view/fchomo/node.js:765 +#: htdocs/luci-static/resources/view/fchomo/node.js:950 +msgid "mpTCP" +msgstr "多路径 TCP (mpTCP)" + +#: htdocs/luci-static/resources/view/fchomo/node.js:221 +#: htdocs/luci-static/resources/view/fchomo/server.js:258 +msgid "new_reno" +msgstr "new_reno" + +#: htdocs/luci-static/resources/view/fchomo/client.js:396 +msgid "no-resolve" +msgstr "no-resolve" + +#: htdocs/luci-static/resources/fchomo.js:740 +#: htdocs/luci-static/resources/fchomo.js:771 +msgid "non-empty value" +msgstr "非空值" + +#: htdocs/luci-static/resources/fchomo.js:185 +#: htdocs/luci-static/resources/view/fchomo/node.js:316 +#: htdocs/luci-static/resources/view/fchomo/node.js:336 +#: htdocs/luci-static/resources/view/fchomo/node.js:409 +#: htdocs/luci-static/resources/view/fchomo/ruleset.js:228 +msgid "none" +msgstr "无" + +#: htdocs/luci-static/resources/view/fchomo/global.js:92 +msgid "not found" +msgstr "未找到" + +#: htdocs/luci-static/resources/view/fchomo/client.js:479 +msgid "not included \",\"" +msgstr "不包含 \",\"" + +#: htdocs/luci-static/resources/fchomo.js:112 +msgid "null" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:410 +msgid "obfs-simple" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:933 +msgid "override.proxy-name" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:337 +msgid "packet addr (v2ray-core v5+)" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/server.js:363 +msgid "private key" +msgstr "私钥" + +#: htdocs/luci-static/resources/fchomo.js:36 +msgid "razord-meta" +msgstr "razord-meta" + +#: htdocs/luci-static/resources/view/fchomo/node.js:413 +msgid "restls" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:412 +msgid "shadow-tls" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:697 +msgid "smux" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/client.js:379 +msgid "src" +msgstr "src" + +#: htdocs/luci-static/resources/view/fchomo/global.js:196 +msgid "unchecked" +msgstr "未检查" + +#: htdocs/luci-static/resources/fchomo.js:573 +msgid "unique UCI identifier" +msgstr "独立 UCI 标识" + +#: htdocs/luci-static/resources/fchomo.js:576 +msgid "unique identifier" +msgstr "独立标识" + +#: htdocs/luci-static/resources/fchomo.js:780 +msgid "unique value" +msgstr "独立值" + +#: htdocs/luci-static/resources/view/fchomo/node.js:963 +msgid "up" +msgstr "Hysteria 上传速率" + +#: htdocs/luci-static/resources/view/fchomo/node.js:197 +#: htdocs/luci-static/resources/view/fchomo/node.js:241 +#: htdocs/luci-static/resources/view/fchomo/node.js:438 +#: htdocs/luci-static/resources/view/fchomo/node.js:470 +msgid "v1" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:198 +#: htdocs/luci-static/resources/view/fchomo/node.js:439 +#: htdocs/luci-static/resources/view/fchomo/node.js:471 +msgid "v2" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:199 +#: htdocs/luci-static/resources/view/fchomo/node.js:440 +msgid "v3" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:714 +#: htdocs/luci-static/resources/fchomo.js:717 +msgid "valid JSON format" +msgstr "有效的 JSON 格式" + +#: htdocs/luci-static/resources/view/fchomo/node.js:542 +msgid "valid SHA256 string with %d characters" +msgstr "包含 %d 个字符的有效 SHA256 字符串" + +#: htdocs/luci-static/resources/fchomo.js:792 +#: htdocs/luci-static/resources/fchomo.js:795 +msgid "valid URL" +msgstr "有效网址" + +#: htdocs/luci-static/resources/fchomo.js:727 +msgid "valid base64 key with %d characters" +msgstr "包含 %d 个字符的有效 base64 密钥" + +#: htdocs/luci-static/resources/fchomo.js:742 +msgid "valid key length with %d characters" +msgstr "包含 %d 个字符的有效密钥" + +#: htdocs/luci-static/resources/fchomo.js:697 +msgid "valid port value" +msgstr "有效端口值" + +#: htdocs/luci-static/resources/fchomo.js:805 +msgid "valid uuid" +msgstr "有效 uuid" + +#: htdocs/luci-static/resources/fchomo.js:191 +msgid "xchacha20-ietf-poly1305" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:35 +msgid "yacd-meta" +msgstr "yacd-meta" + +#: htdocs/luci-static/resources/view/fchomo/node.js:698 +msgid "yamux" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:33 +msgid "zashboard" +msgstr "" + +#: htdocs/luci-static/resources/view/fchomo/node.js:317 +msgid "zero" +msgstr "" + +#: htdocs/luci-static/resources/fchomo.js:551 +msgid "🡇" +msgstr "" diff --git a/luci-app-fchomo/root/etc/capabilities/fchomo.json b/luci-app-fchomo/root/etc/capabilities/fchomo.json new file mode 100644 index 0000000000..2e0ca6107b --- /dev/null +++ b/luci-app-fchomo/root/etc/capabilities/fchomo.json @@ -0,0 +1,47 @@ +{ + "bounding": [ + "CAP_NET_ADMIN", + "CAP_NET_RAW", + "CAP_NET_BIND_SERVICE", + "CAP_SYS_TIME", + "CAP_SYS_PTRACE", + "CAP_DAC_READ_SEARCH", + "CAP_DAC_OVERRIDE" + ], + "effective": [ + "CAP_NET_ADMIN", + "CAP_NET_RAW", + "CAP_NET_BIND_SERVICE", + "CAP_SYS_TIME", + "CAP_SYS_PTRACE", + "CAP_DAC_READ_SEARCH", + "CAP_DAC_OVERRIDE" + ], + "ambient": [ + "CAP_NET_ADMIN", + "CAP_NET_RAW", + "CAP_NET_BIND_SERVICE", + "CAP_SYS_TIME", + "CAP_SYS_PTRACE", + "CAP_DAC_READ_SEARCH", + "CAP_DAC_OVERRIDE" + ], + "permitted": [ + "CAP_NET_ADMIN", + "CAP_NET_RAW", + "CAP_NET_BIND_SERVICE", + "CAP_SYS_TIME", + "CAP_SYS_PTRACE", + "CAP_DAC_READ_SEARCH", + "CAP_DAC_OVERRIDE" + ], + "inheritable": [ + "CAP_NET_ADMIN", + "CAP_NET_RAW", + "CAP_NET_BIND_SERVICE", + "CAP_SYS_TIME", + "CAP_SYS_PTRACE", + "CAP_DAC_READ_SEARCH", + "CAP_DAC_OVERRIDE" + ] +} diff --git a/luci-app-fchomo/root/etc/config/fchomo b/luci-app-fchomo/root/etc/config/fchomo new file mode 100644 index 0000000000..b534d6124f --- /dev/null +++ b/luci-app-fchomo/root/etc/config/fchomo @@ -0,0 +1,70 @@ + +config fchomo 'config' + option __warning 'ACCESS CONTROL FIELDS' + option common_tcpport '20-21,22,53,80,110,143,443,465,853,873,993,995,8080,8443,9418' + option common_udpport '20-21,22,53,80,110,143,443,853,993,995,8080,8443,9418' + option stun_port '3478,19302' + option tun_name 'hmtun0' + option tun_addr4 '198.19.0.1/30' + option tun_addr6 'fdfe:dcba:9877::1/126' + option route_table_id '2022' + option route_rule_pref '9000' + option redirect_gate_mark '2023' + option redirect_pass_mark '2024' + option self_mark '200' + option tproxy_mark '201' + option tun_mark '202' + +config fchomo 'resources' + option auto_update '0' + +config fchomo 'routing' + option routing_tcpport 'common' + option routing_udpport 'common' + +config fchomo 'global' + option mode 'rule' + option find_process_mode 'off' + option log_level 'warning' + option unified_delay '1' + option tcp_concurrent '1' + list skip_auth_prefixes '127.0.0.1/8' + list skip_auth_prefixes '::1/128' + +config fchomo 'inbound' + option mixed_port '7890' + option redir_port '7891' + option tproxy_port '7892' + option tunnel_port '7893' + option proxy_mode 'redir_tproxy' + option tun_stack 'system' + +config fchomo 'tls' + +config fchomo 'api' + option external_controller_port '9090' + option external_controller_tls_port '9443' + +config fchomo 'sniffer' + list skip_domain 'Mijia Cloud' + +config fchomo 'dns' + option port '7853' + option fallback_filter_geoip_code 'cn' + +config fchomo 'experimental' + +config sniff + option protocol 'HTTP' + list ports '80' + list ports '8080-8880' + +config sniff + option protocol 'TLS' + list ports '443' + list ports '8443' + +config sniff + option protocol 'QUIC' + list ports '443' + list ports '8443' diff --git a/luci-app-fchomo/root/etc/fchomo/scripts/clean_log.sh b/luci-app-fchomo/root/etc/fchomo/scripts/clean_log.sh new file mode 100755 index 0000000000..b01ff1b5ca --- /dev/null +++ b/luci-app-fchomo/root/etc/fchomo/scripts/clean_log.sh @@ -0,0 +1,17 @@ +#!/bin/sh +# Thanks to homeproxy + +NAME="fchomo" + +log_max_size="30" #KB +main_log_file="/var/run/$NAME/$NAME.log" +singc_log_file="/var/run/$NAME/mihomo-c.log" +sings_log_file="/var/run/$NAME/mihomo-s.log" + +while true; do + sleep 180 + for i in "$main_log_file" "$singc_log_file" "$sings_log_file"; do + [ -s "$i" ] || continue + [ "$(( $(ls -l "$i" | awk -F ' ' '{print $5}') / 1024 >= log_max_size))" -eq "0" ] || echo "" > "$i" + done +done diff --git a/luci-app-fchomo/root/etc/fchomo/scripts/fchomo.uc b/luci-app-fchomo/root/etc/fchomo/scripts/fchomo.uc new file mode 100755 index 0000000000..5e58ae3ac1 --- /dev/null +++ b/luci-app-fchomo/root/etc/fchomo/scripts/fchomo.uc @@ -0,0 +1,134 @@ +/* thanks for homeproxy */ + +import { mkstemp, popen } from 'fs'; + +/* Global variables START */ +export const HM_DIR = '/etc/fchomo'; +export const RUN_DIR = '/var/run/fchomo'; +export const PRESET_OUTBOUND = [ + 'DIRECT', + 'REJECT', + 'REJECT-DROP', + 'PASS', + 'COMPATIBLE' +]; +/* Global variables END */ + +/* Utilities start */ +/* Kanged from luci-app-commands */ +export function shellQuote(s) { + return `'${replace(s, "'", "'\\''")}'`; +}; + +export function yqRead(flags, command, filepath) { + let out = ''; + + const fd = popen(`yq ${flags} ${shellQuote(command)} ${filepath}`); + if (fd) { + out = fd.read('all'); + fd.close(); + } + + return out; +}; +/* Utilities end */ + +/* String helper start */ +export function isEmpty(res) { + return !res || res === 'nil' || (type(res) in ['array', 'object'] && length(res) === 0); +}; + +export function strToBool(str) { + return (str === '1') || null; +}; + +export function strToInt(str) { + if (isEmpty(str)) + return null; + + return !match(str, /^\d+$/) ? str : int(str) ?? null; +}; + +export function bytesizeToByte(str) { + if (isEmpty(str)) + return null; + + let bytes = 0; + let arr = match(str, /^(\d+)(k|m|g)?b?$/); + if (arr) { + if (arr[2] === 'k') { + bytes = strToInt(arr[1]) * 1024; + } else if (arr[2] === 'm') { + bytes = strToInt(arr[1]) * 1048576; + } else if (arr[2] === 'g') { + bytes = strToInt(arr[1]) * 1073741824; + } else + bytes = strToInt(arr[1]); + } + + return bytes; +}; +export function durationToSecond(str) { + if (isEmpty(str)) + return null; + + let seconds = 0; + let arr = match(str, /^(\d+)(s|m|h|d)?$/); + if (arr) { + if (arr[2] === 's') { + seconds = strToInt(arr[1]); + } else if (arr[2] === 'm') { + seconds = strToInt(arr[1]) * 60; + } else if (arr[2] === 'h') { + seconds = strToInt(arr[1]) * 3600; + } else if (arr[2] === 'd') { + seconds = strToInt(arr[1]) * 86400; + } else + seconds = strToInt(arr[1]); + } + + return seconds; +}; + +export function arrToObj(res) { + if (isEmpty(res)) + return null; + + let object; + if (type(res) === 'array') { + object = {}; + map(res, (e) => { + if (type(e) === 'array') + object[e[0]] = e[1]; + }); + } else + return res; + + return object; +}; + +export function removeBlankAttrs(res) { + let content; + + if (type(res) === 'object') { + content = {}; + map(keys(res), (k) => { + if (type(res[k]) in ['array', 'object']) + content[k] = removeBlankAttrs(res[k]); + else if (res[k] !== null && res[k] !== '') + content[k] = res[k]; + }); + } else if (type(res) === 'array') { + content = []; + map(res, (k, i) => { + if (type(k) in ['array', 'object']) + push(content, removeBlankAttrs(k)); + else if (k !== null && k !== '') + push(content, k); + }); + } else + return res; + + return content; +}; +/* String helper end */ diff --git a/luci-app-fchomo/root/etc/fchomo/scripts/firewall_post.ut b/luci-app-fchomo/root/etc/fchomo/scripts/firewall_post.ut new file mode 100755 index 0000000000..145acbbf73 --- /dev/null +++ b/luci-app-fchomo/root/etc/fchomo/scripts/firewall_post.ut @@ -0,0 +1,454 @@ +#!/usr/bin/utpl -S + +{# Thanks to homeproxy -#} +{%- + import { readfile } from 'fs'; + import { cursor } from 'uci'; + import { isEmpty, yqRead } from '/etc/fchomo/scripts/fchomo.uc'; + + const fw4 = require('fw4'); + + function array_to_nftarr(array) { + if (type(array) !== 'array') + return null; + + return `{ ${join(', ', uniq(array))} }`; + } + + function resolve_ipv6(str) { + if (isEmpty(str)) + return null; + + let ipv6 = fw4.parse_subnet(str)?.[0]; + if (!ipv6 || ipv6.family !== 6) + return null; + + if (ipv6.bits > -1) + return `${ipv6.addr}/${ipv6.bits}`; + else + return `& ${ipv6.mask} == ${ipv6.addr}`; + } + + function resolve_mark(str) { + if (isEmpty(str)) + return null; + + let mark = fw4.parse_mark(str); + if (isEmpty(mark)) + return null; + + if (mark.mask === 0xffffffff) + return fw4.hex(mark.mark); + else if (mark.mark === 0) + return `mark and ${fw4.hex(~mark.mask & 0xffffffff)}`; + else if (mark.mark === mark.mask) + return `mark or ${fw4.hex(mark.mark)}`; + else if (mark.mask === 0) + return `mark xor ${fw4.hex(mark.mark)}`; + else + return `mark and ${fw4.hex(~mark.mask & 0xffffffff)} xor ${fw4.hex(mark.mark)}`; + } + + /* Misc config */ + const resources_dir = '/etc/fchomo/resources'; + + /* UCI config start */ + const cfgname = 'fchomo'; + const uci = cursor(); + uci.load(cfgname); + + const common_tcpport = uci.get(cfgname, 'config', 'common_tcpport') || '20-21,22,53,80,110,143,443,465,853,873,993,995,8080,8443,9418', + common_udpport = uci.get(cfgname, 'config', 'common_udpport') || '20-21,22,53,80,110,143,443,853,993,995,8080,8443,9418', + stun_port = uci.get(cfgname, 'config', 'stun_port') || '3478,19302', + tun_name = uci.get(cfgname, 'config', 'tun_name') || 'hmtun0', + self_mark = uci.get(cfgname, 'config', 'self_mark') || '200', + tproxy_mark = resolve_mark(uci.get(cfgname, 'config', 'tproxy_mark') || '201'), + tun_mark = resolve_mark(uci.get(cfgname, 'config', 'tun_mark') || '202'); + + const redir_port = uci.get(cfgname, 'inbound', 'redir_port') || '7891', + tproxy_port = uci.get(cfgname, 'inbound', 'tproxy_port') || '7892', + tunnel_port = uci.get(cfgname, 'inbound', 'tunnel_port') || '7893', + proxy_mode = uci.get(cfgname, 'inbound', 'proxy_mode') || 'redir_tproxy'; + + const global_ipv6 = uci.get(cfgname, 'global', 'ipv6') || '1', + dns_ipv6 = uci.get(cfgname, 'dns', 'ipv6') || '1', + dns_port = uci.get(cfgname, 'dns', 'dns_port') || '7853'; + + const dnsmasq_hijacked = uci.get('dhcp', '@dnsmasq[0]', 'dns_redirect') || '0', + dnsmasq_port = uci.get('dhcp', '@dnsmasq[0]', 'port') || '53'; + + let client_enabled, routing_tcpport, routing_udpport, routing_mode, routing_domain; + client_enabled = uci.get(cfgname, 'routing', 'client_enabled') || '0', + routing_tcpport = uci.get(cfgname, 'routing', 'routing_tcpport') || null; + routing_udpport = uci.get(cfgname, 'routing', 'routing_udpport') || null; + routing_mode = uci.get(cfgname, 'routing', 'routing_mode') || null; + routing_domain = uci.get(cfgname, 'routing', 'routing_domain') || '0'; + + if (routing_tcpport === 'common') + routing_tcpport = common_tcpport; + else if (routing_tcpport === 'common_stun') + routing_tcpport = `${common_tcpport},${stun_port}`; + + if (routing_udpport === 'common') + routing_udpport = common_udpport; + else if (routing_udpport === 'common_stun') + routing_udpport = `${common_udpport},${stun_port}`; + + if (!routing_mode) + routing_domain = '0'; + + const proxy_router = uci.get(cfgname, 'routing', 'proxy_router') || '1'; + const control_options = [ + "listen_interfaces", "lan_filter", + "lan_direct_mac_addrs", "lan_direct_ipv4_ips", "lan_direct_ipv6_ips", + "lan_proxy_mac_addrs", "lan_proxy_ipv4_ips", "lan_proxy_ipv6_ips" + ]; + const control_info = {}; + + for (let i in control_options) + control_info[i] = uci.get(cfgname, 'routing', i); + + control_info.wan_direct_ipv4_ips = json(trim(yqRead('-oj', '.IPCIDR', resources_dir + '/direct_list.yaml')) || '[]'); + control_info.wan_direct_ipv6_ips = json(trim(yqRead('-oj', '.IPCIDR6', resources_dir + '/direct_list.yaml')) || '[]'); + control_info.wan_proxy_ipv4_ips = json(trim(yqRead('-oj', '.IPCIDR', resources_dir + '/proxy_list.yaml')) || '[]'); + control_info.wan_proxy_ipv6_ips = json(trim(yqRead('-oj', '.IPCIDR6', resources_dir + '/proxy_list.yaml')) || '[]'); + /* UCI config end */ +-%} + +{# Common function START #} +{%- function render_acl_src(inchain, outchain): %} +chain {{ inchain }} { + {% if (control_info.listen_interfaces): %} + meta iifname != {{ array_to_nftarr([...control_info.listen_interfaces, ...filter(['lo'], ((l) => !~index(control_info.listen_interfaces, l)))]) }} counter return + {% endif %} + meta mark {{ self_mark }} counter return + + {% if (control_info.lan_filter === 'white_list'): %} + {% if (!isEmpty(control_info.lan_proxy_ipv4_ips)): %} + ip saddr {{ array_to_nftarr(control_info.lan_proxy_ipv4_ips) }} counter goto {{ outchain }} + {% endif %} + {% for (let ipv6 in control_info.lan_proxy_ipv6_ips): %} + ip6 saddr {{ resolve_ipv6(ipv6) }} counter goto {{ outchain }} + {% endfor %} + {% if (!isEmpty(control_info.lan_proxy_mac_addrs)): %} + ether saddr {{ array_to_nftarr(control_info.lan_proxy_mac_addrs) }} counter goto {{ outchain }} + {% endif %} + {% elif (control_info.lan_filter === 'black_list'): %} + {% if (!isEmpty(control_info.lan_direct_ipv4_ips)): %} + ip saddr {{ array_to_nftarr(control_info.lan_direct_ipv4_ips) }} counter return + {% endif %} + {% for (let ipv6 in control_info.lan_direct_ipv6_ips): %} + ip6 saddr {{ resolve_ipv6(ipv6) }} counter return + {% endfor %} + {% if (!isEmpty(control_info.lan_direct_mac_addrs)): %} + ether saddr {{ array_to_nftarr(control_info.lan_direct_mac_addrs) }} counter return + {% endif %} + {% endif /* lan_filter */ %} + + {% if (control_info.lan_filter !== 'white_list'): %} + counter goto {{ outchain }} + {% endif %} + } +{% endfunction %} + +{%- function render_acl_dst(inchain, outchain): %} +chain {{ inchain }} { + meta mark {{ self_mark }} counter return + fib daddr type { local } counter return + ct direction reply counter return + + ip daddr @inet4_local_addr counter return + {% if (global_ipv6 === '1'): %} + ip6 daddr @inet6_local_addr counter return + {% endif %} + + ip daddr @inet4_wan_proxy_addr counter goto {{ outchain }} + {% if (global_ipv6 === '1'): %} + ip6 daddr @inet6_wan_proxy_addr counter goto {{ outchain }} + {% endif %} + + ip daddr @inet4_wan_direct_addr counter return + {% if (global_ipv6 === '1'): %} + ip6 daddr @inet6_wan_direct_addr counter return + {% endif %} + + {% if (routing_mode === 'routing_gfw'): %} + ip daddr != @inet4_gfw_list_addr counter return + {% if (global_ipv6 === '1'): %} + ip6 daddr != @inet6_gfw_list_addr counter return + {% endif %} + {% elif (routing_mode === 'bypass_cn'): %} + ip daddr @inet4_china_list_addr counter return + {% if (global_ipv6 === '1'): %} + ip6 daddr @inet6_china_list_addr counter return + {% endif %} + {% endif /* routing_mode */ %} + + counter goto {{ outchain }} + } +{% endfunction %} + +{%- function render_acl_dport(inchain, outchain, l4proto): %} +chain {{ inchain }} { + {#- DNS hijack #} + meta l4proto { tcp, udp } th dport 53 counter goto {{ outchain }} comment "!{{ cfgname }}: DNS hijack" + + {% if ((l4proto === 'tcp' || !l4proto) && routing_tcpport): %} + tcp dport != @tcp_routing_port counter return + {% endif %} + {% if ((l4proto === 'udp' || !l4proto) && routing_udpport): %} + udp dport != @udp_routing_port counter return + {% endif %} + + counter goto {{ outchain }} + } +{% endfunction %} +{# Common function END -#} + +table inet fchomo { + {#- Reserved addresses #} + set inet4_local_addr { + type ipv4_addr + flags interval + auto-merge + elements = { + 0.0.0.0/8, + 10.0.0.0/8, + 100.64.0.0/10, + 127.0.0.0/8, + 169.254.0.0/16, + 172.16.0.0/12, + {# 172.25.26.0/30, https://github.com/muink/openwrt-alwaysonline.git -#} + 192.0.0.0/24, + 192.0.2.0/24, + 192.88.99.0/24, + 192.168.0.0/16, + 198.18.0.0/15, + 198.51.100.0/24, + 203.0.113.0/24, + 224.0.0.0/4, + 240.0.0.0/4, + 255.255.255.255/32 + } + } + + {% if (global_ipv6 === '1'): %} + set inet6_local_addr { + type ipv6_addr + flags interval + auto-merge + elements = { + ::/128, + ::1/128, + ::ffff:0:0/96, + ::ffff:0:0:0/96, + 64:ff9b::/96, + 64:ff9b:1::/48, + 100::/64, + 2001::/32, + 2001:10::/28, + 2001:20::/28, + 2001:db8::/32, + 2002::/16, + 3fff::/20, + 5f00::/16, + fc00::/7, + {# fdfe:aead:2526::0/126, https://github.com/muink/openwrt-alwaysonline.git -#} + fe80::/10, + ff00::/8 + } + } + {% endif %} + + {#- Custom Direct list #} + set inet4_wan_direct_addr { + type ipv4_addr + flags interval + auto-merge + elements = { {{ join(', ', control_info.wan_direct_ipv4_ips) }} } + } + + {% if (global_ipv6 === '1'): %} + set inet6_wan_direct_addr { + type ipv6_addr + flags interval + auto-merge + elements = { {{ join(', ', control_info.wan_direct_ipv6_ips) }} } + } + {% endif %} + + {#- Custom Proxy list #} + set inet4_wan_proxy_addr { + type ipv4_addr + flags interval + auto-merge + elements = { {{ join(', ', control_info.wan_proxy_ipv4_ips) }} } + } + + {% if (global_ipv6 === '1'): %} + set inet6_wan_proxy_addr { + type ipv6_addr + flags interval + auto-merge + elements = { {{ join(', ', control_info.wan_proxy_ipv6_ips) }} } + } + {% endif %} + + {#- Routing mode #} + {% if (match(routing_mode, /bypass_cn/)): %} + set inet4_china_list_addr { + type ipv4_addr + flags interval + auto-merge + elements = { {{ join(', ', split(trim(readfile(resources_dir + '/china_ip4.txt')), /[\r\n]/)) }} } + } + + {% if (global_ipv6 === '1'): %} + set inet6_china_list_addr { + type ipv6_addr + flags interval + auto-merge + elements = { {{ join(', ', split(trim(readfile(resources_dir + '/china_ip6.txt')), /[\r\n]/)) }} } + } + {% endif %} + {% elif (match(routing_mode, /routing_gfw/)): %} + set inet4_gfw_list_addr { + type ipv4_addr + flags interval + auto-merge + elements = {} + } + + {% if (global_ipv6 === '1'): %} + set inet6_gfw_list_addr { + type ipv6_addr + flags interval + auto-merge + elements = {} + } + {% endif %} + {% endif /* routing_mode */ %} + + {#- Routing port #} + {% if (routing_tcpport): %} + set tcp_routing_port { + type inet_service + flags interval + auto-merge + elements = { {{ join(', ', split(routing_tcpport, ',')) }} } + } + {% endif %} + + {% if (routing_udpport): %} + set udp_routing_port { + type inet_service + flags interval + auto-merge + elements = { {{ join(', ', split(routing_udpport, ',')) }} } + } + {% endif %} + + {# Main entrypoint START #} + {# https://en.wikipedia.org/wiki/Netfilter#/media/File:Netfilter-packet-flow.svg #} + chain dstnat { + type nat hook prerouting priority dstnat + 5; policy accept; + {#- DNS hijack #} + {% if (dnsmasq_hijacked !== '1'): %} + {% if (control_info.listen_interfaces): %} + meta iifname {{ array_to_nftarr(control_info.listen_interfaces) }} + {% endif %} + meta iifname != lo meta nfproto { {{ (global_ipv6 === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { tcp, udp } th dport 53 counter redirect to :{{ dnsmasq_port }} comment "!{{ cfgname }}: DNS hijack (subnet)" + {% endif /* dnsmasq_hijacked */ %} + {#- TCP redirect entrypoint #} + {% if (match(proxy_mode, /redir/)): %} + meta nfproto { {{ (global_ipv6 === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump redir_acl_src + {% endif %} + } + + chain prerouting { + type filter hook prerouting priority 5; policy accept; + {#- DNS hijack #} + fib daddr type local meta nfproto { {{ (global_ipv6 === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { tcp, udp } th dport { {{ join(', ', [dnsmasq_port, dns_port, tunnel_port]) }} } counter accept comment "!{{ cfgname }}: DNS hijack (bypass local dnsserver)" + {#- UDP tproxy entrypoint #} + {% if (match(proxy_mode, /tproxy/)): %} + meta nfproto { {{ (global_ipv6 === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump tproxy_acl_src + {#- TUN entrypoint #} + {% elif (match(proxy_mode, /tun/)): %} + iifname {{ tun_name }} counter accept + meta nfproto { {{ (global_ipv6 === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { {{ (proxy_mode === 'tun') ? 'tcp, udp' : 'udp' }} } jump tun_acl_src + {% endif %} + } + + {% if (proxy_router === '1'): %} + chain mangle_output { + type route hook output priority 0; policy accept; + {#- UDP tproxy entrypoint #} + {% if (match(proxy_mode, /tproxy/)): %} + meta nfproto { {{ (global_ipv6 === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump tproxy_acl_dst_reroute + {#- TUN entrypoint #} + {% elif (match(proxy_mode, /tun/)): %} + iifname {{ tun_name }} counter accept + meta nfproto { {{ (global_ipv6 === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { {{ (proxy_mode === 'tun') ? 'tcp, udp' : 'udp' }} } jump tun_acl_dst + {% endif %} + } + + {#- TCP redirect entrypoint #} + {% if (match(proxy_mode, /redir/)): %} + chain nat_output { + type nat hook output priority 0; policy accept; + meta nfproto { {{ (global_ipv6 === '1') ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump redir_acl_dst + } + {% endif %} + {% endif /* proxy_router */ %} + {# Main entrypoint END #} + + {# TCP redirect START #} + {% if (match(proxy_mode, /redir/)): %} + {{ render_acl_src('redir_acl_src', 'redir_acl_dst') }} + {{ render_acl_dst('redir_acl_dst', 'redir_acl_dport') }} + {{ render_acl_dport('redir_acl_dport', 'redir_gate', 'tcp') }} + chain redir_gate { + {#- DNS hijack #} + tcp dport 53 counter redirect to :{{ tunnel_port }} comment "!{{ cfgname }}: DNS hijack (router tcp)" + + meta l4proto tcp counter redirect to :{{ redir_port }} + } + {% endif /* proxy_mode */ %} + {# TCP redirect END #} + + {# UDP tproxy START #} + {% if (match(proxy_mode, /tproxy/)): %} + {{ render_acl_src('tproxy_acl_src', 'tproxy_acl_dst') }} + {{ render_acl_dst('tproxy_acl_dst', 'tproxy_acl_dport') }} + {{ render_acl_dport('tproxy_acl_dport', 'tproxy_gate', 'udp') }} + chain tproxy_gate { + meta l4proto udp meta mark set {{ tproxy_mark }} tproxy ip to :{{ tproxy_port }} counter accept + {% if (global_ipv6 === '1'): %} + meta l4proto udp meta mark set {{ tproxy_mark }} tproxy ip6 to :{{ tproxy_port }} counter accept + {% endif %} + } + + {% if (proxy_router === '1'): %} + {{ render_acl_dst('tproxy_acl_dst_reroute', 'tproxy_acl_dport_reroute') }} + {{ render_acl_dport('tproxy_acl_dport_reroute', 'tproxy_mark', 'udp') }} + chain tproxy_mark { + {#- DNS hijack (router udp) #} + {# tproxy_mark --> route_table_id --reroute-to--> lo --> prerouting #} + + meta l4proto udp meta mark set {{ tproxy_mark }} counter accept + } + {% endif /* proxy_router */ %} + {% endif /* proxy_mode */ %} + {# UDP tproxy END #} + + {# TUN START #} + {% if (match(proxy_mode, /tun/)): %} + {{ render_acl_src('tun_acl_src', 'tun_acl_dst') }} + {{ render_acl_dst('tun_acl_dst', 'tun_acl_dport') }} + {{ render_acl_dport('tun_acl_dport', 'tun_mark', (proxy_mode === 'tun') ? '' : 'udp') }} + chain tun_mark { + meta mark set {{ tun_mark }} counter accept + } + {% endif /* proxy_mode */ %} + {# TUN END #} +} diff --git a/luci-app-fchomo/root/etc/fchomo/scripts/firewall_pre.ut b/luci-app-fchomo/root/etc/fchomo/scripts/firewall_pre.ut new file mode 100755 index 0000000000..f4857d7343 --- /dev/null +++ b/luci-app-fchomo/root/etc/fchomo/scripts/firewall_pre.ut @@ -0,0 +1,30 @@ +#!/usr/bin/utpl -S + +{%- + import { cursor } from 'uci'; + const uci = cursor(); + + const cfgname = 'fchomo'; + uci.load(cfgname); + + const proxy_mode = uci.get(cfgname, 'inbound', 'proxy_mode') || 'redir_tproxy'; + let client_enabled, tun_name; + if (match(proxy_mode, /tun/)) { + client_enabled = uci.get(cfgname, 'routing', 'client_enabled') || '0'; + + if (client_enabled === '1') + tun_name = uci.get(cfgname, 'config', 'tun_name') || 'hmtun0'; + } +-%} + +{% if (tun_name): %} +chain forward { + iifname {{ tun_name }} counter accept comment "!{{ cfgname }}: accept tun forward" + oifname {{ tun_name }} counter accept comment "!{{ cfgname }}: accept tun forward" +} + +chain input { + iifname {{ tun_name }} counter accept comment "!{{ cfgname }}: accept tun input" + oifname {{ tun_name }} counter accept comment "!{{ cfgname }}: accept tun input" +} +{% endif %} diff --git a/luci-app-fchomo/root/etc/fchomo/scripts/generate_client.uc b/luci-app-fchomo/root/etc/fchomo/scripts/generate_client.uc new file mode 100755 index 0000000000..c088be9eb7 --- /dev/null +++ b/luci-app-fchomo/root/etc/fchomo/scripts/generate_client.uc @@ -0,0 +1,766 @@ +#!/usr/bin/ucode + +'use strict'; + +import { readfile, writefile } from 'fs'; +import { connect } from 'ubus'; +import { cursor } from 'uci'; + +import { urldecode, urlencode } from 'luci.http'; + +import { + isEmpty, strToBool, strToInt, bytesizeToByte, durationToSecond, + arrToObj, removeBlankAttrs, + HM_DIR, RUN_DIR, PRESET_OUTBOUND +} from 'fchomo'; + +const ubus = connect(); + +/* UCI config START */ +const uci = cursor(); + +const uciconf = 'fchomo'; +uci.load(uciconf); + +const ucifchm = 'config', + ucires = 'resources', + uciroute = 'routing'; + +const uciglobal = 'global', + uciinbound = 'inbound', + ucitls = 'tls', + uciapi = 'api', + ucisniffer = 'sniffer', + ucidns = 'dns', + uciexpr = 'experimental'; + +const ucisniff = 'sniff', + ucidnser = 'dns_server', + ucidnspoli = 'dns_policy', + ucipgrp = 'proxy_group', + ucinode = 'node', + uciprov = 'provider', + ucichain = 'dialer_proxy', + ucirule = 'ruleset', + ucirout = 'rules', + ucisubro = 'subrules'; + +/* Hardcode options */ +const common_tcpport = uci.get(uciconf, ucifchm, 'common_tcpport') || '20-21,22,53,80,110,143,443,465,853,873,993,995,8080,8443,9418', + common_udpport = uci.get(uciconf, ucifchm, 'common_udpport') || '20-21,22,53,80,110,143,443,853,993,995,8080,8443,9418', + stun_port = uci.get(uciconf, ucifchm, 'stun_port') || '3478,19302', + tun_name = uci.get(uciconf, ucifchm, 'tun_name') || 'hmtun0', + tun_addr4 = uci.get(uciconf, ucifchm, 'tun_addr4') || '198.19.0.1/30', + tun_addr6 = uci.get(uciconf, ucifchm, 'tun_addr6') || 'fdfe:dcba:9877::1/126', + route_table_id = strToInt(uci.get(uciconf, ucifchm, 'route_table_id')) || 2022, // global.js + route_rule_pref = strToInt(uci.get(uciconf, ucifchm, 'route_rule_pref')) || 9000, // global.js + redirect_gate_mark = strToInt(uci.get(uciconf, ucifchm, 'redirect_gate_mark')) || 2023, + redirect_pass_mark = strToInt(uci.get(uciconf, ucifchm, 'redirect_pass_mark')) || 2024, + self_mark = strToInt(uci.get(uciconf, ucifchm, 'self_mark')) || 200, // global.js + tproxy_mark = strToInt(uci.get(uciconf, ucifchm, 'tproxy_mark')) || 201, // global.js + tun_mark = strToInt(uci.get(uciconf, ucifchm, 'tun_mark')) || 202, // global.js + posh = 'c2luZ2JveA'; // Yes. Is true. + +const listen_interfaces = uci.get(uciconf, uciroute, 'listen_interfaces') || null, + bind_interface = uci.get(uciconf, uciroute, 'bind_interface') || null, + lan_filter = uci.get(uciconf, uciroute, 'lan_filter') || null, + lan_direct_ipv4_ips = uci.get(uciconf, uciroute, 'lan_direct_ipv4_ips') || null, + lan_direct_ipv6_ips = uci.get(uciconf, uciroute, 'lan_direct_ipv6_ips') || null, + lan_direct_mac_addrs = uci.get(uciconf, uciroute, 'lan_direct_mac_addrs') || null, + lan_proxy_ipv4_ips = uci.get(uciconf, uciroute, 'lan_proxy_ipv4_ips') || null, + lan_proxy_ipv6_ips = uci.get(uciconf, uciroute, 'lan_proxy_ipv6_ips') || null, + lan_proxy_mac_addrs = uci.get(uciconf, uciroute, 'lan_proxy_mac_addrs') || null, + proxy_router = (uci.get(uciconf, uciroute, 'proxy_router') === '0') ? null : true, + client_enabled = uci.get(uciconf, uciroute, 'client_enabled') || '0', + routing_tcpport = uci.get(uciconf, uciroute, 'routing_tcpport') || null, + routing_udpport = uci.get(uciconf, uciroute, 'routing_udpport') || null, + routing_mode = uci.get(uciconf, uciroute, 'routing_mode') || null, + routing_domain = strToBool(uci.get(uciconf, uciroute, 'routing_domain')), + tposh = 'c2luZ2JveA'; + +/* WAN DNS server array */ +let wan_dns = ubus.call('network.interface', 'status', {'interface': 'wan'})?.['dns-server']; +if (length(wan_dns) === 0) + wan_dns = ['223.5.5.5']; + +/* All DNS server object */ +const dnsservers = {}; +uci.foreach(uciconf, ucidnser, (cfg) => { + if (cfg.enabled === '0') + return; + + dnsservers[cfg['.name']] = { + label: cfg.label, + address: cfg.address + }; +}); + +/* UCI config END */ + +/* Config helper START */ +function parse_filter(cfg) { + if (isEmpty(cfg)) + return null; + + if (type(cfg) === 'array') + return join('|', cfg); + else + return cfg; +} + +function get_proxynode(cfg) { + if (isEmpty(cfg)) + return null; + + const label = uci.get(uciconf, cfg, 'label'); + if (isEmpty(label)) + die(sprintf("%s's label is missing, please check your configuration.", cfg)); + else + return label; +} + +function get_proxygroup(cfg) { + if (isEmpty(cfg)) + return null; + + if (cfg in PRESET_OUTBOUND) + return cfg; + + const label = uci.get(uciconf, cfg, 'label'); + if (isEmpty(label)) + die(sprintf("%s's label is missing, please check your configuration.", cfg)); + else + return label; +} + +function get_nameserver(cfg, detour) { + if (isEmpty(cfg)) + return []; + + if ('block-dns' in cfg) + //https://github.com/MetaCubeX/mihomo/blob/0128a0bb1fce17d39158c745a912d7b2b87cf975/config/config.go#L1131 + return 'rcode://name_error'; + + let servers = []; + for (let k in cfg) { + if (k === 'system-dns') { + push(servers, 'system'); + } else if (k === 'default-dns') { + map(wan_dns, (dns) => { + push(servers, dns + '#DIRECT'); + }); + } else + push(servers, replace(dnsservers[k]?.address || '', /#detour=([^&]+)/, (m, c1) => { + return '#' + urlencode(get_proxygroup(detour || c1)); + })); + } + + return servers; +} + +function parse_entry(cfg) { + if (isEmpty(cfg)) + return null; + + let arr = split(cfg, ','); + if (arr[0] === 'MATCH') { + arr[1] = get_proxygroup(arr[1]); + } else if (arr[0] === 'SUB-RULE') { + arr[1] = replace(arr[1], /ꓹ|‚/g, ','); // U+A4F9 | U+201A + arr[2] = replace(arr[2], /ꓹ|‚/g, ','); // U+A4F9 | U+201A + } else { + arr[1] = replace(arr[1], /ꓹ|‚/g, ','); // U+A4F9 | U+201A + arr[2] = get_proxygroup(arr[2]); + } + + return join(',', arr); +} +/* Config helper END */ + +/* Main */ +const config = {}; + +/* All Proxy chain object */ +const dialerproxy = {}; +uci.foreach(uciconf, ucichain, (cfg) => { + if (cfg.enabled === '0') + return; + + let identifier = ''; + if (cfg.type === 'provider') + identifier = cfg.chain_head_sub; + else if (cfg.type === 'node') + identifier = cfg.chain_head; + else + return; + + dialerproxy[identifier] = { + detour: get_proxygroup(cfg.chain_tail_group) || get_proxynode(cfg.chain_tail) + }; +}); + +/* General START */ +/* General settings */ +config["global-ua"] = 'clash.meta'; +config.mode = uci.get(uciconf, uciglobal, 'mode') || 'rule'; +config["find-process-mode"] = uci.get(uciconf, uciglobal, 'find_process_mode') || 'off'; +config["log-level"] = uci.get(uciconf, uciglobal, 'log_level') || 'warning'; +config["etag-support"] = (uci.get(uciconf, uciglobal, 'etag_support') === '0') ? false : true; +config.ipv6 = (uci.get(uciconf, uciglobal, 'ipv6') === '0') ? false : true; +config["unified-delay"] = strToBool(uci.get(uciconf, uciglobal, 'unified_delay')) || false; +config["tcp-concurrent"] = strToBool(uci.get(uciconf, uciglobal, 'tcp_concurrent')) || false; +config["keep-alive-interval"] = durationToSecond(uci.get(uciconf, uciglobal, 'keep_alive_interval')) || 30; +config["keep-alive-idle"] = durationToSecond(uci.get(uciconf, uciglobal, 'keep_alive_idle')) || 600; +/* ACL settings */ +config["interface-name"] = bind_interface; +config["routing-mark"] = self_mark; +/* Global Authentication */ +config.authentication = uci.get(uciconf, uciglobal, 'authentication'); +config["skip-auth-prefixes"] = uci.get(uciconf, uciglobal, 'skip_auth_prefixes'); +/* General END */ + +/* GEOX START */ +/* GEOX settings */ +config["geodata-mode"] = true; +config["geodata-loader"] = 'memconservative'; +config["geo-auto-update"] = false; +/* GEOX END */ + +/* TLS START */ +/* TLS settings */ +config["global-client-fingerprint"] = uci.get(uciconf, ucitls, 'global_client_fingerprint'); +config.tls = { + "certificate": uci.get(uciconf, ucitls, 'tls_cert_path'), + "private-key": uci.get(uciconf, ucitls, 'tls_key_path') +}; +/* TLS END */ + +/* API START */ +const api_port = uci.get(uciconf, uciapi, 'external_controller_port'); +const api_tls_port = uci.get(uciconf, uciapi, 'external_controller_tls_port'); +/* API settings */ +config["external-controller-cors"] = { + "allow-origins": uci.get(uciconf, uciapi, 'external_controller_cors_allow_origins') || ['*'], + "allow-private-network" : (uci.get(uciconf, uciapi, 'external_controller_cors_allow_private_network') === '0') ? false : true +}; +config["external-controller"] = api_port ? '[::]:' + api_port : null; +config["external-controller-tls"] = api_tls_port ? '[::]:' + api_tls_port : null; +config["external-doh-server"] = uci.get(uciconf, uciapi, 'external_doh_server'); +config.secret = uci.get(uciconf, uciapi, 'secret') || trim(readfile('/proc/sys/kernel/random/uuid')); +config["external-ui"] = RUN_DIR + '/ui'; +/* API END */ + +/* Cache START */ +/* Cache settings */ +config.profile = { + "store-selected": true, + "store-fake-ip": false +}; +/* Cache END */ + +/* Experimental START */ +/* Experimental settings */ +config.experimental = { + "quic-go-disable-gso": strToBool(uci.get(uciconf, uciexpr, 'quic_go_disable_gso')), + "quic-go-disable-ecn": strToBool(uci.get(uciconf, uciexpr, 'quic_go_disable_ecn')), + "dialer-ip4p-convert": strToBool(uci.get(uciconf, uciexpr, 'dialer_ip4p_convert')) +}; +/* Experimental END */ + +/* Sniffer START */ +/* Sniffer settings */ +config.sniffer = { + enable: true, + "force-dns-mapping": true, + "parse-pure-ip": true, + "override-destination": (uci.get(uciconf, ucisniffer, 'override_destination') === '0') ? false : true, + sniff: {}, + "force-domain": uci.get(uciconf, ucisniffer, 'force_domain'), + "skip-domain": uci.get(uciconf, ucisniffer, 'skip_domain'), + "skip-src-address": uci.get(uciconf, ucisniffer, 'skip_src_address'), + "skip-dst-address": uci.get(uciconf, ucisniffer, 'skip_dst_address') +}; +/* Sniff protocol settings */ +uci.foreach(uciconf, ucisniff, (cfg) => { + if (cfg.enabled === '0') + return null; + + config.sniffer.sniff[cfg.protocol] = { + ports: map(cfg.ports, (ports) => { + return strToInt(ports); // DEBUG ERROR data type *utils.IntRanges[uint16] + }), + "override-destination": (cfg.override_destination === '0') ? false : true + }; +}); +/* Sniffer END */ + +/* Inbound START */ +const proxy_mode = uci.get(uciconf, uciinbound, 'proxy_mode') || 'redir_tproxy'; +/* Listen ports */ +config.listeners = []; +push(config.listeners, { + name: 'mixed-in', + type: 'mixed', + port: strToInt(uci.get(uciconf, uciinbound, 'mixed_port')) || '7890', + listen: '::', + udp: true +}); +if (match(proxy_mode, /redir/)) + push(config.listeners, { + name: 'redir-in', + type: 'redir', + port: strToInt(uci.get(uciconf, uciinbound, 'redir_port')) || '7891', + listen: '::' + }); +if (match(proxy_mode, /tproxy/)) + push(config.listeners, { + name: 'tproxy-in', + type: 'tproxy', + port: strToInt(uci.get(uciconf, uciinbound, 'tproxy_port')) || '7892', + listen: '::', + udp: true + }); +push(config.listeners, { + name: 'dns-in', + type: 'tunnel', + port: strToInt(uci.get(uciconf, uciinbound, 'tunnel_port')) || '7893', + listen: '::', + network: ['tcp', 'udp'], + target: '1.1.1.1:53' +}); +/* Tun settings */ +if (match(proxy_mode, /tun/)) + push(config.listeners, { + name: 'tun-in', + type: 'tun', + + device: tun_name, + stack: uci.get(uciconf, uciinbound, 'tun_stack') || 'system', + "dns-hijack": ['udp://[::]:53', 'tcp://[::]:53'], + "inet4-address": [ tun_addr4 ], + "inet6-address": [ tun_addr6 ], + mtu: strToInt(uci.get(uciconf, uciinbound, 'tun_mtu')) || 9000, + gso: strToBool(uci.get(uciconf, uciinbound, 'tun_gso')) || false, + "gso-max-size": strToInt(uci.get(uciconf, uciinbound, 'tun_gso_max_size')) || 65536, + "auto-route": false, + "iproute2-table-index": route_table_id, + "iproute2-rule-index": route_rule_pref, + "auto-redirect": false, + "auto-redirect-input-mark": redirect_gate_mark, + "auto-redirect-output-mark": redirect_pass_mark, + "strict-route": false, + "route-address": [ + "0.0.0.0/1", + "128.0.0.0/1", + "::/1", + "8000::/1" + ], + "route-exclude-address": [ + "192.168.0.0/16", + "fc00::/7" + ], + "route-address-set": [], + "route-exclude-address-set": [], + "include-interface": [], + "exclude-interface": [], + "udp-timeout": durationToSecond(uci.get(uciconf, uciinbound, 'tun_udp_timeout')) || 300, + "endpoint-independent-nat": strToBool(uci.get(uciconf, uciinbound, 'tun_endpoint_independent_nat')), + "auto-detect-interface": true + }); +/* Inbound END */ + +/* DNS START */ +/* DNS settings */ +config.dns = { + enable: true, + "prefer-h3": false, + listen: '[::]:' + (uci.get(uciconf, ucidns, 'port') || '7853'), + ipv6: (uci.get(uciconf, ucidns, 'ipv6') === '0') ? false : true, + "enhanced-mode": 'redir-host', + "use-hosts": true, + "use-system-hosts": true, + "respect-rules": true, + "default-nameserver": get_nameserver(uci.get(uciconf, ucidns, 'boot_server')), + "proxy-server-nameserver": get_nameserver(uci.get(uciconf, ucidns, 'bootnode_server')), + nameserver: get_nameserver(uci.get(uciconf, ucidns, 'default_server')), + fallback: get_nameserver(uci.get(uciconf, ucidns, 'fallback_server')), + "nameserver-policy": {}, + "fallback-filter": { + geoip: false + } +}; +/* DNS policy */ +uci.foreach(uciconf, ucidnspoli, (cfg) => { + if (cfg.enabled === '0') + return null; + + let key; + if (cfg.type === 'domain') { + key = isEmpty(cfg.domain) ? null : join(',', cfg.domain); + } else if (cfg.type === 'geosite') { + key = isEmpty(cfg.geosite) ? null : 'geosite:' + join(',', cfg.geosite); + } else if (cfg.type === 'rule_set') { + key = isEmpty(cfg.rule_set) ? null : 'rule-set:' + join(',', cfg.rule_set); + }; + + if (!key) + return null; + + config.dns["nameserver-policy"][key] = get_nameserver(cfg.server, cfg.proxy); +}); +/* Fallback filter */ +if (!isEmpty(config.dns.fallback)) + config.dns["fallback-filter"] = { + geoip: (uci.get(uciconf, ucidns, 'fallback_filter_geoip') === '0') ? false : true, + "geoip-code": uci.get(uciconf, ucidns, 'fallback_filter_geoip_code') || 'cn', + geosite: uci.get(uciconf, ucidns, 'fallback_filter_geosite') || [], + ipcidr: uci.get(uciconf, ucidns, 'fallback_filter_ipcidr') || [], + domain: uci.get(uciconf, ucidns, 'fallback_filter_domain') || [], + }; +/* DNS END */ + +/* Hosts START */ +/* Hosts */ +config.hosts = {}; +/* Hosts END */ + +/* Proxy Node START */ +/* Proxy Node */ +config.proxies = [ + /*{ + name: 'direct-out', + type: 'direct', + udp: true, + "ip-version": undefined, + "interface-name": undefined, + "routing-mark": undefined + },*/ + { + name: 'dns-out', + type: 'dns' + } +]; +uci.foreach(uciconf, ucinode, (cfg) => { + if (cfg.enabled === '0') + return null; + + push(config.proxies, { + name: cfg.label, + type: cfg.type, + + server: cfg.server, + port: strToInt(cfg.port), + + /* Dial fields */ + tfo: strToBool(cfg.tfo), + mptcp: strToBool(cfg.mptcp), + "dialer-proxy": dialerproxy[cfg['.name']]?.detour, + "interface-name": cfg.interface_name, + "routing-mark": strToInt(cfg.routing_mark), + "ip-version": cfg.ip_version, + + /* HTTP / SOCKS / Shadowsocks / VMess / VLESS / Trojan / hysteria2 / TUIC / SSH / WireGuard */ + username: cfg.username, + uuid: cfg.vmess_uuid || cfg.uuid, + cipher: cfg.vmess_chipher || cfg.shadowsocks_chipher, + password: cfg.shadowsocks_password || cfg.password, + headers: cfg.headers ? json(cfg.headers) : null, + "private-key": cfg.wireguard_private_key || cfg.ssh_priv_key, + + /* Hysteria / Hysteria2 */ + ports: isEmpty(cfg.hysteria_ports) ? null : join(',', cfg.hysteria_ports), + up: cfg.hysteria_up_mbps ? cfg.hysteria_up_mbps + ' Mbps' : null, + down: cfg.hysteria_down_mbps ? cfg.hysteria_down_mbps + ' Mbps' : null, + obfs: cfg.hysteria_obfs_type, + "obfs-password": cfg.hysteria_obfs_password, + + /* SSH */ + "private-key-passphrase": cfg.ssh_priv_key_passphrase, + "host-key-algorithms": cfg.ssh_host_key_algorithms, + "host-key": cfg.ssh_host_key, + + /* Shadowsocks */ + + /* Mieru */ + "port-range": cfg.mieru_port_range, + transport: cfg.mieru_transport, + multiplexing: cfg.mieru_multiplexing, + + /* Snell */ + psk: cfg.snell_psk, + version: cfg.snell_version, + "obfs-opts": cfg.type === 'snell' ? { + mode: cfg.plugin_opts_obfsmode, + host: cfg.plugin_opts_host, + } : null, + + /* TUIC */ + ip: cfg.tuic_ip, + "congestion-controller": cfg.tuic_congestion_controller, + "udp-relay-mode": cfg.tuic_udp_relay_mode, + "udp-over-stream": strToBool(cfg.tuic_udp_over_stream), + "udp-over-stream-version": cfg.tuic_udp_over_stream_version, + "max-udp-relay-packet-size": strToInt(cfg.tuic_max_udp_relay_packet_size), + "reduce-rtt": strToBool(cfg.tuic_reduce_rtt), + "heartbeat-interval": strToInt(cfg.tuic_heartbeat), + "request-timeout": strToInt(cfg.tuic_request_timeout), + // fast-open: true + // max-open-streams: 20 + + /* Trojan */ + "ss-opts": cfg.trojan_ss_enabled === '1' ? { + enabled: true, + method: cfg.trojan_ss_chipher, + password: cfg.trojan_ss_password + }: null, + + /* VMess / VLESS */ + flow: cfg.vless_flow, + alterId: strToInt(cfg.vmess_alterid), + "global-padding": cfg.type === 'vmess' ? (cfg.vmess_global_padding === '0' ? false : true) : null, + "authenticated-length": strToBool(cfg.vmess_authenticated_length), + "packet-encoding": cfg.vmess_packet_encoding, + + /* WireGuard */ + ip: cfg.wireguard_ip, + ipv6: cfg.wireguard_ipv6, + "public-key": cfg.wireguard_peer_public_key, + "pre-shared-key": cfg.wireguard_pre_shared_key, + "allowed-ips": cfg.wireguard_allowed_ips, + reserved: cfg.wireguard_reserved, + mtu: strToInt(cfg.wireguard_mtu), + "remote-dns-resolve": strToBool(cfg.wireguard_remote_dns_resolve), + dns: cfg.wireguard_dns, + + /* Plugin fields */ + plugin: cfg.plugin, + "plugin-opts": cfg.plugin ? { + mode: cfg.plugin_opts_obfsmode, + host: cfg.plugin_opts_host, + password: cfg.plugin_opts_thetlspassword, + version: cfg.plugin_opts_shadowtls_version, + "version-hint": cfg.plugin_opts_restls_versionhint, + "restls-script": cfg.plugin_opts_restls_script + } : null, + + /* Extra fields */ + udp: strToBool(cfg.udp), + "udp-over-tcp": strToBool(cfg.uot), + "udp-over-tcp-version": cfg.uot_version, + + /* TLS fields */ + tls: (cfg.type in ['trojan', 'hysteria', 'hysteria2', 'tuic']) ? null : strToBool(cfg.tls), + "disable-sni": strToBool(cfg.tls_disable_sni), + ...arrToObj([[(cfg.type in ['vmess', 'vless']) ? 'servername' : 'sni', cfg.tls_sni]]), + fingerprint: cfg.tls_fingerprint, + alpn: cfg.tls_alpn, // Array + "skip-cert-verify": strToBool(cfg.tls_skip_cert_verify), + "client-fingerprint": cfg.tls_client_fingerprint, + "reality-opts": cfg.tls_reality === '1' ? { + "public-key": cfg.tls_reality_public_key, + "short-id": cfg.tls_reality_short_id + } : null, + + /* Transport fields */ + // https://github.com/muink/mihomo/blob/3e966e82c793ca99e3badc84bf3f2907b100edae/adapter/outbound/vmess.go#L74 + ...(cfg.transport_enabled === '1' ? { + network: cfg.transport_type, + "http-opts": cfg.transport_type === 'http' ? { + method: cfg.transport_http_method, + path: isEmpty(cfg.transport_paths) ? ['/'] : cfg.transport_paths, // Array + headers: cfg.transport_http_headers ? json(cfg.transport_http_headers) : null, + } : null, + "h2-opts": cfg.transport_type === 'h2' ? { + host: cfg.transport_hosts, // Array + path: cfg.transport_path || '/', + } : null, + "grpc-opts": cfg.transport_type === 'grpc' ? { + "grpc-service-name": cfg.transport_grpc_servicename + } : null, + "ws-opts": cfg.transport_type === 'ws' ? { + path: cfg.transport_path || '/', + headers: cfg.transport_http_headers ? json(cfg.transport_http_headers) : null, + "max-early-data": strToInt(cfg.transport_ws_max_early_data), + "early-data-header-name": cfg.transport_ws_early_data_header, + "v2ray-http-upgrade": strToBool(cfg.transport_ws_v2ray_http_upgrade), + "v2ray-http-upgrade-fast-open": strToBool(cfg.transport_ws_v2ray_http_upgrade_fast_open) + } : null + } : {}), + + /* Multiplex fields */ + smux: cfg.smux_enabled === '1' ? { + enabled: true, + protocol: cfg.smux_protocol, + "max-connections": strToInt(cfg.smux_max_connections), + "min-streams": strToInt(cfg.smux_min_streams), + "max-streams": strToInt(cfg.smux_max_streams), + statistic: strToBool(cfg.smux_statistic), + "only-tcp": strToBool(cfg.smux_only_tcp), + padding: strToBool(cfg.smux_padding), + "brutal-opts": cfg.smux_brutal === '1' ? { + enabled: true, + up: strToInt(cfg.smux_brutal_up), // Mbps + down: strToInt(cfg.smux_brutal_down) // Mbps + } : null + } : null + }); +}); +/* Proxy Node END */ + +/* Proxy Group START */ +/* Proxy Group */ +config["proxy-groups"] = []; +uci.foreach(uciconf, ucipgrp, (cfg) => { + if (cfg.enabled === '0') + return null; + + push(config["proxy-groups"], { + name: cfg.label, + type: cfg.type, + proxies: [ + ...map(cfg.groups || [], cfg => get_proxygroup(cfg)), + ...map(cfg.proxies || [], cfg => get_proxynode(cfg)) + ], + use: cfg.use, + "include-all": strToBool(cfg.include_all), + "include-all-proxies": strToBool(cfg.include_all_proxies), + "include-all-providers": strToBool(cfg.include_all_providers), + // Url-test fields + tolerance: (cfg.type === 'url-test') ? strToInt(cfg.tolerance) || 150 : null, + // Load-balance fields + strategy: cfg.strategy, + // Override fields + "disable-udp": strToBool(cfg.disable_udp) || false, + "interface-name": cfg.interface_name, + "routing-mark": strToInt(cfg.routing_mark), + // Health fields + url: cfg.url, + interval: cfg.url ? durationToSecond(cfg.interval) || 600 : null, + timeout: cfg.url ? strToInt(cfg.timeout) || 5000 : null, + lazy: (cfg.lazy === '0') ? false : null, + "expected-status": cfg.url ? cfg.expected_status || '204' : null, + "max-failed-times": cfg.url ? strToInt(cfg.max_failed_times) || 5 : null, + filter: parse_filter(cfg.filter), + "exclude-filter": parse_filter(cfg.exclude_filter), + "exclude-type": parse_filter(cfg.exclude_type) + }); +}); +/* Proxy Group END */ + +/* Provider START */ +/* Provider settings */ +config["proxy-providers"] = {}; +uci.foreach(uciconf, uciprov, (cfg) => { + if (cfg.enabled === '0') + return null; + + /* General fields */ + config["proxy-providers"][cfg['.name']] = { + type: cfg.type, + ...(cfg.payload ? { + payload: trim(cfg.payload) + } : { + path: HM_DIR + '/provider/' + cfg['.name'] + }), + url: cfg.url, + "size-limit": bytesizeToByte(cfg.size_limit) || null, + interval: (cfg.type === 'http') ? durationToSecond(cfg.interval) || 86400 : null, + proxy: get_proxygroup(cfg.proxy), + header: cfg.header ? json(cfg.header) : null, + "health-check": {}, + override: {}, + filter: parse_filter(cfg.filter), + "exclude-filter": parse_filter(cfg.exclude_filter), + "exclude-type": parse_filter(cfg.exclude_type) + }; + + /* Override fields */ + config["proxy-providers"][cfg['.name']].override = { + "additional-prefix": cfg.override_prefix, + "additional-suffix": cfg.override_suffix, + "proxy-name": isEmpty(cfg.override_replace) ? null : map(cfg.override_replace, (obj) => json(obj)), + // Configuration Items + tfo: strToBool(cfg.override_tfo), + mptcp: strToBool(cfg.override_mptcp), + udp: (cfg.override_udp === '0') ? false : true, + "udp-over-tcp": strToBool(cfg.override_uot), + up: cfg.override_up ? cfg.override_up + ' Mbps' : null, + down: cfg.override_down ? cfg.override_down + ' Mbps' : null, + "skip-cert-verify": strToBool(cfg.override_skip_cert_verify) || false, + "dialer-proxy": dialerproxy[cfg['.name']]?.detour, + "interface-name": cfg.override_interface_name, + "routing-mark": strToInt(cfg.override_routing_mark), + "ip-version": cfg.override_ip_version + }; + + /* Health fields */ + if (cfg.health_enable === '0') { + config["proxy-providers"][cfg['.name']]["health-check"] = null; + } else { + config["proxy-providers"][cfg['.name']]["health-check"] = { + enable: true, + url: cfg.health_url, + interval: durationToSecond(cfg.health_interval) || 600, + timeout: strToInt(cfg.health_timeout) || 5000, + lazy: (cfg.health_lazy === '0') ? false : null, + "expected-status": cfg.health_expected_status || '204' + }; + } +}); +/* Provider END */ + +/* Rule set START */ +/* Rule set settings */ +config["rule-providers"] = {}; +uci.foreach(uciconf, ucirule, (cfg) => { + if (cfg.enabled === '0') + return null; + + config["rule-providers"][cfg['.name']] = { + type: cfg.type, + format: cfg.format, + behavior: cfg.behavior, + ...(cfg.payload ? { + payload: trim(cfg.payload) + } : { + path: HM_DIR + '/ruleset/' + cfg['.name'] + }), + url: cfg.url, + "size-limit": bytesizeToByte(cfg.size_limit) || null, + interval: (cfg.type === 'http') ? durationToSecond(cfg.interval) || 259200 : null, + proxy: get_proxygroup(cfg.proxy) + }; +}); +/* Rule set END */ + +/* Routing rules START */ +/* Routing rules */ +config.rules = [ + "IN-NAME,dns-in,dns-out", + "DST-PORT,53,dns-out" +]; +uci.foreach(uciconf, ucirout, (cfg) => { + if (cfg.enabled === '0') + return null; + + push(config.rules, parse_entry(cfg.entry)); +}); +/* Routing rules END */ + +/* Sub rules START */ +/* Sub rules */ +config["sub-rules"] = {}; +uci.foreach(uciconf, ucisubro, (cfg) => { + if (cfg.enabled === '0') + return null; + + if (!config["sub-rules"][cfg.group]) + config["sub-rules"][cfg.group] = []; + push(config["sub-rules"][cfg.group], parse_entry(cfg.entry)); +}); +/* Sub rules END */ + +/* Debug dialer-proxy */ +//config.dialerproxy = dialerproxy; + +printf('%.J\n', removeBlankAttrs(config)); diff --git a/luci-app-fchomo/root/etc/fchomo/scripts/generate_server.uc b/luci-app-fchomo/root/etc/fchomo/scripts/generate_server.uc new file mode 100755 index 0000000000..ecea392ab1 --- /dev/null +++ b/luci-app-fchomo/root/etc/fchomo/scripts/generate_server.uc @@ -0,0 +1,105 @@ +#!/usr/bin/ucode + +'use strict'; + +import { cursor } from 'uci'; + +import { + isEmpty, strToBool, strToInt, durationToSecond, + arrToObj, removeBlankAttrs, + HM_DIR, RUN_DIR, PRESET_OUTBOUND +} from 'fchomo'; + +/* UCI config START */ +const uci = cursor(); + +const uciconf = 'fchomo'; +uci.load(uciconf); + +const uciserver = 'server'; + +/* UCI config END */ + +/* Config helper START */ +function parse_users(cfg) { + if (isEmpty(cfg)) + return null; + + let uap, arr, users=[]; + for (uap in cfg) { + arr = split(uap, ':'); + users[arr[0]] = arr[1]; + } + + return users; +} +/* Config helper END */ + +/* Main */ +const config = {}; + +/* Inbound START */ +config.listeners = []; +uci.foreach(uciconf, uciserver, (cfg) => { + if (cfg.enabled === '0') + return; + + push(config.listeners, { + name: cfg['.name'], + type: cfg.type, + + listen: cfg.listen || '::', + port: strToInt(cfg.port), + proxy: 'DIRECT', + udp: strToBool(cfg.udp), + + /* Hysteria2 */ + up: strToInt(cfg.hysteria_up_mbps), + down: strToInt(cfg.hysteria_down_mbps), + "ignore-client-bandwidth": strToBool(cfg.hysteria_ignore_client_bandwidth), + obfs: cfg.hysteria_obfs_type, + "obfs-password": cfg.hysteria_obfs_password, + masquerade: cfg.hysteria_masquerade, + + /* Shadowsocks */ + cipher: cfg.shadowsocks_chipher, + password: cfg.shadowsocks_password, + + /* Tuic */ + "congestion-controller": cfg.tuic_congestion_controller, + "max-idle-time": durationToSecond(cfg.tuic_max_idle_time), + "authentication-timeout": durationToSecond(cfg.tuic_authentication_timeout), + "max-udp-relay-packet-size": strToInt(cfg.tuic_max_udp_relay_packet_size), + + /* HTTP / SOCKS / VMess / Tuic / Hysteria2 */ + users: (cfg.type in ['http', 'socks', 'mixed', 'vmess']) ? [ + { + /* HTTP / SOCKS */ + username: cfg.username, + password: cfg.password, + + /* VMess */ + uuid: cfg.vmess_uuid, + alterId: strToInt(cfg.vmess_alterid) + } + /*{ + }*/ + ] : ((cfg.type in ['tuic', 'hysteria2']) ? { + /* Hysteria2 */ + ...arrToObj([[cfg.username, cfg.password]]), + + /* Tuic */ + ...arrToObj([[cfg.uuid, cfg.password]]) + } : null), + + /* TLS */ + ...(cfg.tls === '1' ? { + alpn: cfg.tls_alpn, + certificate: cfg.tls_cert_path, + "private-key": cfg.tls_key_path + } : {}) + }); +}); +/* Inbound END */ + +printf('%.J\n', removeBlankAttrs(config)); diff --git a/luci-app-fchomo/root/etc/fchomo/scripts/update_resources.sh b/luci-app-fchomo/root/etc/fchomo/scripts/update_resources.sh new file mode 100755 index 0000000000..d46483a71d --- /dev/null +++ b/luci-app-fchomo/root/etc/fchomo/scripts/update_resources.sh @@ -0,0 +1,253 @@ +#!/bin/sh + +. /usr/share/libubox/jshn.sh + +CONF="fchomo" + +RESOURCES_DIR="/etc/$CONF/resources" +VER_PATH="/etc/$CONF/resources.json" +mkdir -p "$RESOURCES_DIR" + +RUN_DIR="/var/run/$CONF" +LOG_PATH="$RUN_DIR/$CONF.log" +mkdir -p "$RUN_DIR" + +log() { + echo -e "$(date "+%F %T") $*" >> "$LOG_PATH" +} + +set_lock() { + local act="$1" + local type="$2" + + local lock="$RUN_DIR/update_resources-$type.lock" + if [ "$act" = "set" ]; then + if [ -e "$lock" ]; then + log "[$(to_upper "$type")] A task is already running." + exit 2 + else + touch "$lock" + fi + elif [ "$act" = "remove" ]; then + rm -f "$lock" + fi +} + +to_upper() { + echo -e "$1" | tr "[a-z]" "[A-Z]" +} + +get_local_ver() { + local type="$1" + local repoid="$2" + + local ver + if [ -n "$repoid" ]; then + ver="$(eval "jsonfilter -qi \"$VER_PATH\" -e '@[\"$type\"][\"$repoid\"].version'")" + else + ver="$(eval "jsonfilter -qi \"$VER_PATH\" -e '@[\"$type\"]'")" + fi + + [ -n "$ver" ] && echo "$ver" || return 1 +} + +check_dashboard_update() { + local dashtype="$1" + local dashrepo="$2" + local dashrepoid="$(echo -n "$dashrepo" | sed 's|\W|_|g' | tr 'A-Z' 'a-z')" + local wget="wget --tries=1 --timeout=10 -q" + + set_lock "set" "$dashtype" + + local dash_ver="$($wget -O- "https://api.github.com/repos/$dashrepo/releases/latest" | jsonfilter -e "@.tag_name" 2>/dev/null)" + [ -n "$dash_ver" ] || { + dash_ver="$($wget -O- "https://api.github.com/repos/$dashrepo/tags" | jsonfilter -e "@[*].name" | head -n1)" + } + if [ -z "$dash_ver" ]; then + log "[$(to_upper "$dashtype")] [$dashrepo] Failed to get the latest version, please retry later." + + set_lock "remove" "$dashtype" + return 1 + fi + + local local_dash_ver="$(get_local_ver "$dashtype" "$dashrepoid" || echo "NOT FOUND")" + if [ "$local_dash_ver" = "$dash_ver" ]; then + log "[$(to_upper "$dashtype")] [$dashrepo] Current version: $dash_ver." + log "[$(to_upper "$dashtype")] [$dashrepo] You're already at the latest version." + + set_lock "remove" "$dashtype" + return 3 + else + log "[$(to_upper "$dashtype")] [$dashrepo] Local version: $local_dash_ver, latest version: $dash_ver." + fi + + if ! $wget "https://codeload.github.com/$dashrepo/tar.gz/refs/heads/gh-pages" -O "$RUN_DIR/$dashtype.tgz" || ! tar -tzf "$RUN_DIR/$dashtype.tgz" >/dev/null; then + rm -f "$RUN_DIR/$dashtype.tgz" + log "[$(to_upper "$dashtype")] [$dashrepo] Update failed." + + set_lock "remove" "$dashtype" + return 1 + fi + + mv -f "$RUN_DIR/$dashtype.tgz" "$RESOURCES_DIR/$dashrepoid.tgz" + touch "$VER_PATH" + json_init + json_load_file "$VER_PATH" + json_select "$dashtype" 2>/dev/null || json_add_object "$dashtype" + json_select "$dashrepoid" 2>/dev/null || json_add_object "$dashrepoid" + json_add_string repo "$dashrepo" + json_add_string version "$dash_ver" + json_dump > "$VER_PATH" + log "[$(to_upper "$dashtype")] [$dashrepo] Successfully updated." + + set_lock "remove" "$dashtype" + return 0 +} + +# Reference from homeproxy +check_geodata_update() { + local geotype="$1" + local georepo="$2" + local wget="wget --tries=1 --timeout=10 -q" + + set_lock "set" "$geotype" + + local geodata_ver="$($wget -O- "https://api.github.com/repos/$georepo/releases/latest" | jsonfilter -e "@.tag_name")" + if [ -z "$geodata_ver" ]; then + log "[$(to_upper "$geotype")] Failed to get the latest version, please retry later." + + set_lock "remove" "$geotype" + return 1 + fi + + local local_geodata_ver="$(get_local_ver "$geotype" || echo "NOT FOUND")" + if [ "$local_geodata_ver" = "$geodata_ver" ]; then + log "[$(to_upper "$geotype")] Current version: $geodata_ver." + log "[$(to_upper "$geotype")] You're already at the latest version." + + set_lock "remove" "$geotype" + return 3 + else + log "[$(to_upper "$geotype")] Local version: $local_geodata_ver, latest version: $geodata_ver." + fi + + local geodata_hash + $wget "https://github.com/$georepo/releases/download/$geodata_ver/$geotype.dat" -O "$RUN_DIR/$geotype.dat" + geodata_hash="$($wget -O- "https://github.com/$georepo/releases/download/$geodata_ver/$geotype.dat.sha256sum" | awk '{print $1}')" + if ! echo -e "$geodata_hash $RUN_DIR/$geotype.dat" | sha256sum -s -c -; then + rm -f "$RUN_DIR/$geotype.dat" + log "[$(to_upper "$geotype")] Update failed." + + set_lock "remove" "$geotype" + return 1 + fi + + mv -f "$RUN_DIR/$geotype.dat" "$RESOURCES_DIR/../$geotype.dat" + touch "$VER_PATH" + json_init + json_load_file "$VER_PATH" + json_add_string "$geotype" "$geodata_ver" + json_dump > "$VER_PATH" + log "[$(to_upper "$geotype")] Successfully updated." + + set_lock "remove" "$geotype" + return 0 +} + +# Reference from homeproxy +check_list_update() { + local listtype="$1" + local listrepo="$2" + local listref="$3" + local listname="$4" + local wget="wget --tries=1 --timeout=10 -q" + + set_lock "set" "$listtype" + + local list_info="$($wget -O- "https://api.github.com/repos/$listrepo/commits?sha=$listref&path=$listname")" + local list_sha="$(echo -e "$list_info" | jsonfilter -e "@[0].sha")" + local list_ver="$(echo -e "$list_info" | jsonfilter -e "@[0].commit.message" | grep -Eo "[0-9-]+" | tr -d '-')" + if [ -z "$list_sha" ] || [ -z "$list_ver" ]; then + log "[$(to_upper "$listtype")] Failed to get the latest version, please retry later." + + set_lock "remove" "$listtype" + return 1 + fi + + local local_list_ver="$(get_local_ver "$listtype" || echo "NOT FOUND")" + if [ "$local_list_ver" = "$list_ver" ]; then + log "[$(to_upper "$listtype")] Current version: $list_ver." + log "[$(to_upper "$listtype")] You're already at the latest version." + + set_lock "remove" "$listtype" + return 3 + else + log "[$(to_upper "$listtype")] Local version: $local_list_ver, latest version: $list_ver." + fi + + if ! $wget "https://fastly.jsdelivr.net/gh/$listrepo@$list_sha/$listname" -O "$RUN_DIR/$listname" || [ ! -s "$RUN_DIR/$listname" ]; then + rm -f "$RUN_DIR/$listname" + log "[$(to_upper "$listtype")] Update failed." + + set_lock "remove" "$listtype" + return 1 + fi + + mv -f "$RUN_DIR/$listname" "$RESOURCES_DIR/$listtype.${listname##*.}" + touch "$VER_PATH" + json_init + json_load_file "$VER_PATH" + json_add_string "$listtype" "$list_ver" + json_dump > "$VER_PATH" + log "[$(to_upper "$listtype")] Successfully updated." + + set_lock "remove" "$listtype" + return 0 +} + +case "$1" in +"ALL") + # Since the VER_PATH lock is not designed, parallelism is not currently supported. + for _type in geoip geosite asn china_ip4 china_ip6 gfw_list china_list; do + "$0" "$_type" + done + # dashboard + _repos="$(jsonfilter -qi "$VER_PATH" -e '@.dashboard[*].repo')" + if [ -n "$_repos" ]; then + for i in $(echo "$_repos" | sed -n '='); do + "$0" "dashboard" "$(echo "$_repos" | sed -n "${i}p")" + done + else + "$0" "dashboard" + fi + ;; +"dashboard") + check_dashboard_update "$1" "${2:-MetaCubeX/metacubexd}" + ;; +"geoip") + check_geodata_update "$1" "Loyalsoldier/v2ray-rules-dat" + ;; +"geosite") + check_geodata_update "$1" "Loyalsoldier/v2ray-rules-dat" + ;; +"asn") + check_list_update "$1" "Loyalsoldier/geoip" "release" "GeoLite2-ASN.mmdb" && + mv -f "$RESOURCES_DIR/asn.mmdb" "$RESOURCES_DIR/../asn.mmdb" + ;; +"china_ip4") + check_list_update "$1" "muink/route-list" "release" "china_ipv4.txt" + ;; +"china_ip6") + check_list_update "$1" "muink/route-list" "release" "china_ipv6.txt" + ;; +"gfw_list") + check_list_update "$1" "muink/route-list" "release" "gfwlist.txt" + ;; +"china_list") + check_list_update "$1" "muink/route-list" "release" "china_list2.txt" + ;; +*) + echo -e "Usage: $0 " + exit 1 + ;; +esac diff --git a/luci-app-fchomo/root/etc/init.d/fchomo b/luci-app-fchomo/root/etc/init.d/fchomo new file mode 100755 index 0000000000..e591c8ee74 --- /dev/null +++ b/luci-app-fchomo/root/etc/init.d/fchomo @@ -0,0 +1,475 @@ +#!/bin/sh /etc/rc.common + +. "${IPKG_INSTROOT}/lib/functions/network.sh" + +USE_PROCD=1 + +START=99 +STOP=10 + +CONF="fchomo" +PROG="/usr/bin/mihomo" + +HM_DIR="/etc/fchomo" +TEMPS_DIR="$HM_DIR/templates" +RUN_DIR="/var/run/fchomo" +LOG_PATH="$RUN_DIR/fchomo.log" + +# Compatibility +[ -x "$(command -v apk)" ] && OPM='apk' || OPM='opkg' +# +# thanks to homeproxy +# we don't know which is the default server, just take the first one +DNSMASQ_UCI_CONFIG="$(uci -q show "dhcp.@dnsmasq[0]" | awk 'NR==1 {split($0, conf, /[.=]/); print conf[2]}')" +if [ -f "/tmp/etc/dnsmasq.conf.$DNSMASQ_UCI_CONFIG" ]; then + DNSMASQ_DIR="$(awk -F '=' '/^conf-dir=/ {print $2}' "/tmp/etc/dnsmasq.conf.$DNSMASQ_UCI_CONFIG")/dnsmasq-fchomo.d" +else + DNSMASQ_DIR="/tmp/dnsmasq.d/dnsmasq-fchomo.d" +fi +# +# opmc action $@ +opmc() { + local action="$1"; shift; + + if [ "$OPM" = "apk" ]; then + case "$action" in + "list") + action="list -q";; + "list-installed") + action="list -qI";; + esac + fi + + $OPM $action "$@" +} + +config_load "$CONF" + +# define global var: DEF_WAN DEF_WAN6 NIC_* NIC6_* +define_nic() { + local dev sub addr + # get all active NICs + for dev in $(ls /sys/class/net/); do + #ipv4 + sub=$(ip -o -4 addr|sed -En "s|.*${dev}\s+inet\s+([0-9\./]+).*|\1|gp") + eval "NIC_${dev//[[:punct:]]/_}=\"\$sub\"" + #ipv6 + sub=$(ip -o -6 addr|sed -En "s|.*${dev}\s+inet6\s+([A-Za-z0-9\./:]+).*|\1|gp") + # ref: https://github.com/openwrt/openwrt/blob/main/package/base-files/files/lib/functions/network.sh#L53 #network_get_subnet6() + for _ in $sub; do + for addr in $sub; do + case "$addr" in fe[8ab]?:*|f[cd]??:*) + continue + esac + sub=$addr; break + done + # Attempt to return first non-fe80::/10 range + for addr in $sub; do + case "$addr" in fe[8ab]?:*) + continue + esac + sub=$addr; break + done + # Return first item + for addr in $sub; do + sub=$addr; break + done + done + eval "NIC6_${dev//[[:punct:]]/_}=\"\$sub\"" + done + # get default gateway 0.0.0.0/:: + network_find_wan DEF_WAN true + network_find_wan6 DEF_WAN6 true + + return 0 +} +define_nic + +load_interfaces() { + local bind_ifname + config_get bind_ifname "$1" "bind_interface" + + [ -z "$bind_ifname" ] || interfaces=" $(uci -q show network|grep "device='$bind_ifname'"|cut -f2 -d'.') $interfaces" +} + +log() { + echo -e "$(date "+%Y-%m-%d %H:%M:%S") [DAEMON] $*" >> "$LOG_PATH" +} + +start_service() { + local client_enabled server_enabled server_auto_firewall + config_get client_enabled "routing" "client_enabled" "0" + config_get_bool server_enabled "routing" "server_enabled" "0" + config_get_bool server_auto_firewall "routing" "server_auto_firewall" "0" + + if [ "$client_enabled" = "0" -a "$server_enabled" = "0" ]; then + return 1 + fi + + mkdir -p "$RUN_DIR" + + # Client + if [ "$client_enabled" = "1" ]; then + if [ -z "$1" -o "$1" = "mihomo-c" ]; then + # Generate/Validate client config + ucode -S "$HM_DIR/scripts/generate_client.uc" 2>>"$LOG_PATH" | yq -Poy | yq \ + '.sniffer["force-domain"][] style="double" + | .sniffer["skip-domain"][] style="double" + | with(.dns["nameserver-policy"] | keys; .. style="double") + | .dns["fallback-filter"].domain[] style="double" + | with(.["proxy-providers"][] | select(.payload); .payload style="literal") + | with(.["rule-providers"][] | select(.payload); .payload style="literal")' \ + | sed -E 's,^(\s*payload:) \|-,\1,' \ + > "$RUN_DIR/mihomo-c.yaml" + yq eval-all -i '. as $item ireduce ({}; . * $item )' "$RUN_DIR/mihomo-c.yaml" "$TEMPS_DIR/"*.yaml + + if [ ! -e "$RUN_DIR/mihomo-c.yaml" ]; then + log "Error: failed to generate client configuration." + return 1 + elif ! "$PROG" -t -d "$HM_DIR" -f "$RUN_DIR/mihomo-c.yaml" >/dev/null 2>>"$LOG_PATH"; then + log "Error: wrong client configuration detected." + return 1 + fi + echo > "$RUN_DIR/mihomo-c.log" + + # Deploy Clash API Dashboard + local dashboard_repo + config_get dashboard_repo "api" "dashboard_repo" "" + + if [ -n "$dashboard_repo" -a ! -d "$RUN_DIR/ui" ]; then + tar -xzf "$HM_DIR/resources/$(echo "$dashboard_repo" | sed 's|\W|_|g').tgz" -C "$RUN_DIR/" + mv "$RUN_DIR/"*-gh-pages/ "$RUN_DIR/ui/" + fi + + # Setup DNSMasq servers and IP-sets + local global_ipv6 dns_ipv6 + config_get_bool global_ipv6 "global" "ipv6" "1" + config_get_bool dns_ipv6 "dns" "ipv6" "1" + local dns_port tcp_dns_port + config_get dns_port "dns" "port" "7853" + config_get tcp_dns_port "inbound" "tunnel_port" "7893" + local routing_mode routing_domain + config_get routing_mode "routing" "routing_mode" "" + config_get_bool routing_domain "routing" "routing_domain" "0" + + mkdir -p "$DNSMASQ_DIR" + echo -e "conf-dir=$DNSMASQ_DIR" > "$DNSMASQ_DIR/../dnsmasq-fchomo.conf" + cat <<-EOF > "$DNSMASQ_DIR/forward-dns.conf" + no-poll + no-resolv + server=127.0.0.1#$dns_port + EOF + + # [yaml] + write_ipset_file() { + local family=$1 + local set_name=$2 + local src="$3" + local dst="$4" + local yaml="$5" + + if [ -n "$yaml" ]; then + yq '.[] |= with(select(. == null); . = []) | .FQDN[]' "$src" | \ + sed "s|^|nftset=/|;s|$|/${family}#inet#fchomo#${set_name}|" > "$dst" + else + sed "s|^|nftset=/|;s|$|/${family}#inet#fchomo#${set_name}|" "$src" > "$dst" + fi + } + + # IP-sets + if [ -n "$(opmc list-installed dnsmasq-full)" ]; then + write_ipset_file 4 inet4_wan_direct_addr "$HM_DIR/resources/direct_list.yaml" "$DNSMASQ_DIR/direct_list.conf" yaml + [ "$global_ipv6" != "1" ] || \ + write_ipset_file 6 inet6_wan_direct_addr "$HM_DIR/resources/direct_list.yaml" "$DNSMASQ_DIR/direct_list6.conf" yaml + + write_ipset_file 4 inet4_wan_proxy_addr "$HM_DIR/resources/proxy_list.yaml" "$DNSMASQ_DIR/proxy_list.conf" yaml + [ "$global_ipv6" != "1" ] || \ + write_ipset_file 6 inet6_wan_proxy_addr "$HM_DIR/resources/proxy_list.yaml" "$DNSMASQ_DIR/proxy_list6.conf" yaml + + if [ "$routing_domain" = "1" ]; then + case "$routing_mode" in + bypass_cn) + write_ipset_file 4 inet4_china_list_addr "$HM_DIR/resources/china_list.txt" "$DNSMASQ_DIR/china_list.conf" + [ "$global_ipv6" != "1" ] || \ + write_ipset_file 6 inet6_china_list_addr "$HM_DIR/resources/china_list.txt" "$DNSMASQ_DIR/china_list6.conf" + ;; + routing_gfw) + write_ipset_file 4 inet4_gfw_list_addr "$HM_DIR/resources/gfw_list.txt" "$DNSMASQ_DIR/gfw_list.conf" + [ "$global_ipv6" != "1" ] || \ + write_ipset_file 6 inet6_gfw_list_addr "$HM_DIR/resources/gfw_list.txt" "$DNSMASQ_DIR/gfw_list6.conf" + ;; + esac + fi + fi + + /etc/init.d/dnsmasq reload >/dev/null 2>&1 + + # Setup routing table + local proxy_mode table_id rule_pref + config_get proxy_mode "inbound" "proxy_mode" "redir_tproxy" + config_get table_id "config" "route_table_id" "2022" + config_get rule_pref "config" "route_rule_pref" "9000" + case "$proxy_mode" in + "redir_tproxy") + local tproxy_mark + config_get tproxy_mark "config" "tproxy_mark" "201" + + ip rule add fwmark "$tproxy_mark" pref "$rule_pref" table "$table_id" + ip route add local default dev lo table "$table_id" + + if [ "$global_ipv6" = "1" ]; then + ip -6 rule add fwmark "$tproxy_mark" pref "$rule_pref" table "$table_id" + ip -6 route add local default dev lo table "$table_id" + fi + ;; + "redir_tun"|"tun") + local tun_name tun_mark + config_get tun_name "config" "tun_name" "hmtun0" + config_get tun_mark "config" "tun_mark" "202" + + ip tuntap add mode tun user root name "$tun_name" + sleep 1s + ip link set "$tun_name" up + + ip route replace default dev "$tun_name" table "$table_id" + ip rule add fwmark "$tun_mark" pref "$rule_pref" table "$table_id" + + if [ "$global_ipv6" = "1" ]; then + ip -6 route replace default dev "$tun_name" table "$table_id" + ip -6 rule add fwmark "$tun_mark" pref "$rule_pref" table "$table_id" + fi + ;; + esac + + # mihomo (client) + procd_open_instance "mihomo-c" + + procd_set_param command /bin/sh + procd_append_param command -c "'$PROG' -d '$HM_DIR' -f '$RUN_DIR/mihomo-c.yaml' >> '$RUN_DIR/mihomo-c.log' 2>&1" + + # Only supports `Global`` and does not support `Proxy Group` and `Proxy Node` + local bind_ifname + config_get bind_ifname "routing" "bind_interface" + + procd_set_param netdev "br-lan" + if [ -n "$bind_ifname" ]; then + procd_append_param netdev "$bind_ifname" + else + local ifname + network_get_device ifname "$DEF_WAN" && procd_append_param netdev "$ifname" + network_get_device ifname "$DEF_WAN6" && procd_append_param netdev "$ifname" + fi + + #procd_set_param capabilities "/etc/capabilities/fchomo.json" + #procd_set_param user mihomo + #procd_set_param group mihomo + + procd_set_param limits core="unlimited" + procd_set_param limits nofile="1000000 1000000" + procd_set_param stderr 1 + procd_set_param respawn + + procd_close_instance + fi + fi + + # Server + if [ "$server_enabled" = "1" ]; then + if [ -z "$1" -o "$1" = "mihomo-s" ]; then + # Generate/Validate server config + ucode -S "$HM_DIR/scripts/generate_server.uc" 2>>"$LOG_PATH" | yq -Poy > "$RUN_DIR/mihomo-s.yaml" + + if [ ! -e "$RUN_DIR/mihomo-s.yaml" ]; then + log "Error: failed to generate server configuration." + return 1 + elif ! "$PROG" -t -d "$HM_DIR" -f "$RUN_DIR/mihomo-s.yaml" >/dev/null 2>>"$LOG_PATH"; then + log "Error: wrong server configuration detected." + return 1 + fi + echo > "$RUN_DIR/mihomo-s.log" + + # mihomo (server) + procd_open_instance "mihomo-s" + + procd_set_param command /bin/sh + procd_append_param command -c "'$PROG' -d '$HM_DIR' -f '$RUN_DIR/mihomo-s.yaml' >> '$RUN_DIR/mihomo-s.log' 2>&1" + + #procd_set_param capabilities "/etc/capabilities/fchomo.json" + #procd_set_param user mihomo + #procd_set_param group mihomo + + procd_set_param limits core="unlimited" + procd_set_param limits nofile="1000000 1000000" + procd_set_param stderr 1 + procd_set_param respawn + + if [ "$server_auto_firewall" = "1" ]; then + add_firewall() { + local enabled listen port + config_get_bool enabled "$1" "enabled" "1" + config_get listen "$1" "listen" "::" + config_get port "$1" "port" + + [ "$enabled" = "0" ] && return 0 + + json_add_object '' + json_add_string type rule + json_add_string target ACCEPT + json_add_string name "$1" + #json_add_string family '' # '' = IPv4 and IPv6 + json_add_string proto 'tcp udp' + json_add_string src "*" + #json_add_string dest '' # '' = input + json_add_string dest_ip "$(echo "$listen" | grep -vE '^(0\.\d+\.\d+\.\d+|::)$')" + json_add_string dest_port "$port" + json_close_object + } + + procd_open_data + # configure firewall + json_add_array firewall + # meta l4proto %s th dport %s counter accept comment "!%s: accept server instance [%s]" + config_foreach add_firewall "server" + json_close_array + procd_close_data + fi + + procd_close_instance + fi + fi + + # log-cleaner + procd_open_instance "log-cleaner" + procd_set_param command "$HM_DIR/scripts/clean_log.sh" + procd_set_param respawn + procd_close_instance + + # Setup firewall + utpl -S "$HM_DIR/scripts/firewall_pre.ut" > "$RUN_DIR/fchomo_pre.nft" + # Setup Nftables rules + if [ "$client_enabled" = "1" ]; then + [ -z "$1" -o "$1" = "mihomo-c" ] && utpl -S "$HM_DIR/scripts/firewall_post.ut" > "$RUN_DIR/fchomo_post.nft" + fi + fw4 reload >"/dev/null" 2>&1 + + log "$(mihomo -v | awk 'NR==1{print $1,$3}') started." +} + +service_started() { procd_set_config_changed firewall; } + +stop_service() { + # Client + [ -z "$1" -o "$1" = "mihomo-c" ] && stop_client + # Server + [ -z "$1" -o "$1" = "mihomo-s" ] && stop_server + # Setup firewall + echo > "$RUN_DIR/fchomo_pre.nft" 2>"/dev/null" + fw4 reload >"/dev/null" 2>&1 + return 0 +} + +stop_client() { + # Load config + local table_id tproxy_mark tun_mark tun_name + config_get table_id "cofnig" "route_table_id" "2022" + config_get rule_pref "config" "route_rule_pref" "9000" + config_get tproxy_mark "cofnig" "tproxy_mark" "201" + config_get tun_mark "cofnig" "tun_mark" "202" + config_get tun_name "cofnig" "tun_name" "hmtun0" + + # Remove routing table + # Tproxy + ip rule del pref "$rule_pref" table "$table_id" 2>"/dev/null" + ip route del local default dev lo table "$table_id" 2>"/dev/null" + + ip -6 rule del pref "$rule_pref" table "$table_id" 2>"/dev/null" + ip -6 route del local default dev lo table "$table_id" 2>"/dev/null" + + # TUN + ip route del default dev "$tun_name" table "$table_id" 2>"/dev/null" + ip rule del pref "$rule_pref" table "$table_id" 2>"/dev/null" + + ip -6 route del default dev "$tun_name" table "$table_id" 2>"/dev/null" + ip -6 rule del pref "$rule_pref" table "$table_id" 2>"/dev/null" + + # Remove Nftables rules + nft flush table inet fchomo 2>"/dev/null" + nft delete table inet fchomo 2>"/dev/null" + echo > "$RUN_DIR/fchomo_post.nft" 2>"/dev/null" + + # Remove DNSMasq servers + rm -rf "$DNSMASQ_DIR/../dnsmasq-fchomo.conf" "$DNSMASQ_DIR" + /etc/init.d/dnsmasq reload >/dev/null 2>&1 + + # Remove Clash API Dashboard + rm -rf "$RUN_DIR/ui" + + # Remove client config + rm -f "$RUN_DIR/mihomo-c.yaml" "$RUN_DIR/mihomo-c.log" + + log "Service mihomo-c stopped." +} + +stop_server() { + # Remove server config + rm -f "$RUN_DIR/mihomo-s.yaml" "$RUN_DIR/mihomo-s.log" + + log "Service mihomo-s stopped." +} + +service_stopped() { + sleep 1s # Wait for procd_kill complete + # Client + [ -n "$(/etc/init.d/$CONF info | jsonfilter -q -e '@.'"$CONF"'.instances["mihomo-c"]')" ] || client_stopped + # Server + + procd_set_config_changed firewall; +} + +client_stopped() { + # Load config + local tun_name + config_get tun_name "config" "tun_name" "hmtun0" + + # TUN + ip link set "$tun_name" down 2>"/dev/null" + ip tuntap del mode tun name "$tun_name" 2>"/dev/null" + + ip -6 rule del oif "$tun_name" 2>"/dev/null" +} + +server_stopped() { + return 0 +} + +reload_service() { + log "Reloading service${1:+ $1}..." + + stop "$@" + start "$@" +} + +service_triggers() { + procd_add_reload_trigger "$CONF" 'network' + + local interfaces + + # Only supports `Global`` and does not support `Proxy Group` and `Proxy Node` + load_interfaces 'routing' + [ -n "$interfaces" ] && { + for n in $interfaces; do + procd_add_reload_interface_trigger $n + done + } || { + for n in $DEF_WAN $DEF_WAN6; do + procd_add_reload_interface_trigger $n + done + } + + interfaces=$(uci show network|grep "device='br-lan'"|cut -f2 -d'.') + [ -n "$interfaces" ] && { + for n in $interfaces; do + procd_add_reload_interface_trigger $n + done + } +} diff --git a/luci-app-fchomo/root/etc/uci-defaults/luci-app-fchomo b/luci-app-fchomo/root/etc/uci-defaults/luci-app-fchomo new file mode 100755 index 0000000000..ca456e16c1 --- /dev/null +++ b/luci-app-fchomo/root/etc/uci-defaults/luci-app-fchomo @@ -0,0 +1,72 @@ +#!/bin/sh + +for d in certs provider ruleset resources templates; do + mkdir -p "/etc/fchomo/$d/" 2>/dev/null +done + +if ! uci -q get fchomo.global.authentication; then + uci add_list fchomo.global.authentication="fchomodef:$(cat /proc/sys/kernel/random/uuid)" + uci commit fchomo +fi + +if [ ! -s "/etc/fchomo/resources/direct_list.yaml" ]; then + cat <<- EOF > "/etc/fchomo/resources/direct_list.yaml" + FQDN: + IPCIDR: + - '223.0.0.0/12' + IPCIDR6: + - '2400:3200::/32' + EOF +fi + +if [ ! -s "/etc/fchomo/resources/proxy_list.yaml" ]; then + cat <<- EOF > "/etc/fchomo/resources/proxy_list.yaml" + FQDN: + - www.google.com + IPCIDR: + - '91.105.192.0/23' + - '91.108.4.0/22' + - '91.108.8.0/22' + - '91.108.16.0/22' + - '91.108.12.0/22' + - '91.108.20.0/22' + - '91.108.56.0/22' + - '149.154.160.0/20' + - '185.76.151.0/24' + IPCIDR6: + - '2001:67c:4e8::/48' + - '2001:b28:f23c::/48' + - '2001:b28:f23d::/48' + - '2001:b28:f23f::/48' + - '2a0a:f280::/32' + EOF +fi + +if [ ! -s "/etc/fchomo/templates/hosts.yaml" ]; then + cat <<- EOF > "/etc/fchomo/templates/hosts.yaml" + hosts: + # '*.clash.dev': 127.0.0.1 + # 'alpha.clash.dev': '::1' + # test.com: [1.1.1.1, 2.2.2.2] + # baidu.com: google.com + dns.alidns.com: [223.6.6.6, '2400:3200:baba::1', 223.5.5.5, '2400:3200::1'] + dns.google: [8.8.8.8, '2001:4860:4860::8888', 8.8.4.4, '2001:4860:4860::8844'] + EOF +fi + +uci -q batch <<-EOF >"/dev/null" + delete firewall.fchomo_pre + set firewall.fchomo_pre=include + set firewall.fchomo_pre.type=nftables + set firewall.fchomo_pre.path="/var/run/fchomo/fchomo_pre.nft" + set firewall.fchomo_pre.position="table-pre" + + delete firewall.fchomo_post + set firewall.fchomo_post=include + set firewall.fchomo_post.type=nftables + set firewall.fchomo_post.path="/var/run/fchomo/fchomo_post.nft" + set firewall.fchomo_post.position="ruleset-post" + commit firewall +EOF + +exit 0 diff --git a/luci-app-fchomo/root/etc/uci-defaults/luci-app-fchomo-migration b/luci-app-fchomo/root/etc/uci-defaults/luci-app-fchomo-migration new file mode 100755 index 0000000000..052304823e --- /dev/null +++ b/luci-app-fchomo/root/etc/uci-defaults/luci-app-fchomo-migration @@ -0,0 +1,15 @@ +#!/bin/sh + +default_proxy=$(uci -q get fchomo.routing.default_proxy) +if [ -n "$default_proxy" ]; then + uci -q batch <<-EOF >"/dev/null" + delete fchomo.routing.default_proxy + set fchomo.routing.client_enabled="1" + set fchomo.autogened_final_host="rules" + set fchomo.autogened_final_host.label="Auto Generated Final" + set fchomo.autogened_final_host.entry="MATCH,$default_proxy" + commit fchomo + EOF +fi + +exit 0 diff --git a/luci-app-fchomo/root/usr/share/luci/menu.d/luci-app-fchomo.json b/luci-app-fchomo/root/usr/share/luci/menu.d/luci-app-fchomo.json new file mode 100644 index 0000000000..487ce1a4ed --- /dev/null +++ b/luci-app-fchomo/root/usr/share/luci/menu.d/luci-app-fchomo.json @@ -0,0 +1,69 @@ +{ + "admin/services/fchomo": { + "title": "FullCombo Mihomo", + "order": 12, + "action": { + "type": "firstchild" + }, + "depends": { + "acl": [ "luci-app-fchomo" ], + "uci": { "fchomo": true } + } + }, + "admin/services/fchomo/global": { + "title": "Global", + "order": 10, + "action": { + "type": "view", + "path": "fchomo/global" + } + }, + "admin/services/fchomo/client": { + "title": "Client", + "order": 20, + "action": { + "type": "view", + "path": "fchomo/client" + } + }, + "admin/services/fchomo/hosts": { + "title": "Hosts", + "order": 25, + "action": { + "type": "view", + "path": "fchomo/hosts" + } + }, + "admin/services/fchomo/node": { + "title": "Node", + "order": 30, + "action": { + "type": "view", + "path": "fchomo/node" + } + }, + "admin/services/fchomo/ruleset": { + "title": "Ruleset", + "order": 35, + "action": { + "type": "view", + "path": "fchomo/ruleset" + } + }, + "admin/services/fchomo/server": { + "title": "Server", + "order": 40, + "action": { + "type": "view", + "path": "fchomo/server" + } + }, + "admin/services/fchomo/log": { + "title": "Log", + "order": 60, + "action": { + "type": "view", + "path": "fchomo/log" + } + } +} diff --git a/luci-app-fchomo/root/usr/share/rpcd/acl.d/luci-app-fchomo.json b/luci-app-fchomo/root/usr/share/rpcd/acl.d/luci-app-fchomo.json new file mode 100644 index 0000000000..1483865c3f --- /dev/null +++ b/luci-app-fchomo/root/usr/share/rpcd/acl.d/luci-app-fchomo.json @@ -0,0 +1,25 @@ +{ + "luci-app-fchomo": { + "description": "Grant access to fchomo configuration", + "read": { + "file": { + "/etc/init.d/fchomo reload *": [ "exec" ], + "/var/run/fchomo/fchomo.log": [ "read" ], + "/var/run/fchomo/mihomo-c.log": [ "read" ], + "/var/run/fchomo/mihomo-s.log": [ "read" ] + }, + "ubus": { + "service": [ "list" ], + "luci.fchomo": [ "*" ] + }, + "uci": [ "fchomo" ] + }, + "write": { + "file": { + "/tmp/fchomo_certificate.tmp": [ "write" ], + "/tmp/fchomo_initialpack.tmp": [ "write" ] + }, + "uci": [ "fchomo" ] + } + } +} diff --git a/luci-app-fchomo/root/usr/share/rpcd/ucode/luci.fchomo b/luci-app-fchomo/root/usr/share/rpcd/ucode/luci.fchomo new file mode 100755 index 0000000000..a0e8c638e2 --- /dev/null +++ b/luci-app-fchomo/root/usr/share/rpcd/ucode/luci.fchomo @@ -0,0 +1,379 @@ +#!/usr/bin/ucode + +'use strict'; + +import { access, lsdir, lstat, popen, readfile, writefile } from 'fs'; + +/* Kanged from ucode/luci */ +function shellquote(s) { + return `'${replace(s, "'", "'\\''")}'`; +} + +function isBinary(str) { + for (let off = 0, byte = ord(str); off < length(str); byte = ord(str, ++off)) + if (byte <= 8 || (byte >= 14 && byte <= 31)) + return true; + + return false; +} + +function hasKernelModule(kmod) { + return (system(sprintf('[ -e "/lib/modules/$(uname -r)"/%s ]', shellquote(kmod))) === 0); +} + +function yqRead(flags, command, filepath) { + let out = ''; + + const fd = popen(`yq ${flags} ${shellquote(command)} ${filepath}`); + if (fd) { + out = fd.read('all'); + fd.close(); + } + + return out; +} + +function wGET(url, header, filepath) { + if (!url || type(url) !== 'string') + return null; + + let ua = 'Wget/1.21 (FullCombo Mihomo)'; + + if (header) { + header = json(trim(header) || {}); + + header = join(' ', filter(map(keys(header), (k) => { + let v = join(', ', type(header[k]) === 'array' ? filter(header[k], v => v) : []); + if (k === 'User-Agent') { + ua = v; + v = null; + } + return v ? '--header=' + shellquote(`${k}: ${v}`) : null; + }), v => v)); + } else + header = ''; + + let exitcode = system(`wget --tries=1 --timeout=10 --user-agent ${shellquote(ua)} ${header} -q ${shellquote(url)} -O ${shellquote(filepath)}`); + + return exitcode; +} + +const HM_DIR = '/etc/fchomo'; +const RUN_DIR = '/var/run/fchomo'; +const RES_TYPE = ['certs', 'provider', 'ruleset', 'resources', 'templates']; + +const methods = { + get_features: { + call: function() { + let features = {}; + + const use_apk = system('command -v apk') == 0 || null; + + const fd = popen('/usr/bin/mihomo -v'); + if (fd) { + for (let line = fd.read('line'); length(line); line = fd.read('line')) { + let ver = match(trim(line), /Mihomo Meta (.*)/); + if (ver) + features.core_version = split(ver[1], ' ')[0]; + let tags = match(trim(line), /Use tags: (.*)/); + if (tags) + for (let k in split(tags[1], ',')) + features[k] = true; + } + + fd.close(); + } + + const fp = popen(`${use_apk ? 'apk list -I' : 'opkg list-installed'} luci-app-fchomo | ` + + `awk '{print $${use_apk ? '1' : 'NF'}}'`); + if (fp) { + features.luciapp_version = trim(fp.read('line')) || null; + + fp.close(); + } + + features.hm_has_dnsmasq_full = system(`[ -n "$(${use_apk ? 'apk list -qI' : 'opkg list-installed'} dnsmasq-full)" ]`) == 0 || null; + features.hm_has_ip_full = access('/usr/libexec/ip-full'); + features.hm_has_tcp_brutal = hasKernelModule('brutal.ko'); + features.hm_has_tproxy = hasKernelModule('nft_tproxy.ko') || access('/etc/modules.d/nft-tproxy'); + features.hm_has_tun = hasKernelModule('tun.ko') || access('/etc/modules.d/30-tun'); + + return features; + } + }, + + get_clash_api: { + args: { instance: 'instance' }, + call: function(req) { + if (req.args?.instance) { + const instance = req.args?.instance; + + let config = json(trim(yqRead('-oj', '.[] |= with(select(type == "!!map"); del(.)) |= with(select(type == "!!seq"); del(.))', `${RUN_DIR}/${instance}.yaml`)) || '{}'); + + return { + http: config['external-controller'], + https: config['external-controller-tls'], + doh: config['external-doh-server'], + secret: config.secret + }; + } else + return {} + } + }, + + connection_check: { + args: { url: 'url' }, + call: function(req) { + if (!req.args?.url) + return { httpcode: null, error: 'illegal url' }; + + let httpcode = '-1'; + const fd = popen("wget --spider -t1 -ST3 '" + req.args?.url + "' 2>&1 | awk '/^\\s*HTTP\\//{print $2}'"); + if (fd) { + httpcode = trim(fd.read('line')) || httpcode; + + fd.close(); + } + + return { httpcode: httpcode }; + } + }, + + crond_set: { + args: { type: 'type', expr: 'expr' }, + call: function(req) { + if (req.args?.type == 'resources') { + system(`sed -i "/${replace(HM_DIR, "/", "\\/")}\\/scripts\\/update_resources.sh/d" /etc/crontabs/root`); + if (req.args?.expr) + system(`echo -e "` + req.args?.expr + ` ${HM_DIR}/scripts/update_resources.sh ALL" >> /etc/crontabs/root`); + } else + return { result: false, error: 'illegal type' }; + + system(`/etc/init.d/cron restart`); + return { result: true }; + } + }, + + log_clean: { + args: { type: 'type' }, + call: function(req) { + if (!(req.args?.type in ['fchomo', 'mihomo-c', 'mihomo-s'])) + return { result: false, error: 'illegal type' }; + + const filestat = lstat(`${RUN_DIR}/${req.args?.type}.log`); + if (filestat) + writefile(`${RUN_DIR}/${req.args?.type}.log`, ''); + return { result: true }; + } + }, + + resources_get_version: { + args: { type: 'type', repo: 'repo' }, + call: function(req) { + const resources = json(trim(readfile(`${HM_DIR}/resources.json`)) || '{}'); + const versions = resources[req.args?.type]; + if (req.args?.repo) { + for (let obj in values(versions)) + if (obj.repo === req.args?.repo) + return { version: obj.version }; + + return { version: null }; + } else + return { version: versions }; + } + }, + + resources_update: { + args: { type: 'type', repo: 'repo' }, + call: function(req) { + if (req.args?.type) { + const type = shellquote(req.args?.type), + repo = shellquote(req.args?.repo); + const exit_code = system(`${HM_DIR}/scripts/update_resources.sh ${type} ${repo}`); + return { status: exit_code }; + } else + return { status: 255, error: 'illegal type' }; + } + }, + + dir_ls: { + args: { type: 'type' }, + call: function(req) { + if (!(req.args?.type in RES_TYPE)) + return { result: null, error: 'illegal type' }; + + const list = lsdir(`${HM_DIR}/${req.args?.type}/`); + return { result: list }; + } + }, + + file_read: { + args: { type: 'type', filename: 'filename' }, + call: function(req) { + if (!(req.args?.type in RES_TYPE)) + return { content: null, error: 'illegal type' }; + + if ((!req.args?.filename) || match(req.args?.filename, /\.\.\//)) + return { content: null, error: 'illegal filename' }; + + const filecontent = readfile(`${HM_DIR}/${req.args?.type}/${req.args?.filename}`); + return { content: filecontent }; + } + }, + + file_write: { + args: { type: 'type', filename: 'filename', content: 'content' }, + call: function(req) { + if (!(req.args?.type in RES_TYPE)) + return { result: false, error: 'illegal type' }; + + if ((!req.args?.filename) || match(req.args?.filename, /\.\.\//)) + return { result: false, error: 'illegal filename' }; + + const file = `${HM_DIR}/${req.args?.type}/${req.args?.filename}`; + let content = req.args?.content; + + /* Sanitize content */ + if (content) { + content = trim(content); + content = replace(content, /\r\n?/g, '\n'); + if (!match(content, /\n$/)) + content += '\n'; + } + + //system(`mkdir -p ${HM_DIR}/${req.args?.type}`); + writefile(file, content); + + return { result: true }; + } + }, + + file_download: { + args: { type: 'type', filename: 'filename', url: 'url', header: 'header' }, + call: function(req) { + if (!(req.args?.type in RES_TYPE)) + return { result: false, error: 'illegal type' }; + + if ((!req.args?.filename) || match(req.args?.filename, /\.\.\//)) + return { result: false, error: 'illegal filename' }; + + if (!req.args?.url) + return { result: false, error: 'illegal url' }; + + const file = `${HM_DIR}/${req.args?.type}/${req.args?.filename}`; + + //system(`mkdir -p ${HM_DIR}/${req.args?.type}`); + let exitcode = wGET(req.args?.url, req.args?.header, file); + + if (exitcode === 0) { + return { result: true }; + } else + return { result: false, error: 'wget exitcode: ' + sprintf("%d", exitcode) }; + } + }, + + file_remove: { + args: { type: 'type', filename: 'filename' }, + call: function(req) { + if (!(req.args?.type in RES_TYPE)) + return { result: false, error: 'illegal type' }; + + if ((!req.args?.filename) || match(req.args?.filename, /\.\.\//)) + return { result: false, error: 'illegal filename' }; + + system(`rm -f ${HM_DIR}/${req.args?.type}/${req.args?.filename}`); + + return { result: true }; + } + }, + + // thanks to homeproxy + certificate_write: { + args: { filename: 'filename' }, + call: function(req) { + const writeCertificate = function(filename, priv) { + const tmpcert = '/tmp/fchomo_certificate.tmp'; + const filestat = lstat(tmpcert); + + if (!filestat || filestat.type !== 'file' || filestat.size <= 0) { + system(`rm -f ${tmpcert}`); + return { result: false, error: 'empty certificate file' }; + } + + let filecontent = readfile(tmpcert); + if (isBinary(filecontent)) { + system(`rm -f ${tmpcert}`); + return { result: false, error: 'illegal file type: binary' }; + } + + /* Kanged from luci-proto-openconnect */ + const beg = priv ? /^-----BEGIN (RSA|EC) PRIVATE KEY-----$/ : /^-----BEGIN CERTIFICATE-----$/, + end = priv ? /^-----END (RSA|EC) PRIVATE KEY-----$/ : /^-----END CERTIFICATE-----$/, + lines = split(trim(filecontent), /[\r\n]/); + let start = false, i; + + for (i = 0; i < length(lines); i++) { + if (match(lines[i], beg)) + start = true; + else if (start && !b64dec(lines[i]) && length(lines[i]) !== 64) + break; + } + + if (!start || i < length(lines) - 1 || !match(lines[i], end)) { + system(`rm -f ${tmpcert}`); + return { result: false, error: 'this does not look like a correct PEM file' }; + } + + /* Sanitize certificate */ + filecontent = trim(filecontent); + filecontent = replace(filecontent, /\r\n?/g, '\n'); + if (!match(filecontent, /\n$/)) + filecontent += '\n'; + + system(`mkdir -p ${HM_DIR}/certs`); + writefile(`${HM_DIR}/certs/${filename}.pem`, filecontent); + system(`rm -f ${tmpcert}`); + + return { result: true }; + }; + + const filename = req.args?.filename; + if (!filename || match(filename, /\.\.\//)) + return { result: false, error: 'illegal cerificate filename' }; + switch (filename) { + case 'client_ca': + case 'server_publickey': + return writeCertificate(filename, false); + break; + case 'server_privatekey': + return writeCertificate(filename, true); + break; + default: + return { result: false, error: 'illegal cerificate filename' }; + break; + } + } + }, + initialpack_write: { + call: function(req) { + const writeResources = function() { + const tmpcert = '/tmp/fchomo_initialpack.tmp'; + const filestat = lstat(tmpcert); + + if (!filestat || filestat.type !== 'file' || filestat.size <= 0) { + system(`rm -f ${tmpcert}`); + return { result: false, error: 'empty initialpack file' }; + } + + system(`tar -C '${HM_DIR}/' -xzf ${tmpcert} `); + system(`rm -f ${tmpcert}`); + + return { result: true }; + }; + + return writeResources(); + } + } +}; + +return { 'luci.fchomo': methods };