├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_request.yml └── workflows │ ├── build-packages.yml │ ├── delete-workflow-runs.yml │ ├── dependabot.yml │ ├── release-packages.yml │ └── stale-issues.yml ├── .gitignore ├── LICENSE ├── README.md ├── feed.sh ├── install.sh ├── luci-app-nikki ├── Makefile ├── htdocs │ └── luci-static │ │ └── resources │ │ ├── tools │ │ └── nikki.js │ │ └── view │ │ └── nikki │ │ ├── app.js │ │ ├── editor.js │ │ ├── log.js │ │ ├── mixin.js │ │ ├── profile.js │ │ └── proxy.js ├── po │ ├── templates │ │ └── nikki.pot │ └── zh_Hans │ │ └── nikki.po └── root │ └── usr │ └── share │ ├── luci │ └── menu.d │ │ └── luci-app-nikki.json │ └── rpcd │ ├── acl.d │ └── luci-app-nikki.json │ └── ucode │ └── luci.nikki ├── nikki ├── Makefile └── files │ ├── mixin.yaml │ ├── nftables │ ├── geoip6_cn.nft │ ├── geoip_cn.nft │ ├── reserved_ip.nft │ └── reserved_ip6.nft │ ├── nikki.conf │ ├── nikki.init │ ├── nikki.upgrade │ ├── scripts │ ├── debug.sh │ ├── firewall_include.sh │ └── include.sh │ ├── uci-defaults │ ├── firewall.sh │ ├── init.sh │ └── migrate.sh │ └── ucode │ ├── hijack.ut │ ├── include.uc │ └── mixin.uc └── uninstall.sh /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: BUG Report / BUG 报告 2 | description: "Create a report to help us improve" 3 | title: "[BUG]" 4 | labels: ["bug"] 5 | body: 6 | - type: checkboxes 7 | id: self_check 8 | attributes: 9 | label: Self-check / 自查 10 | description: Self-Check before submitting the Issue / 在提交Issue之前的自查 11 | options: 12 | - label: DNSMASQ's `DNS Redirect` option is turned off (ignore if not applicable) / 已经关闭了DNSMASQ的`DNS 重定向`选项(如无此项请忽略) 13 | required: false 14 | - label: The other proxy app has been stopped and the environment is normal / 已停止运行其他代理插件,并确认环境正常 15 | required: true 16 | - type: checkboxes 17 | id: confirm 18 | attributes: 19 | label: Confirm / 确认 20 | description: Please confirm / 请你确认 21 | options: 22 | - label: I have checked the Wiki and found no solution / 我已经查看过Wiki,没有找到解决办法 23 | required: true 24 | - label: I have searched the Issue and found no related issues / 我已经搜索过Issue,没有找到相关问题 25 | required: true 26 | - label: I am using the latest version of the app which is build from this repository / 我使用的是本仓库构建的最新版的插件 27 | required: true 28 | - label: I provided information which does not include sensitive information / 我提供的信息里不包含敏感信息 29 | required: true 30 | - label: I have provided correct, valid, and helpful information for debugging / 我提供了正确的、有效的、可以帮助DEBUG的信息 31 | required: true 32 | - type: textarea 33 | id: description 34 | attributes: 35 | label: BUG Description / BUG 描述 36 | description: Describe your BUG here / 在此描述你的BUG 37 | validations: 38 | required: true 39 | - type: textarea 40 | id: expected_behavior 41 | attributes: 42 | label: Expected Behavior / 预期行为 43 | description: What you expect to happen / 你认为的预期行为 44 | validations: 45 | required: true 46 | - type: textarea 47 | id: reproduce 48 | attributes: 49 | label: Reproduction Steps / 复现步骤 50 | description: How to reproduce / 如何复现 51 | validations: 52 | required: true 53 | - type: textarea 54 | id: debug 55 | attributes: 56 | label: Debug Log / 调试日志 57 | description: Download from Log -> Debug Log then upload it to here / 从 日志 -> 调试日志 下载后上传到此处 58 | validations: 59 | required: true 60 | - type: textarea 61 | id: app_log 62 | attributes: 63 | label: App Log / 插件日志 64 | description: Attach your app log / 附上你的插件日志 65 | validations: 66 | required: true 67 | - type: textarea 68 | id: core_log 69 | attributes: 70 | label: Core Log / 核心日志 71 | description: Attach your core log (info required, debug recommended) / 附上你的核心日志(级别至少为info,建议使用debug) 72 | validations: 73 | required: true 74 | - type: textarea 75 | id: additional_context 76 | attributes: 77 | label: Additional Information / 附加信息 78 | description: Any other information you think might be helpful to solve this BUG / 你觉得对解决此BUG有帮助的其它信息 79 | validations: 80 | required: false 81 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Wiki / 文档 4 | about: Check it before start a new issue 5 | url: https://github.com/nikkinikki-org/OpenWrt-nikki/wiki 6 | - name: FAQ / 常见问题 7 | about: Check it before start a new issue 8 | url: https://github.com/nikkinikki-org/OpenWrt-nikki/wiki/FAQ 9 | - name: Changelog / 更新日志 10 | about: Check it before start a new issue 11 | url: https://github.com/nikkinikki-org/OpenWrt-nikki/wiki/Changelog 12 | - name: How To Ask Questions The Smart Way / 提问的智慧 13 | about: Read it before start a new issue 14 | url: http://www.catb.org/~esr/faqs/smart-questions.html 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature request / 功能请求 2 | description: "Suggest an idea for this project" 3 | title: "[Feature Request]" 4 | labels: ["enhancement"] 5 | body: 6 | - type: checkboxes 7 | id: confirm 8 | attributes: 9 | label: Confirm / 确认 10 | description: Please confirm / 请你确认 11 | options: 12 | - label: I have checked the wiki and can't find the feature I'm looking for / 我已经查看过Wiki,没有找到我想要的功能 13 | required: true 14 | - label: I have searched the Issue and found no related feature requests / 我已经搜索过Issue,没有找到相关的功能请求 15 | required: true 16 | - type: textarea 17 | id: description 18 | attributes: 19 | label: Feature Description / 功能描述 20 | description: Describe the feature you want / 描述你想要的功能 21 | validations: 22 | required: true 23 | - type: textarea 24 | id: how_to 25 | attributes: 26 | label: How to Implement / 如何实现 27 | description: How to implement this feature / 应该如何实现这个功能 28 | validations: 29 | required: false 30 | - type: textarea 31 | id: additional_context 32 | attributes: 33 | label: Additional Information / 附加信息 34 | description: Any other information you think might be helpful to implement this feature / 你觉得对实现此功能有帮助的其它信息 35 | validations: 36 | required: false 37 | -------------------------------------------------------------------------------- /.github/workflows/build-packages.yml: -------------------------------------------------------------------------------- 1 | name: build-packages 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | build: 8 | name: ${{ matrix.arch }}-${{ matrix.branch }} build 9 | runs-on: ubuntu-latest 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | arch: 14 | - arm_cortex-a5_vfpv4 15 | - arm_cortex-a7_neon-vfpv4 16 | - arm_cortex-a8_vfpv3 17 | - arm_cortex-a9 18 | - arm_cortex-a9_vfpv3-d16 19 | - arm_cortex-a9_neon 20 | - arm_cortex-a15_neon-vfpv4 21 | - aarch64_cortex-a53 22 | - aarch64_cortex-a72 23 | - aarch64_cortex-a76 24 | - aarch64_generic 25 | - mips_24kc 26 | - mips_4kec 27 | - mips_mips32 28 | - mipsel_24kc 29 | - mipsel_24kc_24kf 30 | - mipsel_74kc 31 | - mipsel_mips32 32 | - mips64_octeonplus 33 | - i386_pentium4 34 | - x86_64 35 | branch: 36 | - openwrt-23.05 37 | - openwrt-24.10 38 | - SNAPSHOT 39 | exclude: 40 | - arch: aarch64_cortex-a76 41 | branch: openwrt-23.05 42 | 43 | steps: 44 | - name: checkout 45 | uses: actions/checkout@v4 46 | 47 | - name: build 48 | uses: openwrt/gh-action-sdk@main 49 | env: 50 | ARCH: ${{ matrix.arch }}-${{ matrix.branch }} 51 | FEEDNAME: nikki 52 | PACKAGES: luci-app-nikki 53 | NO_REFRESH_CHECK: true 54 | 55 | - name: upload 56 | uses: actions/upload-artifact@v4 57 | with: 58 | name: nikki_${{ matrix.arch }}-${{ matrix.branch }} 59 | path: bin/packages/${{ matrix.arch }}/nikki 60 | -------------------------------------------------------------------------------- /.github/workflows/delete-workflow-runs.yml: -------------------------------------------------------------------------------- 1 | name: delete-workflow-runs 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | jobs: 7 | delete: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | actions: write 11 | contents: read 12 | steps: 13 | - name: delete 14 | uses: Mattraks/delete-workflow-runs@v2 15 | with: 16 | retain_days: 3 17 | keep_minimum_runs: 0 18 | -------------------------------------------------------------------------------- /.github/workflows/dependabot.yml: -------------------------------------------------------------------------------- 1 | name: dependabot 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | get_current_info: 10 | runs-on: ubuntu-latest 11 | outputs: 12 | pkg_source_version: ${{ steps.info.outputs.pkg_source_version }} 13 | pkg_mirror_hash: ${{ steps.info.outputs.pkg_mirror_hash }} 14 | pkg_build_version: ${{ steps.info.outputs.pkg_build_version }} 15 | steps: 16 | - id: checkout 17 | name: checkout 18 | uses: actions/checkout@v4 19 | with: 20 | repository: nikkinikki-org/OpenWrt-nikki 21 | ref: main 22 | path: OpenWrt-nikki 23 | - id: info 24 | name: info 25 | run: | 26 | echo "pkg_source_version=$(grep "PKG_SOURCE_VERSION:=" OpenWrt-nikki/nikki/Makefile | cut -d '=' -f 2)" >> $GITHUB_OUTPUT 27 | echo "pkg_mirror_hash=$(grep "PKG_MIRROR_HASH:=" OpenWrt-nikki/nikki/Makefile | cut -d '=' -f 2)" >> $GITHUB_OUTPUT 28 | echo "pkg_build_version=$(grep "PKG_BUILD_VERSION:=" OpenWrt-nikki/nikki/Makefile | cut -d '=' -f 2)" >> $GITHUB_OUTPUT 29 | get_latest_info: 30 | runs-on: ubuntu-latest 31 | outputs: 32 | commit_date: ${{ steps.info.outputs.commit_date }} 33 | commit_sha: ${{ steps.info.outputs.commit_sha }} 34 | short_commit_sha: ${{ steps.info.outputs.short_commit_sha }} 35 | checksum: ${{ steps.info.outputs.checksum }} 36 | steps: 37 | - id: checkout 38 | name: checkout 39 | uses: actions/checkout@v4 40 | with: 41 | repository: 'MetaCubeX/mihomo' 42 | ref: 'Alpha' 43 | path: 'mihomo' 44 | - id: info 45 | name: info 46 | run: | 47 | echo "commit_date=$(git -C mihomo log -n 1 --format=%cs)" >> $GITHUB_OUTPUT 48 | echo "commit_sha=$(git -C mihomo rev-parse HEAD)" >> $GITHUB_OUTPUT 49 | echo "short_commit_sha=$(git -C mihomo rev-parse --short HEAD)" >> $GITHUB_OUTPUT 50 | git -C mihomo config tar.xz.command "xz -c" 51 | git -C mihomo archive --output=mihomo.tar.xz HEAD 52 | echo "checksum=$(sha256sum mihomo/mihomo.tar.xz | cut -d ' ' -f 1)" >> $GITHUB_OUTPUT 53 | update: 54 | needs: 55 | - get_current_info 56 | - get_latest_info 57 | if: ${{ needs.get_current_info.outputs.pkg_source_version != needs.get_latest_info.outputs.commit_sha }} 58 | runs-on: ubuntu-latest 59 | steps: 60 | - id: checkout 61 | name: checkout 62 | uses: actions/checkout@v4 63 | with: 64 | repository: nikkinikki-org/OpenWrt-nikki 65 | ref: main 66 | path: OpenWrt-nikki 67 | - id: update 68 | name: update 69 | run: | 70 | sed -i "s/PKG_RELEASE:=.*/PKG_RELEASE:=1/" OpenWrt-nikki/nikki/Makefile 71 | sed -i "s/PKG_SOURCE_DATE:=.*/PKG_SOURCE_DATE:=${{ needs.get_latest_info.outputs.commit_date }}/" OpenWrt-nikki/nikki/Makefile 72 | sed -i "s/PKG_SOURCE_VERSION:=.*/PKG_SOURCE_VERSION:=${{ needs.get_latest_info.outputs.commit_sha }}/" OpenWrt-nikki/nikki/Makefile 73 | sed -i "s/PKG_MIRROR_HASH:=.*/PKG_MIRROR_HASH:=${{ needs.get_latest_info.outputs.checksum }}/" OpenWrt-nikki/nikki/Makefile 74 | sed -i "s/PKG_BUILD_VERSION:=.*/PKG_BUILD_VERSION:=alpha-${{ needs.get_latest_info.outputs.short_commit_sha }}/" OpenWrt-nikki/nikki/Makefile 75 | - id: pr 76 | name: pr 77 | uses: peter-evans/create-pull-request@v6 78 | with: 79 | path: OpenWrt-nikki 80 | branch: dependabot 81 | commit-message: "build: update mihomo to ${{ needs.get_latest_info.outputs.short_commit_sha }}" 82 | title: "build: update mihomo to ${{ needs.get_latest_info.outputs.short_commit_sha }}" 83 | body: | 84 | [Changelog](https://github.com/metacubex/mihomo/compare/${{ needs.get_current_info.outputs.pkg_source_version }}...${{ needs.get_latest_info.outputs.commit_sha }}) 85 | -------------------------------------------------------------------------------- /.github/workflows/release-packages.yml: -------------------------------------------------------------------------------- 1 | name: release-packages 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: 7 | - v* 8 | 9 | jobs: 10 | release: 11 | name: ${{ matrix.arch }}-${{ matrix.branch }} release 12 | runs-on: ubuntu-latest 13 | continue-on-error: true 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | arch: 18 | - arm_cortex-a5_vfpv4 19 | - arm_cortex-a7_neon-vfpv4 20 | - arm_cortex-a8_vfpv3 21 | - arm_cortex-a9 22 | - arm_cortex-a9_vfpv3-d16 23 | - arm_cortex-a9_neon 24 | - arm_cortex-a15_neon-vfpv4 25 | - aarch64_cortex-a53 26 | - aarch64_cortex-a72 27 | - aarch64_cortex-a76 28 | - aarch64_generic 29 | - mips_24kc 30 | - mips_4kec 31 | - mips_mips32 32 | - mipsel_24kc 33 | - mipsel_24kc_24kf 34 | - mipsel_74kc 35 | - mipsel_mips32 36 | - mips64_octeonplus 37 | - i386_pentium4 38 | - x86_64 39 | branch: 40 | - openwrt-23.05 41 | - openwrt-24.10 42 | - SNAPSHOT 43 | exclude: 44 | - arch: aarch64_cortex-a76 45 | branch: openwrt-23.05 46 | 47 | steps: 48 | - name: checkout 49 | uses: actions/checkout@v4 50 | 51 | - name: build 52 | uses: openwrt/gh-action-sdk@main 53 | env: 54 | ARCH: ${{ matrix.arch }}-${{ matrix.branch }} 55 | FEEDNAME: nikki 56 | PACKAGES: luci-app-nikki 57 | INDEX: 1 58 | KEY_BUILD: ${{ secrets.KEY_BUILD }} 59 | PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} 60 | NO_REFRESH_CHECK: true 61 | 62 | - name: compress 63 | run: | 64 | tar -c -z -f nikki_${{ matrix.arch }}-${{ matrix.branch }}.tar.gz -C bin/packages/${{ matrix.arch }}/nikki . 65 | mkdir -p public/${{ matrix.branch }}/${{ matrix.arch }} 66 | mv bin/packages/${{ matrix.arch }}/nikki public/${{ matrix.branch }}/${{ matrix.arch }} 67 | tar -c -z -f feed_nikki_${{ matrix.arch }}-${{ matrix.branch }}.tar.gz public/${{ matrix.branch }}/${{ matrix.arch }} 68 | 69 | - if: github.event_name == 'push' && startsWith(github.ref_name, 'v') 70 | name: release 71 | uses: softprops/action-gh-release@v2 72 | with: 73 | files: | 74 | nikki_${{ matrix.arch }}-${{ matrix.branch }}.tar.gz 75 | 76 | - name: upload 77 | uses: actions/upload-artifact@v4 78 | with: 79 | name: feed_nikki_${{ matrix.arch }}-${{ matrix.branch }} 80 | path: feed_nikki_${{ matrix.arch }}-${{ matrix.branch }}.tar.gz 81 | 82 | feed: 83 | needs: release 84 | name: feed 85 | runs-on: ubuntu-latest 86 | 87 | steps: 88 | - name: download 89 | uses: actions/download-artifact@v4 90 | with: 91 | pattern: feed_nikki_* 92 | merge-multiple: true 93 | 94 | - name: uncompress 95 | run: | 96 | for file in feed_nikki_*.tar.gz; do tar -x -z -f "$file"; done 97 | 98 | - name: prepare 99 | run: | 100 | echo "${{ secrets.KEY_BUILD_PUB }}" > public/key-build.pub 101 | echo "${{ secrets.PUBLIC_KEY }}" > public/public-key.pem 102 | tree --dirsfirst --sort name -P '*.apk|*.ipk' --prune --noreport -H "" -T "Nikki's Feed" --charset utf-8 -o public/index.html public 103 | sed -i '/

/,/<\/p>/d' public/index.html 104 | 105 | - name: feed 106 | uses: cloudflare/wrangler-action@v3 107 | with: 108 | accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} 109 | apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} 110 | command: pages deploy public --project-name=nikkinikki 111 | -------------------------------------------------------------------------------- /.github/workflows/stale-issues.yml: -------------------------------------------------------------------------------- 1 | name: stale-issues 2 | 3 | on: 4 | schedule: 5 | - cron: "0 16 * * *" 6 | 7 | jobs: 8 | stale: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/stale@v9 12 | with: 13 | stale-issue-message: 'This issue is stale because it has been open 3 days with no activity. Remove stale label or comment or this will be closed in 1 days.' 14 | close-issue-message: 'This issue was closed because it has been stalled for 1 days with no activity.' 15 | days-before-issue-stale: 3 16 | days-before-issue-close: 1 17 | days-before-pr-stale: -1 18 | days-before-pr-close: -1 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### VisualStudioCode 2 | .vscode/* 3 | !.vscode/settings.json 4 | !.vscode/tasks.json 5 | !.vscode/launch.json 6 | !.vscode/extensions.json 7 | !.vscode/*.code-snippets 8 | 9 | # Local History for Visual Studio Code 10 | .history/ 11 | 12 | # Built Visual Studio Code Extensions 13 | *.vsix 14 | 15 | ### JetBrains 16 | .idea 17 | *.iml 18 | out 19 | gen 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![GitHub License](https://img.shields.io/github/license/nikkinikki-org/OpenWrt-nikki?style=for-the-badge&logo=github) ![GitHub Tag](https://img.shields.io/github/v/release/nikkinikki-org/OpenWrt-nikki?style=for-the-badge&logo=github) ![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/nikkinikki-org/OpenWrt-nikki/total?style=for-the-badge&logo=github) ![GitHub Repo stars](https://img.shields.io/github/stars/nikkinikki-org/OpenWrt-nikki?style=for-the-badge&logo=github) [![Telegram](https://img.shields.io/badge/Telegram-gray?style=for-the-badge&logo=telegram)](https://t.me/nikkinikki_org) 2 | 3 | # Nikki 4 | 5 | Transparent Proxy with Mihomo on OpenWrt. 6 | 7 | ## Prerequisites 8 | 9 | - OpenWrt >= 23.05 10 | - Linux Kernel >= 5.13 11 | - firewall4 12 | 13 | ## Feature 14 | 15 | - Transparent Proxy (Redirect/TPROXY/TUN, IPv4 and/or IPv6) 16 | - Access Control 17 | - Profile Mixin 18 | - Profile Editor 19 | - Scheduled Restart 20 | 21 | ## Install & Update 22 | 23 | ### A. Install From Feed (Recommended) 24 | 25 | 1. Add Feed 26 | 27 | ```shell 28 | # only needs to be run once 29 | curl -s -L https://github.com/nikkinikki-org/OpenWrt-nikki/raw/refs/heads/main/feed.sh | ash 30 | ``` 31 | 32 | 2. Install 33 | 34 | ```shell 35 | # you can install from shell or `Software` menu in LuCI 36 | # for opkg 37 | opkg install nikki 38 | opkg install luci-app-nikki 39 | opkg install luci-i18n-nikki-zh-cn 40 | # for apk 41 | apk add nikki 42 | apk add luci-app-nikki 43 | apk add luci-i18n-nikki-zh-cn 44 | ``` 45 | 46 | ### B. Install From Release 47 | 48 | ```shell 49 | curl -s -L https://github.com/nikkinikki-org/OpenWrt-nikki/raw/refs/heads/main/install.sh | ash 50 | ``` 51 | 52 | ## Uninstall & Reset 53 | 54 | ```shell 55 | curl -s -L https://github.com/nikkinikki-org/OpenWrt-nikki/raw/refs/heads/main/uninstall.sh | ash 56 | ``` 57 | 58 | ## How To Use 59 | 60 | See [Wiki](https://github.com/nikkinikki-org/OpenWrt-nikki/wiki) 61 | 62 | ## How does it work 63 | 64 | 1. Mixin and Update profile. 65 | 2. Run mihomo. 66 | 3. Set scheduled restart. 67 | 4. Set ip rule/route 68 | 5. Generate nftables and apply it. 69 | 70 | Note that the steps above may change base on config. 71 | 72 | ## Compilation 73 | 74 | ```shell 75 | # add feed 76 | echo "src-git nikki https://github.com/nikkinikki-org/OpenWrt-nikki.git;main" >> "feeds.conf.default" 77 | # update & install feeds 78 | ./scripts/feeds update -a 79 | ./scripts/feeds install -a 80 | # make package 81 | make package/luci-app-nikki/compile 82 | ``` 83 | 84 | The package files will be found under `bin/packages/your_architecture/nikki`. 85 | 86 | ## Dependencies 87 | 88 | - ca-bundle 89 | - curl 90 | - yq 91 | - firewall4 92 | - ip-full 93 | - kmod-inet-diag 94 | - kmod-nft-socket 95 | - kmod-nft-tproxy 96 | - kmod-tun 97 | 98 | ## Contributors 99 | 100 | [![Contributors](https://contrib.rocks/image?repo=nikkinikki-org/OpenWrt-nikki)](https://github.com/nikkinikki-org/OpenWrt-nikki/graphs/contributors) 101 | 102 | ## Special Thanks 103 | 104 | - [@ApoisL](https://github.com/apoiston) 105 | - [@xishang0128](https://github.com/xishang0128) 106 | -------------------------------------------------------------------------------- /feed.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Nikki's feed 4 | 5 | # check env 6 | if [[ ! -x "/bin/opkg" && ! -x "/usr/bin/apk" || ! -x "/sbin/fw4" ]]; then 7 | echo "only supports OpenWrt build with firewall4!" 8 | exit 1 9 | fi 10 | 11 | # include openwrt_release 12 | . /etc/openwrt_release 13 | 14 | # get branch/arch 15 | arch="$DISTRIB_ARCH" 16 | branch= 17 | case "$DISTRIB_RELEASE" in 18 | *"23.05"*) 19 | branch="openwrt-23.05" 20 | ;; 21 | *"24.10"*) 22 | branch="openwrt-24.10" 23 | ;; 24 | "SNAPSHOT") 25 | branch="SNAPSHOT" 26 | ;; 27 | *) 28 | echo "unsupported release: $DISTRIB_RELEASE" 29 | exit 1 30 | ;; 31 | esac 32 | 33 | # feed url 34 | repository_url="https://nikkinikki.pages.dev" 35 | feed_url="$repository_url/$branch/$arch/nikki" 36 | 37 | if [ -x "/bin/opkg" ]; then 38 | # add key 39 | echo "add key" 40 | key_build_pub_file="key-build.pub" 41 | curl -s -L -o "$key_build_pub_file" "$repository_url/key-build.pub" 42 | opkg-key add "$key_build_pub_file" 43 | rm -f "$key_build_pub_file" 44 | # add feed 45 | echo "add feed" 46 | if (grep -q nikki /etc/opkg/customfeeds.conf); then 47 | sed -i '/nikki/d' /etc/opkg/customfeeds.conf 48 | fi 49 | echo "src/gz nikki $feed_url" >> /etc/opkg/customfeeds.conf 50 | # update feeds 51 | echo "update feeds" 52 | opkg update 53 | elif [ -x "/usr/bin/apk" ]; then 54 | # add key 55 | echo "add key" 56 | curl -s -L -o "/etc/apk/keys/nikki.pem" "$repository_url/public-key.pem" 57 | # add feed 58 | echo "add feed" 59 | if (grep -q nikki /etc/apk/repositories.d/customfeeds.list); then 60 | sed -i '/nikki/d' /etc/apk/repositories.d/customfeeds.list 61 | fi 62 | echo "$feed_url/packages.adb" >> /etc/apk/repositories.d/customfeeds.list 63 | # update feeds 64 | echo "update feeds" 65 | apk update 66 | fi 67 | 68 | echo "success" 69 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Nikki's installer 4 | 5 | # check env 6 | if [[ ! -x "/bin/opkg" && ! -x "/usr/bin/apk" || ! -x "/sbin/fw4" ]]; then 7 | echo "only supports OpenWrt build with firewall4!" 8 | exit 1 9 | fi 10 | 11 | # include openwrt_release 12 | . /etc/openwrt_release 13 | 14 | # get branch/arch 15 | arch="$DISTRIB_ARCH" 16 | branch= 17 | case "$DISTRIB_RELEASE" in 18 | *"23.05"*) 19 | branch="openwrt-23.05" 20 | ;; 21 | *"24.10"*) 22 | branch="openwrt-24.10" 23 | ;; 24 | "SNAPSHOT") 25 | branch="SNAPSHOT" 26 | ;; 27 | *) 28 | echo "unsupported release: $DISTRIB_RELEASE" 29 | exit 1 30 | ;; 31 | esac 32 | 33 | # feed url 34 | repository_url="https://nikkinikki.pages.dev" 35 | feed_url="$repository_url/$branch/$arch/nikki" 36 | 37 | if [ -x "/bin/opkg" ]; then 38 | # download ipks 39 | eval $(curl -s -L $feed_url/index.json | jsonfilter -e 'version=@["packages"]["nikki"]' -e 'app_version=@["packages"]["luci-app-nikki"]' -e 'i18n_version=@["packages"]["luci-i18n-nikki-zh-cn"]') 40 | curl -s -L -J -O $feed_url/nikki_${version}_${arch}.ipk 41 | curl -s -L -J -O $feed_url/luci-app-nikki_${app_version}_all.ipk 42 | curl -s -L -J -O $feed_url/luci-i18n-nikki-zh-cn_${i18n_version}_all.ipk 43 | # update feeds 44 | echo "update feeds" 45 | opkg update 46 | # install ipks 47 | echo "install ipks" 48 | opkg install nikki_*.ipk luci-app-nikki_*.ipk luci-i18n-nikki-zh-cn_*.ipk 49 | rm -f -- *nikki*.ipk 50 | elif [ -x "/usr/bin/apk" ]; then 51 | # add key 52 | echo "add key" 53 | curl -s -L -o "/etc/apk/keys/nikki.pem" "$repository_url/public-key.pem" 54 | # install apks from remote repository 55 | echo "install apks from remote repository" 56 | apk add --repository $feed_url/packages.adb nikki luci-app-nikki luci-i18n-nikki-zh-cn 57 | # remove key 58 | echo "remove key" 59 | rm -f /etc/apk/keys/nikki.pem 60 | fi 61 | 62 | echo "success" 63 | -------------------------------------------------------------------------------- /luci-app-nikki/Makefile: -------------------------------------------------------------------------------- 1 | include $(TOPDIR)/rules.mk 2 | 3 | PKG_VERSION:=1.22.4 4 | 5 | LUCI_TITLE:=LuCI Support for nikki 6 | LUCI_DEPENDS:=+luci-base +nikki 7 | 8 | include $(TOPDIR)/feeds/luci/luci.mk 9 | 10 | # call BuildPackage - OpenWrt buildroot signature -------------------------------------------------------------------------------- /luci-app-nikki/htdocs/luci-static/resources/tools/nikki.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'require baseclass'; 3 | 'require uci'; 4 | 'require fs'; 5 | 'require rpc'; 6 | 'require request'; 7 | 8 | const callRCList = rpc.declare({ 9 | object: 'rc', 10 | method: 'list', 11 | params: ['name'], 12 | expect: { '': {} } 13 | }); 14 | 15 | const callRCInit = rpc.declare({ 16 | object: 'rc', 17 | method: 'init', 18 | params: ['name', 'action'], 19 | expect: { '': {} } 20 | }); 21 | 22 | const callNikkiVersion = rpc.declare({ 23 | object: 'luci.nikki', 24 | method: 'version', 25 | expect: { '': {} } 26 | }); 27 | 28 | const callNikkiProfile = rpc.declare({ 29 | object: 'luci.nikki', 30 | method: 'profile', 31 | params: [ 'defaults' ], 32 | expect: { '': {} } 33 | }); 34 | 35 | const callNikkiUpdateSubscription = rpc.declare({ 36 | object: 'luci.nikki', 37 | method: 'update_subscription', 38 | params: ['section_id'], 39 | expect: { '': {} } 40 | }); 41 | 42 | const callNikkiGetIdentifiers = rpc.declare({ 43 | object: 'luci.nikki', 44 | method: 'get_identifiers', 45 | expect: { '': {} } 46 | }); 47 | 48 | const callNikkiDebug = rpc.declare({ 49 | object: 'luci.nikki', 50 | method: 'debug', 51 | expect: { '': {} } 52 | }); 53 | 54 | const homeDir = '/etc/nikki'; 55 | const profilesDir = `${homeDir}/profiles`; 56 | const subscriptionsDir = `${homeDir}/subscriptions`; 57 | const mixinFilePath = `${homeDir}/mixin.yaml`; 58 | const runDir = `${homeDir}/run`; 59 | const runProfilePath = `${runDir}/config.yaml`; 60 | const providersDir = `${runDir}/providers`; 61 | const ruleProvidersDir = `${providersDir}/rule`; 62 | const proxyProvidersDir = `${providersDir}/proxy`; 63 | const logDir = `/var/log/nikki`; 64 | const appLogPath = `${logDir}/app.log`; 65 | const coreLogPath = `${logDir}/core.log`; 66 | const debugLogPath = `${logDir}/debug.log`; 67 | const nftDir = `${homeDir}/nftables`; 68 | const reservedIPNFT = `${nftDir}/reserved_ip.nft`; 69 | const reservedIP6NFT = `${nftDir}/reserved_ip6.nft`; 70 | 71 | return baseclass.extend({ 72 | homeDir: homeDir, 73 | profilesDir: profilesDir, 74 | subscriptionsDir: subscriptionsDir, 75 | ruleProvidersDir: ruleProvidersDir, 76 | proxyProvidersDir: proxyProvidersDir, 77 | mixinFilePath: mixinFilePath, 78 | runDir: runDir, 79 | appLogPath: appLogPath, 80 | coreLogPath: coreLogPath, 81 | debugLogPath: debugLogPath, 82 | runProfilePath: runProfilePath, 83 | reservedIPNFT: reservedIPNFT, 84 | reservedIP6NFT: reservedIP6NFT, 85 | 86 | status: async function () { 87 | return (await callRCList('nikki'))?.nikki?.running; 88 | }, 89 | 90 | reload: function () { 91 | return callRCInit('nikki', 'reload'); 92 | }, 93 | 94 | restart: function () { 95 | return callRCInit('nikki', 'restart'); 96 | }, 97 | 98 | version: function () { 99 | return callNikkiVersion(); 100 | }, 101 | 102 | profile: function (defaults) { 103 | return callNikkiProfile(defaults); 104 | }, 105 | 106 | updateSubscription: function (section_id) { 107 | return callNikkiUpdateSubscription(section_id); 108 | }, 109 | 110 | api: async function (method, path, query, body) { 111 | const profile = await callNikkiProfile({ 'external-controller': null, 'secret': null }); 112 | const apiListen = profile['external-controller']; 113 | const apiSecret = profile['secret'] ?? ''; 114 | const apiPort = apiListen.substring(apiListen.lastIndexOf(':') + 1); 115 | const url = `http://${window.location.hostname}:${apiPort}${path}`; 116 | return request.request(url, { 117 | method: method, 118 | headers: { 'Authorization': `Bearer ${apiSecret}` }, 119 | query: query, 120 | content: body 121 | }) 122 | }, 123 | 124 | openDashboard: async function () { 125 | const profile = await callNikkiProfile({ 'external-ui-name': null, 'external-controller': null, 'secret': null }); 126 | const uiName = profile['external-ui-name']; 127 | const apiListen = profile['external-controller']; 128 | const apiSecret = profile['secret'] ?? ''; 129 | const apiPort = apiListen.substring(apiListen.lastIndexOf(':') + 1); 130 | const params = { 131 | host: window.location.hostname, 132 | hostname: window.location.hostname, 133 | port: apiPort, 134 | secret: apiSecret 135 | }; 136 | const query = new URLSearchParams(params).toString(); 137 | let url; 138 | if (uiName) { 139 | url = `http://${window.location.hostname}:${apiPort}/ui/${uiName}/?${query}`; 140 | } else { 141 | url = `http://${window.location.hostname}:${apiPort}/ui/?${query}`; 142 | } 143 | setTimeout(function () { window.open(url, '_blank') }, 0); 144 | }, 145 | 146 | updateDashboard: function () { 147 | return this.api('POST', '/upgrade/ui'); 148 | }, 149 | 150 | getIdentifiers: function () { 151 | return callNikkiGetIdentifiers(); 152 | }, 153 | 154 | listProfiles: function () { 155 | return L.resolveDefault(fs.list(this.profilesDir), []); 156 | }, 157 | 158 | listRuleProviders: function () { 159 | return L.resolveDefault(fs.list(this.ruleProvidersDir), []); 160 | }, 161 | 162 | listProxyProviders: function () { 163 | return L.resolveDefault(fs.list(this.proxyProvidersDir), []); 164 | }, 165 | 166 | getAppLog: function () { 167 | return L.resolveDefault(fs.read_direct(this.appLogPath)); 168 | }, 169 | 170 | getCoreLog: function () { 171 | return L.resolveDefault(fs.read_direct(this.coreLogPath)); 172 | }, 173 | 174 | clearAppLog: function () { 175 | return fs.write(this.appLogPath); 176 | }, 177 | 178 | clearCoreLog: function () { 179 | return fs.write(this.coreLogPath); 180 | }, 181 | 182 | debug: function () { 183 | return callNikkiDebug(); 184 | }, 185 | }) 186 | -------------------------------------------------------------------------------- /luci-app-nikki/htdocs/luci-static/resources/view/nikki/app.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'require form'; 3 | 'require view'; 4 | 'require uci'; 5 | 'require poll'; 6 | 'require tools.nikki as nikki'; 7 | 8 | function renderStatus(running) { 9 | return updateStatus(E('input', { id: 'core_status', style: 'border: unset; font-style: italic; font-weight: bold;', readonly: '' }), running); 10 | } 11 | 12 | function updateStatus(element, running) { 13 | if (element) { 14 | element.style.color = running ? 'green' : 'red'; 15 | element.value = running ? _('Running') : _('Not Running'); 16 | } 17 | return element; 18 | } 19 | 20 | return view.extend({ 21 | load: function () { 22 | return Promise.all([ 23 | uci.load('nikki'), 24 | nikki.version(), 25 | nikki.status(), 26 | nikki.listProfiles() 27 | ]); 28 | }, 29 | render: function (data) { 30 | const subscriptions = uci.sections('nikki', 'subscription'); 31 | const appVersion = data[1].app ?? ''; 32 | const coreVersion = data[1].core ?? ''; 33 | const running = data[2]; 34 | const profiles = data[3]; 35 | 36 | let m, s, o; 37 | 38 | m = new form.Map('nikki', _('Nikki'), `${_('Transparent Proxy with Mihomo on OpenWrt.')} ${_('How To Use')}`); 39 | 40 | s = m.section(form.TableSection, 'status', _('Status')); 41 | s.anonymous = true; 42 | 43 | o = s.option(form.Value, '_app_version', _('App Version')); 44 | o.readonly = true; 45 | o.load = function () { 46 | return appVersion; 47 | }; 48 | o.write = function () { }; 49 | 50 | o = s.option(form.Value, '_core_version', _('Core Version')); 51 | o.readonly = true; 52 | o.load = function () { 53 | return coreVersion; 54 | }; 55 | o.write = function () { }; 56 | 57 | o = s.option(form.DummyValue, '_core_status', _('Core Status')); 58 | o.cfgvalue = function () { 59 | return renderStatus(running); 60 | }; 61 | poll.add(function () { 62 | return L.resolveDefault(nikki.status()).then(function (running) { 63 | updateStatus(document.getElementById('core_status'), running); 64 | }); 65 | }); 66 | 67 | o = s.option(form.Button, 'reload'); 68 | o.inputstyle = 'action'; 69 | o.inputtitle = _('Reload Service'); 70 | o.onclick = function () { 71 | return nikki.reload(); 72 | }; 73 | 74 | o = s.option(form.Button, 'restart'); 75 | o.inputstyle = 'negative'; 76 | o.inputtitle = _('Restart Service'); 77 | o.onclick = function () { 78 | return nikki.restart(); 79 | }; 80 | 81 | o = s.option(form.Button, 'update_dashboard'); 82 | o.inputstyle = 'positive'; 83 | o.inputtitle = _('Update Dashboard'); 84 | o.onclick = function () { 85 | return nikki.updateDashboard(); 86 | }; 87 | 88 | o = s.option(form.Button, 'open_dashboard'); 89 | o.inputtitle = _('Open Dashboard'); 90 | o.onclick = function () { 91 | return nikki.openDashboard(); 92 | }; 93 | 94 | s = m.section(form.NamedSection, 'config', 'config', _('App Config')); 95 | 96 | o = s.option(form.Flag, 'enabled', _('Enable')); 97 | o.rmempty = false; 98 | 99 | o = s.option(form.ListValue, 'profile', _('Choose Profile')); 100 | o.optional = true; 101 | 102 | for (const profile of profiles) { 103 | o.value('file:' + profile.name, _('File:') + profile.name); 104 | }; 105 | 106 | for (const subscription of subscriptions) { 107 | o.value('subscription:' + subscription['.name'], _('Subscription:') + subscription.name); 108 | }; 109 | 110 | o = s.option(form.Value, 'start_delay', _('Start Delay')); 111 | o.datatype = 'uinteger'; 112 | o.placeholder = '0'; 113 | 114 | o = s.option(form.Flag, 'scheduled_restart', _('Scheduled Restart')); 115 | o.rmempty = false; 116 | 117 | o = s.option(form.Value, 'cron_expression', _('Cron Expression')); 118 | o.retain = true; 119 | o.rmempty = false; 120 | o.depends('scheduled_restart', '1'); 121 | 122 | o = s.option(form.Flag, 'test_profile', _('Test Profile')); 123 | o.rmempty = false; 124 | 125 | o = s.option(form.Flag, 'fast_reload', _('Fast Reload')); 126 | o.rmempty = false; 127 | 128 | s = m.section(form.NamedSection, 'env', 'env', _('Core Environment Variable Config')); 129 | 130 | o = s.option(form.Flag, 'disable_safe_path_check', _('Disable Safe Path Check')); 131 | o.rmempty = false; 132 | 133 | o = s.option(form.Flag, 'disable_loopback_detector', _('Disable Loopback Detector')); 134 | o.rmempty = false; 135 | 136 | o = s.option(form.Flag, 'disable_quic_go_gso', _('Disable GSO of quic-go')); 137 | o.rmempty = false; 138 | 139 | o = s.option(form.Flag, 'disable_quic_go_ecn', _('Disable ECN of quic-go')); 140 | o.rmempty = false; 141 | 142 | return m.render(); 143 | } 144 | }); 145 | -------------------------------------------------------------------------------- /luci-app-nikki/htdocs/luci-static/resources/view/nikki/editor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'require form'; 3 | 'require view'; 4 | 'require uci'; 5 | 'require fs'; 6 | 'require tools.nikki as nikki' 7 | 8 | return view.extend({ 9 | load: function () { 10 | return Promise.all([ 11 | uci.load('nikki'), 12 | nikki.listProfiles(), 13 | nikki.listRuleProviders(), 14 | nikki.listProxyProviders(), 15 | ]); 16 | }, 17 | render: function (data) { 18 | const subscriptions = uci.sections('nikki', 'subscription'); 19 | const profiles = data[1]; 20 | const ruleProviders = data[2]; 21 | const proxyProviders = data[3]; 22 | 23 | let m, s, o; 24 | 25 | m = new form.Map('nikki'); 26 | 27 | s = m.section(form.NamedSection, 'editor', 'editor', _('Editor')); 28 | 29 | o = s.option(form.ListValue, '_file', _('Choose File')); 30 | o.optional = true; 31 | 32 | for (const profile of profiles) { 33 | o.value(nikki.profilesDir + '/' + profile.name, _('File:') + profile.name); 34 | }; 35 | 36 | for (const subscription of subscriptions) { 37 | o.value(nikki.subscriptionsDir + '/' + subscription['.name'] + '.yaml', _('Subscription:') + subscription.name); 38 | }; 39 | 40 | for (const ruleProvider of ruleProviders) { 41 | o.value(nikki.ruleProvidersDir + '/' + ruleProvider.name, _('Rule Provider:') + ruleProvider.name); 42 | }; 43 | 44 | for (const proxyProvider of proxyProviders) { 45 | o.value(nikki.proxyProvidersDir + '/' + proxyProvider.name, _('Proxy Provider:') + proxyProvider.name); 46 | }; 47 | 48 | o.value(nikki.mixinFilePath, _('File for Mixin')); 49 | o.value(nikki.runProfilePath, _('Profile for Startup')); 50 | o.value(nikki.reservedIPNFT, _('File for Reserved IP')); 51 | o.value(nikki.reservedIP6NFT, _('File for Reserved IP6')); 52 | 53 | o.write = function (section_id, formvalue) { 54 | return true; 55 | }; 56 | o.onchange = function (event, section_id, value) { 57 | return L.resolveDefault(fs.read_direct(value), '').then(function (content) { 58 | m.lookupOption('nikki.editor._file_content')[0].getUIElement('editor').setValue(content); 59 | }); 60 | }; 61 | 62 | o = s.option(form.TextValue, '_file_content',); 63 | o.rows = 25; 64 | o.wrap = false; 65 | o.write = function (section_id, formvalue) { 66 | const path = m.lookupOption('nikki.editor._file')[0].formvalue('editor'); 67 | return fs.write(path, formvalue); 68 | }; 69 | o.remove = function (section_id) { 70 | const path = m.lookupOption('nikki.editor._file')[0].formvalue('editor'); 71 | return fs.write(path); 72 | }; 73 | 74 | return m.render(); 75 | }, 76 | handleSaveApply: function (ev, mode) { 77 | return this.handleSave(ev).finally(function () { 78 | return mode === '0' ? nikki.reload() : nikki.restart(); 79 | }); 80 | }, 81 | handleReset: null 82 | }); 83 | -------------------------------------------------------------------------------- /luci-app-nikki/htdocs/luci-static/resources/view/nikki/log.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'require form'; 3 | 'require view'; 4 | 'require uci'; 5 | 'require fs'; 6 | 'require poll'; 7 | 'require tools.nikki as nikki'; 8 | 9 | return view.extend({ 10 | load: function () { 11 | return Promise.all([ 12 | uci.load('nikki'), 13 | nikki.getAppLog(), 14 | nikki.getCoreLog() 15 | ]); 16 | }, 17 | render: function (data) { 18 | const appLog = data[1]; 19 | const coreLog = data[2]; 20 | 21 | let m, s, o; 22 | 23 | m = new form.Map('nikki'); 24 | 25 | s = m.section(form.NamedSection, 'log', 'log', _('Log')); 26 | 27 | s.tab('app_log', _('App Log')); 28 | 29 | o = s.taboption('app_log', form.Button, 'clear_app_log'); 30 | o.inputstyle = 'negative'; 31 | o.inputtitle = _('Clear Log'); 32 | o.onclick = function (_, section_id) { 33 | m.lookupOption('_app_log', section_id)[0].getUIElement(section_id).setValue(''); 34 | return nikki.clearAppLog(); 35 | }; 36 | 37 | o = s.taboption('app_log', form.TextValue, '_app_log'); 38 | o.rows = 25; 39 | o.wrap = false; 40 | o.load = function (section_id) { 41 | return appLog; 42 | }; 43 | o.write = function (section_id, formvalue) { 44 | return true; 45 | }; 46 | poll.add(L.bind(function () { 47 | const option = this; 48 | return L.resolveDefault(nikki.getAppLog()).then(function (log) { 49 | option.getUIElement('log').setValue(log); 50 | }); 51 | }, o)); 52 | 53 | o = s.taboption('app_log', form.Button, 'scroll_app_log_to_bottom'); 54 | o.inputtitle = _('Scroll To Bottom'); 55 | o.onclick = function (_, section_id) { 56 | const element = m.lookupOption('_app_log', section_id)[0].getUIElement(section_id).node.firstChild; 57 | element.scrollTop = element.scrollHeight; 58 | }; 59 | 60 | s.tab('core_log', _('Core Log')); 61 | 62 | o = s.taboption('core_log', form.Button, 'clear_core_log'); 63 | o.inputstyle = 'negative'; 64 | o.inputtitle = _('Clear Log'); 65 | o.onclick = function (_, section_id) { 66 | m.lookupOption('_core_log', section_id)[0].getUIElement(section_id).setValue(''); 67 | return nikki.clearCoreLog(); 68 | }; 69 | 70 | o = s.taboption('core_log', form.TextValue, '_core_log'); 71 | o.rows = 25; 72 | o.wrap = false; 73 | o.load = function (section_id) { 74 | return coreLog; 75 | }; 76 | o.write = function (section_id, formvalue) { 77 | return true; 78 | }; 79 | poll.add(L.bind(function () { 80 | const option = this; 81 | return L.resolveDefault(nikki.getCoreLog()).then(function (log) { 82 | option.getUIElement('log').setValue(log); 83 | }); 84 | }, o)); 85 | 86 | o = s.taboption('core_log', form.Button, 'scroll_core_log_to_bottom'); 87 | o.inputtitle = _('Scroll To Bottom'); 88 | o.onclick = function (_, section_id) { 89 | const element = m.lookupOption('_core_log', section_id)[0].getUIElement(section_id).node.firstChild; 90 | element.scrollTop = element.scrollHeight; 91 | }; 92 | 93 | s.tab('debug_log', _('Debug Log')); 94 | 95 | o = s.taboption('debug_log', form.Button, '_generate_download_debug_log'); 96 | o.inputstyle = 'negative'; 97 | o.inputtitle = _('Generate & Download'); 98 | o.onclick = function () { 99 | return nikki.debug().then(function () { 100 | fs.read_direct(nikki.debugLogPath, 'blob').then(function (data) { 101 | // create url 102 | const url = window.URL.createObjectURL(data, { type: 'text/markdown' }); 103 | // create link 104 | const link = document.createElement('a'); 105 | link.href = url; 106 | link.download = 'debug.log'; 107 | // append to body 108 | document.body.appendChild(link); 109 | // download 110 | link.click(); 111 | // remove from body 112 | document.body.removeChild(link); 113 | // revoke url 114 | window.URL.revokeObjectURL(url); 115 | }); 116 | }); 117 | }; 118 | 119 | return m.render(); 120 | }, 121 | handleSaveApply: null, 122 | handleSave: null, 123 | handleReset: null 124 | }); -------------------------------------------------------------------------------- /luci-app-nikki/htdocs/luci-static/resources/view/nikki/mixin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'require form'; 3 | 'require view'; 4 | 'require uci'; 5 | 'require fs'; 6 | 'require network'; 7 | 'require poll'; 8 | 'require tools.widgets as widgets'; 9 | 'require tools.nikki as nikki'; 10 | 11 | return view.extend({ 12 | load: function () { 13 | return Promise.all([ 14 | uci.load('nikki'), 15 | network.getNetworks(), 16 | 17 | ]); 18 | }, 19 | render: function (data) { 20 | const networks = data[1]; 21 | 22 | let m, s, o, so; 23 | 24 | m = new form.Map('nikki'); 25 | 26 | s = m.section(form.NamedSection, 'mixin', 'mixin', _('Mixin Option')); 27 | 28 | s.tab('general', _('General Config')); 29 | 30 | o = s.taboption('general', form.ListValue, 'log_level', _('Log Level')); 31 | o.optional = true; 32 | o.placeholder = _('Unmodified'); 33 | o.value('silent'); 34 | o.value('error'); 35 | o.value('warning'); 36 | o.value('info'); 37 | o.value('debug'); 38 | 39 | o = s.taboption('general', form.ListValue, 'mode', _('Mode')); 40 | o.optional = true; 41 | o.placeholder = _('Unmodified'); 42 | o.value('global', _('Global Mode')); 43 | o.value('rule', _('Rule Mode')); 44 | o.value('direct', _('Direct Mode')); 45 | 46 | o = s.taboption('general', form.ListValue, 'match_process', _('Match Process')); 47 | o.optional = true; 48 | o.placeholder = _('Unmodified'); 49 | o.value('off'); 50 | o.value('strict'); 51 | o.value('always'); 52 | 53 | o = s.taboption('general', form.ListValue, 'outbound_interface', _('Outbound Interface')); 54 | o.optional = true; 55 | o.placeholder = _('Unmodified'); 56 | 57 | for (const network of networks) { 58 | if (network.getName() === 'loopback') { 59 | continue; 60 | } 61 | o.value(network.getName()); 62 | } 63 | 64 | o = s.taboption('general', form.ListValue, 'ipv6', 'IPv6'); 65 | o.optional = true; 66 | o.placeholder = _('Unmodified'); 67 | o.value('0', _('Disable')); 68 | o.value('1', _('Enable')); 69 | 70 | o = s.taboption('general', form.ListValue, 'unify_delay', _('Unify Delay')); 71 | o.optional = true; 72 | o.placeholder = _('Unmodified'); 73 | o.value('0', _('Disable')); 74 | o.value('1', _('Enable')); 75 | 76 | o = s.taboption('general', form.ListValue, 'tcp_concurrent', _('TCP Concurrent')); 77 | o.optional = true; 78 | o.placeholder = _('Unmodified'); 79 | o.value('0', _('Disable')); 80 | o.value('1', _('Enable')); 81 | 82 | o = s.taboption('general', form.ListValue, 'disable_tcp_keep_alive', _('Disable TCP Keep Alive')); 83 | o.optional = true; 84 | o.placeholder = _('Unmodified'); 85 | o.value('0', _('Disable')); 86 | o.value('1', _('Enable')); 87 | 88 | o = s.taboption('general', form.Value, 'tcp_keep_alive_idle', _('TCP Keep Alive Idle')); 89 | o.datatype = 'uinteger'; 90 | o.placeholder = _('Unmodified'); 91 | 92 | o = s.taboption('general', form.Value, 'tcp_keep_alive_interval', _('TCP Keep Alive Interval')); 93 | o.datatype = 'uinteger'; 94 | o.placeholder = _('Unmodified'); 95 | 96 | o = s.taboption('general', form.Value, 'global_client_fingerprint', _('Global Client Fingerprint')); 97 | o.placeholder = _('Unmodified'); 98 | o.value('random', _('Random')); 99 | o.value('chrome', 'Chrome'); 100 | o.value('firefox', 'Firefox'); 101 | o.value('safari', 'Safari'); 102 | o.value('edge', 'Edge'); 103 | 104 | s.tab('external_control', _('External Control Config')); 105 | 106 | o = s.taboption('external_control', form.Value, 'ui_path', _('UI Path')); 107 | o.placeholder = _('Unmodified'); 108 | 109 | o = s.taboption('external_control', form.Value, 'ui_name', _('UI Name')); 110 | o.placeholder = _('Unmodified'); 111 | 112 | o = s.taboption('external_control', form.Value, 'ui_url', _('UI Url')); 113 | o.placeholder = _('Unmodified'); 114 | o.value('https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip', 'Zashboard (CDN Fonts)'); 115 | o.value('https://github.com/Zephyruso/zashboard/releases/latest/download/dist.zip', 'Zashboard'); 116 | o.value('https://github.com/MetaCubeX/metacubexd/archive/refs/heads/gh-pages.zip', 'MetaCubeXD'); 117 | o.value('https://github.com/MetaCubeX/Yacd-meta/archive/refs/heads/gh-pages.zip', 'YACD'); 118 | o.value('https://github.com/MetaCubeX/Razord-meta/archive/refs/heads/gh-pages.zip', 'Razord'); 119 | 120 | o = s.taboption('external_control', form.Value, 'api_listen', '*' + ' ' + _('API Listen')); 121 | o.datatype = 'ipaddrport(1)'; 122 | o.placeholder = _('Unmodified'); 123 | o.rmempty = false; 124 | 125 | o = s.taboption('external_control', form.Value, 'api_secret', _('API Secret')); 126 | o.password = true; 127 | o.placeholder = _('Unmodified'); 128 | 129 | o = s.taboption('external_control', form.ListValue, 'selection_cache', _('Save Proxy Selection')); 130 | o.optional = true; 131 | o.placeholder = _('Unmodified'); 132 | o.value('0', _('Disable')); 133 | o.value('1', _('Enable')); 134 | 135 | s.tab('inbound', _('Inbound Config')); 136 | 137 | o = s.taboption('inbound', form.ListValue, 'allow_lan', _('Allow Lan')); 138 | o.optional = true; 139 | o.placeholder = _('Unmodified'); 140 | o.value('0', _('Disable')); 141 | o.value('1', _('Enable')); 142 | 143 | o = s.taboption('inbound', form.Value, 'http_port', _('HTTP Port')); 144 | o.datatype = 'port'; 145 | o.placeholder = _('Unmodified'); 146 | 147 | o = s.taboption('inbound', form.Value, 'socks_port', _('SOCKS Port')); 148 | o.datatype = 'port'; 149 | o.placeholder = _('Unmodified'); 150 | 151 | o = s.taboption('inbound', form.Value, 'mixed_port', _('Mixed Port')); 152 | o.datatype = 'port'; 153 | o.placeholder = _('Unmodified'); 154 | 155 | o = s.taboption('inbound', form.Value, 'redir_port', '*' + ' ' + _('Redirect Port')); 156 | o.datatype = 'port'; 157 | o.placeholder = _('Unmodified'); 158 | o.rmempty = false; 159 | 160 | o = s.taboption('inbound', form.Value, 'tproxy_port', '*' + ' ' + _('TPROXY Port')); 161 | o.datatype = 'port'; 162 | o.placeholder = _('Unmodified'); 163 | o.rmempty = false; 164 | 165 | o = s.taboption('inbound', form.Flag, 'authentication', _('Overwrite Authentication')); 166 | o.rmempty = false; 167 | 168 | o = s.taboption('inbound', form.SectionValue, '_authentications', form.TableSection, 'authentication', _('Edit Authentications')); 169 | o.retain = true; 170 | o.depends('authentication', '1'); 171 | 172 | o.subsection.addremove = true; 173 | o.subsection.anonymous = true; 174 | o.subsection.sortable = true; 175 | 176 | so = o.subsection.option(form.Flag, 'enabled', _('Enable')); 177 | so.rmempty = false; 178 | 179 | so = o.subsection.option(form.Value, 'username', _('Username')); 180 | so.rmempty = false; 181 | 182 | so = o.subsection.option(form.Value, 'password', _('Password')); 183 | so.password = true; 184 | so.rmempty = false; 185 | 186 | s.tab('tun', _('TUN Config')); 187 | 188 | o = s.taboption('tun', form.Value, 'tun_device', '*' + ' ' + _('Device Name')); 189 | o.placeholder = _('Unmodified'); 190 | o.rmempty = false; 191 | 192 | o = s.taboption('tun', form.ListValue, 'tun_stack', _('Stack')); 193 | o.optional = true; 194 | o.placeholder = _('Unmodified'); 195 | o.value('system', 'System'); 196 | o.value('gvisor', 'gVisor'); 197 | o.value('mixed', 'Mixed'); 198 | 199 | o = s.taboption('tun', form.Value, 'tun_mtu', _('MTU')); 200 | o.datatype = 'uinteger'; 201 | o.placeholder = _('Unmodified'); 202 | 203 | o = s.taboption('tun', form.ListValue, 'tun_gso', _('GSO')); 204 | o.optional = true; 205 | o.placeholder = _('Unmodified'); 206 | o.value('0', _('Disable')); 207 | o.value('1', _('Enable')); 208 | 209 | o = s.taboption('tun', form.Value, 'tun_gso_max_size', _('GSO Max Size')); 210 | o.datatype = 'uinteger'; 211 | o.placeholder = _('Unmodified'); 212 | 213 | o = s.taboption('tun', form.ListValue, 'tun_endpoint_independent_nat', _('Endpoint Independent NAT')); 214 | o.optional = true; 215 | o.placeholder = _('Unmodified'); 216 | o.value('0', _('Disable')); 217 | o.value('1', _('Enable')); 218 | 219 | o = s.taboption('tun', form.Flag, 'tun_dns_hijack', _('Overwrite DNS Hijack')); 220 | o.rmempty = false; 221 | 222 | o = s.taboption('tun', form.DynamicList, 'tun_dns_hijacks', _('Edit DNS Hijacks')); 223 | o.retain = true; 224 | o.depends('tun_dns_hijack', '1'); 225 | o.value('tcp://any:53'); 226 | o.value('udp://any:53'); 227 | 228 | s.tab('dns', _('DNS Config')); 229 | 230 | o = s.taboption('dns', form.Value, 'dns_listen', '*' + ' ' + _('DNS Listen')); 231 | o.datatype = 'ipaddrport(1)'; 232 | o.placeholder = _('Unmodified'); 233 | o.rmempty = false; 234 | 235 | o = s.taboption('dns', form.ListValue, 'dns_ipv6', 'IPv6'); 236 | o.optional = true; 237 | o.placeholder = _('Unmodified'); 238 | o.value('0', _('Disable')); 239 | o.value('1', _('Enable')); 240 | 241 | o = s.taboption('dns', form.ListValue, 'dns_mode', '*' + ' ' + _('DNS Mode')); 242 | o.placeholder = _('Unmodified'); 243 | o.value('redir-host', 'Redir-Host'); 244 | o.value('fake-ip', 'Fake-IP'); 245 | 246 | o = s.taboption('dns', form.Value, 'fake_ip_range', '*' + ' ' + _('Fake-IP Range')); 247 | o.datatype = 'cidr4'; 248 | o.placeholder = _('Unmodified'); 249 | o.rmempty = false; 250 | 251 | o = s.taboption('dns', form.Flag, 'fake_ip_filter', _('Overwrite Fake-IP Filter')); 252 | o.rmempty = false; 253 | 254 | o = s.taboption('dns', form.DynamicList, 'fake_ip_filters', _('Edit Fake-IP Filters')); 255 | o.retain = true; 256 | o.depends('fake_ip_filter', '1'); 257 | 258 | o = s.taboption('dns', form.ListValue, 'fake_ip_filter_mode', _('Fake-IP Filter Mode')); 259 | o.optional = true; 260 | o.placeholder = _('Unmodified'); 261 | o.value('blacklist', _('Block Mode')); 262 | o.value('whitelist', _('Allow Mode')); 263 | 264 | o = s.taboption('dns', form.ListValue, 'fake_ip_cache', _('Fake-IP Cache')); 265 | o.optional = true; 266 | o.placeholder = _('Unmodified'); 267 | o.value('0', _('Disable')); 268 | o.value('1', _('Enable')); 269 | 270 | o = s.taboption('dns', form.ListValue, 'dns_respect_rules', _('Respect Rules')); 271 | o.optional = true; 272 | o.placeholder = _('Unmodified'); 273 | o.value('0', _('Disable')); 274 | o.value('1', _('Enable')); 275 | 276 | o = s.taboption('dns', form.ListValue, 'dns_doh_prefer_http3', _('DoH Prefer HTTP/3')); 277 | o.optional = true; 278 | o.placeholder = _('Unmodified'); 279 | o.value('0', _('Disable')); 280 | o.value('1', _('Enable')); 281 | 282 | o = s.taboption('dns', form.ListValue, 'dns_system_hosts', _('Use System Hosts')); 283 | o.optional = true; 284 | o.placeholder = _('Unmodified'); 285 | o.value('0', _('Disable')); 286 | o.value('1', _('Enable')); 287 | 288 | o = s.taboption('dns', form.ListValue, 'dns_hosts', _('Use Hosts')); 289 | o.optional = true; 290 | o.placeholder = _('Unmodified'); 291 | o.value('0', _('Disable')); 292 | o.value('1', _('Enable')); 293 | 294 | o = s.taboption('dns', form.Flag, 'hosts', _('Overwrite Hosts')); 295 | o.rmempty = false; 296 | 297 | o = s.taboption('dns', form.SectionValue, '_hosts', form.TableSection, 'hosts', _('Edit Hosts')); 298 | o.retain = true; 299 | o.depends('hosts', '1'); 300 | 301 | o.subsection.addremove = true; 302 | o.subsection.anonymous = true; 303 | o.subsection.sortable = true; 304 | 305 | so = o.subsection.option(form.Flag, 'enabled', _('Enable')); 306 | so.rmempty = false; 307 | 308 | so = o.subsection.option(form.Value, 'domain_name', _('Domain Name')); 309 | so.rmempty = false; 310 | 311 | so = o.subsection.option(form.DynamicList, 'ip', 'IP'); 312 | 313 | o = s.taboption('dns', form.Flag, 'dns_nameserver', _('Overwrite Nameserver')); 314 | o.rmempty = false; 315 | 316 | o = s.taboption('dns', form.SectionValue, '_dns_nameservers', form.TableSection, 'nameserver', _('Edit Nameservers')); 317 | o.retain = true; 318 | o.depends('dns_nameserver', '1'); 319 | 320 | o.subsection.addremove = true; 321 | o.subsection.anonymous = true; 322 | o.subsection.sortable = true; 323 | 324 | so = o.subsection.option(form.Flag, 'enabled', _('Enable')); 325 | so.rmempty = false; 326 | 327 | so = o.subsection.option(form.ListValue, 'type', _('Type')); 328 | so.value('default-nameserver'); 329 | so.value('proxy-server-nameserver'); 330 | so.value('direct-nameserver'); 331 | so.value('nameserver'); 332 | so.value('fallback'); 333 | 334 | so = o.subsection.option(form.DynamicList, 'nameserver', _('Nameserver')); 335 | 336 | o = s.taboption('dns', form.Flag, 'dns_nameserver_policy', _('Overwrite Nameserver Policy')); 337 | o.rmempty = false; 338 | 339 | o = s.taboption('dns', form.SectionValue, '_dns_nameserver_policies', form.TableSection, 'nameserver_policy', _('Edit Nameserver Policies')); 340 | o.retain = true; 341 | o.depends('dns_nameserver_policy', '1'); 342 | 343 | o.subsection.addremove = true; 344 | o.subsection.anonymous = true; 345 | o.subsection.sortable = true; 346 | 347 | so = o.subsection.option(form.Flag, 'enabled', _('Enable')); 348 | so.rmempty = false; 349 | 350 | so = o.subsection.option(form.Value, 'matcher', _('Matcher')); 351 | so.rmempty = false; 352 | 353 | so = o.subsection.option(form.DynamicList, 'nameserver', _('Nameserver')); 354 | 355 | s.tab('sniffer', _('Sniffer Config')); 356 | 357 | o = s.taboption('sniffer', form.ListValue, 'sniffer', _('Enable')); 358 | o.optional = true; 359 | o.placeholder = _('Unmodified'); 360 | o.value('0', _('Disable')); 361 | o.value('1', _('Enable')); 362 | 363 | o = s.taboption('sniffer', form.ListValue, 'sniffer_sniff_dns_mapping', _('Sniff Redir-Host')); 364 | o.optional = true; 365 | o.placeholder = _('Unmodified'); 366 | o.value('0', _('Disable')); 367 | o.value('1', _('Enable')); 368 | 369 | o = s.taboption('sniffer', form.ListValue, 'sniffer_sniff_pure_ip', _('Sniff Pure IP')); 370 | o.optional = true; 371 | o.placeholder = _('Unmodified'); 372 | o.value('0', _('Disable')); 373 | o.value('1', _('Enable')); 374 | 375 | o = s.taboption('sniffer', form.Flag, 'sniffer_force_domain_name', _('Overwrite Force Sniff Domain Name')); 376 | o.rmempty = false; 377 | 378 | o = s.taboption('sniffer', form.DynamicList, 'sniffer_force_domain_names', _('Force Sniff Domain Name')); 379 | o.retain = true; 380 | o.depends('sniffer_force_domain_name', '1'); 381 | 382 | o = s.taboption('sniffer', form.Flag, 'sniffer_ignore_domain_name', _('Overwrite Ignore Sniff Domain Name')); 383 | o.rmempty = false; 384 | 385 | o = s.taboption('sniffer', form.DynamicList, 'sniffer_ignore_domain_names', _('Ignore Sniff Domain Name')); 386 | o.retain = true; 387 | o.depends('sniffer_ignore_domain_name', '1'); 388 | 389 | o = s.taboption('sniffer', form.Flag, 'sniffer_sniff', _('Overwrite Sniff By Protocol')); 390 | o.rmempty = false; 391 | 392 | o = s.taboption('sniffer', form.SectionValue, '_sniffer_sniffs', form.TableSection, 'sniff', _('Sniff By Protocol')); 393 | o.retain = true; 394 | o.depends('sniffer_sniff', '1'); 395 | 396 | o.subsection.anonymous = true; 397 | o.subsection.addremove = false; 398 | 399 | so = o.subsection.option(form.Flag, 'enabled', _('Enable')); 400 | so.rmempty = false; 401 | 402 | so = o.subsection.option(form.ListValue, 'protocol', _('Protocol')); 403 | so.value('HTTP'); 404 | so.value('TLS'); 405 | so.value('QUIC'); 406 | so.readonly = true; 407 | 408 | so = o.subsection.option(form.DynamicList, 'port', _('Port')); 409 | so.datatype = 'portrange'; 410 | 411 | so = o.subsection.option(form.Flag, 'overwrite_destination', _('Overwrite Destination')); 412 | so.rmempty = false; 413 | 414 | s.tab('rule', _('Rule Config')); 415 | 416 | o = s.taboption('rule', form.Flag, 'rule_provider', _('Append Rule Provider')); 417 | o.rmempty = false; 418 | 419 | o = s.taboption('rule', form.SectionValue, '_rule_providers', form.GridSection, 'rule_provider', _('Edit Rule Providers')); 420 | o.retain = true; 421 | o.depends('rule_provider', '1'); 422 | 423 | o.subsection.anonymous = true; 424 | o.subsection.addremove = true; 425 | o.subsection.sortable = true; 426 | 427 | so = o.subsection.option(form.Flag, 'enabled', _('Enable')); 428 | so.default = 1; 429 | so.editable = true; 430 | so.modalonly = false; 431 | so.rmempty = false; 432 | 433 | so = o.subsection.option(form.Value, 'name', _('Name')); 434 | so.rmempty = false; 435 | 436 | so = o.subsection.option(form.ListValue, 'type', _('Type')); 437 | so.default = 'http'; 438 | so.rmempty = false; 439 | so.value('http'); 440 | so.value('file'); 441 | 442 | so = o.subsection.option(form.Value, 'url', _('Url')); 443 | so.modalonly = true; 444 | so.rmempty = false; 445 | so.depends('type', 'http'); 446 | 447 | so = o.subsection.option(form.Value, 'node', _('Node')); 448 | so.default = 'DIRECT'; 449 | so.modalonly = true; 450 | so.depends('type', 'http'); 451 | so.value('GLOBAL'); 452 | so.value('DIRECT'); 453 | 454 | so = o.subsection.option(form.Value, 'file_size_limit', _('File Size Limit')); 455 | so.datatype = 'uinteger'; 456 | so.default = 0; 457 | so.modalonly = true; 458 | so.depends('type', 'http'); 459 | 460 | so = o.subsection.option(form.FileUpload, 'file_path', _('File Path')); 461 | so.modalonly = true; 462 | so.rmempty = false; 463 | so.root_directory = nikki.ruleProvidersDir; 464 | so.depends('type', 'file'); 465 | 466 | so = o.subsection.option(form.ListValue, 'file_format', _('File Format')); 467 | so.default = 'yaml'; 468 | so.value('mrs'); 469 | so.value('yaml'); 470 | so.value('text'); 471 | 472 | so = o.subsection.option(form.ListValue, 'behavior', _('Behavior')); 473 | so.default = 'classical'; 474 | so.rmempty = false; 475 | so.value('classical'); 476 | so.value('domain'); 477 | so.value('ipcidr'); 478 | 479 | so = o.subsection.option(form.Value, 'update_interval', _('Update Interval')); 480 | so.datatype = 'uinteger'; 481 | so.default = 0; 482 | so.modalonly = true; 483 | so.depends('type', 'http'); 484 | 485 | o = s.taboption('rule', form.Flag, 'rule', _('Append Rule')); 486 | o.rmempty = false; 487 | 488 | o = s.taboption('rule', form.SectionValue, '_rules', form.TableSection, 'rule', _('Edit Rules')); 489 | o.retain = true; 490 | o.depends('rule', '1'); 491 | 492 | o.subsection.anonymous = true; 493 | o.subsection.addremove = true; 494 | o.subsection.sortable = true; 495 | 496 | so = o.subsection.option(form.Flag, 'enabled', _('Enable')); 497 | so.default = 1; 498 | so.rmempty = false; 499 | 500 | so = o.subsection.option(form.Value, 'type', _('Type')); 501 | so.rmempty = false; 502 | so.value('RULE-SET', _('Rule Set')); 503 | so.value('DOMAIN', _('Domain Name')); 504 | so.value('DOMAIN-SUFFIX', _('Domain Name Suffix')); 505 | so.value('DOMAIN-KEYWORD', _('Domain Name Keyword')); 506 | so.value('DOMAIN-REGEX', _('Domain Name Regex')); 507 | so.value('IP-CIDR', _('Destination IP')); 508 | so.value('DST-PORT', _('Destination Port')); 509 | so.value('PROCESS-NAME', _('Process Name')); 510 | so.value('GEOSITE', _('Domain Name Geo')); 511 | so.value('GEOIP', _('Destination IP Geo')); 512 | 513 | so = o.subsection.option(form.Value, 'matcher', _('Matcher')); 514 | so.rmempty = false; 515 | so.depends({ 'type': /MATCH/i, '!reverse': true }); 516 | 517 | so = o.subsection.option(form.Value, 'node', _('Node')); 518 | so.default = 'GLOBAL'; 519 | so.value('GLOBAL'); 520 | so.value('DIRECT'); 521 | so.value('REJECT'); 522 | so.value('REJECT-DROP'); 523 | 524 | so = o.subsection.option(form.Flag, 'no_resolve', _('No Resolve')); 525 | so.rmempty = false; 526 | so.depends('type', /IP-CIDR6?/i); 527 | so.depends('type', /GEOIP/i); 528 | 529 | s.tab('geox', _('GeoX Config')); 530 | 531 | o = s.taboption('geox', form.ListValue, 'geoip_format', _('GeoIP Format')); 532 | o.optional = true; 533 | o.placeholder = _('Unmodified'); 534 | o.value('dat', 'DAT'); 535 | o.value('mmdb', 'MMDB'); 536 | 537 | o = s.taboption('geox', form.ListValue, 'geodata_loader', _('GeoData Loader')); 538 | o.optional = true; 539 | o.placeholder = _('Unmodified'); 540 | o.value('standard', _('Standard Loader')); 541 | o.value('memconservative', _('Memory Conservative Loader')); 542 | 543 | o = s.taboption('geox', form.Value, 'geosite_url', _('GeoSite Url')); 544 | o.placeholder = _('Unmodified'); 545 | 546 | o = s.taboption('geox', form.Value, 'geoip_mmdb_url', _('GeoIP(MMDB) Url')); 547 | o.placeholder = _('Unmodified'); 548 | 549 | o = s.taboption('geox', form.Value, 'geoip_dat_url', _('GeoIP(DAT) Url')); 550 | o.placeholder = _('Unmodified'); 551 | 552 | o = s.taboption('geox', form.Value, 'geoip_asn_url', _('GeoIP(ASN) Url')); 553 | o.placeholder = _('Unmodified'); 554 | 555 | o = s.taboption('geox', form.ListValue, 'geox_auto_update', _('GeoX Auto Update')); 556 | o.optional = true; 557 | o.placeholder = _('Unmodified'); 558 | o.value('0', _('Disable')); 559 | o.value('1', _('Enable')); 560 | 561 | o = s.taboption('geox', form.Value, 'geox_update_interval', _('GeoX Update Interval')); 562 | o.datatype = 'uinteger'; 563 | o.placeholder = _('Unmodified'); 564 | 565 | s.tab('mixin_file_content', _('Mixin File Content')); 566 | 567 | o = s.taboption('mixin_file_content', form.Flag, 'mixin_file_content', _('Enable'), _('Please go to the editor tab to edit the file for mixin')); 568 | o.rmempty = false; 569 | 570 | return m.render(); 571 | } 572 | }); -------------------------------------------------------------------------------- /luci-app-nikki/htdocs/luci-static/resources/view/nikki/profile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'require form'; 3 | 'require view'; 4 | 'require uci'; 5 | 'require tools.nikki as nikki'; 6 | 7 | return view.extend({ 8 | load: function () { 9 | return Promise.all([ 10 | uci.load('nikki') 11 | ]); 12 | }, 13 | render: function (data) { 14 | let m, s, o, so; 15 | 16 | m = new form.Map('nikki'); 17 | 18 | s = m.section(form.NamedSection, 'config', 'config', _('Profile')); 19 | 20 | o = s.option(form.FileUpload, '_upload_profile', _('Upload Profile')); 21 | o.browser = true; 22 | o.enable_download = true; 23 | o.root_directory = nikki.profilesDir; 24 | o.write = function (section_id, formvalue) { 25 | return true; 26 | }; 27 | 28 | s = m.section(form.GridSection, 'subscription', _('Subscription')); 29 | s.addremove = true; 30 | s.anonymous = true; 31 | s.sortable = true; 32 | s.modaltitle = _('Edit Subscription'); 33 | 34 | o = s.option(form.Value, 'name', _('Subscription Name')); 35 | o.rmempty = false; 36 | 37 | o = s.option(form.Value, 'used', _('Used')); 38 | o.modalonly = false; 39 | o.optional = true; 40 | o.readonly = true; 41 | 42 | o = s.option(form.Value, 'total', _('Total')); 43 | o.modalonly = false; 44 | o.optional = true; 45 | o.readonly = true; 46 | 47 | o = s.option(form.Value, 'expire', _('Expire At')); 48 | o.modalonly = false; 49 | o.optional = true; 50 | o.readonly = true; 51 | 52 | o = s.option(form.Value, 'update', _('Update At')); 53 | o.modalonly = false; 54 | o.optional = true; 55 | o.readonly = true; 56 | 57 | o = s.option(form.Button, 'update_subscription'); 58 | o.editable = true; 59 | o.inputstyle = 'positive'; 60 | o.inputtitle = _('Update'); 61 | o.modalonly = false; 62 | o.onclick = function (_, section_id) { 63 | return nikki.updateSubscription(section_id); 64 | }; 65 | 66 | o = s.option(form.Value, 'url', _('Subscription Url')); 67 | o.modalonly = true; 68 | o.rmempty = false; 69 | 70 | o = s.option(form.Value, 'user_agent', _('User Agent')); 71 | o.default = 'clash'; 72 | o.modalonly = true; 73 | o.rmempty = false; 74 | o.value('clash'); 75 | o.value('clash.meta'); 76 | o.value('mihomo'); 77 | 78 | o = s.option(form.ListValue, 'prefer', _('Prefer')); 79 | o.default = 'remote'; 80 | o.modalonly = true; 81 | o.value('remote', _('Remote')); 82 | o.value('local', _('Local')); 83 | 84 | return m.render(); 85 | } 86 | }); 87 | -------------------------------------------------------------------------------- /luci-app-nikki/htdocs/luci-static/resources/view/nikki/proxy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'require form'; 3 | 'require view'; 4 | 'require uci'; 5 | 'require network'; 6 | 'require tools.widgets as widgets'; 7 | 'require tools.nikki as nikki'; 8 | 9 | return view.extend({ 10 | load: function () { 11 | return Promise.all([ 12 | uci.load('nikki'), 13 | network.getHostHints(), 14 | network.getNetworks(), 15 | nikki.getIdentifiers(), 16 | ]); 17 | }, 18 | render: function (data) { 19 | const hosts = data[1].hosts; 20 | const networks = data[2]; 21 | const users = data[3]?.users ?? []; 22 | const groups = data[3]?.groups ?? []; 23 | const cgroups = data[3]?.cgroups ?? []; 24 | 25 | let m, s, o, so; 26 | 27 | m = new form.Map('nikki'); 28 | 29 | s = m.section(form.NamedSection, 'proxy', 'proxy', _('Proxy Config')); 30 | 31 | s.tab('proxy', _('Proxy Config')); 32 | 33 | o = s.taboption('proxy', form.Flag, 'enabled', _('Enable')); 34 | o.rmempty = false; 35 | 36 | o = s.taboption('proxy', form.ListValue, 'tcp_mode', _('TCP Mode')); 37 | o.optional = true; 38 | o.placeholder = _('Disable'); 39 | o.value('redirect', _('Redirect Mode')); 40 | o.value('tproxy', _('TPROXY Mode')); 41 | o.value('tun', _('TUN Mode')); 42 | 43 | o = s.taboption('proxy', form.ListValue, 'udp_mode', _('UDP Mode')); 44 | o.optional = true; 45 | o.placeholder = _('Disable'); 46 | o.value('tproxy', _('TPROXY Mode')); 47 | o.value('tun', _('TUN Mode')); 48 | 49 | o = s.taboption('proxy', form.Flag, 'ipv4_dns_hijack', _('IPv4 DNS Hijack')); 50 | o.rmempty = false; 51 | 52 | o = s.taboption('proxy', form.Flag, 'ipv6_dns_hijack', _('IPv6 DNS Hijack')); 53 | o.rmempty = false; 54 | 55 | o = s.taboption('proxy', form.Flag, 'ipv4_proxy', _('IPv4 Proxy')); 56 | o.rmempty = false; 57 | 58 | o = s.taboption('proxy', form.Flag, 'ipv6_proxy', _('IPv6 Proxy')); 59 | o.rmempty = false; 60 | 61 | o = s.taboption('proxy', form.Flag, 'fake_ip_ping_hijack', _('Fake-IP Ping Hijack')); 62 | o.rmempty = false; 63 | 64 | s.tab('router', _('Router Proxy')); 65 | 66 | o = s.taboption('router', form.Flag, 'router_proxy', _('Enable')); 67 | o.rmempty = false; 68 | 69 | o = s.taboption('router', form.SectionValue, '_router_access_control', form.TableSection, 'router_access_control', _('Access Control')); 70 | o.retain = true; 71 | o.depends('router_proxy', '1'); 72 | 73 | o.subsection.addremove = true; 74 | o.subsection.anonymous = true; 75 | o.subsection.sortable = true; 76 | 77 | so = o.subsection.option(form.Flag, 'enabled', _('Enable')); 78 | so.default = '1'; 79 | so.rmempty = false; 80 | 81 | so = o.subsection.option(form.DynamicList, 'user', _('User')); 82 | 83 | for (const user of users) { 84 | so.value(user); 85 | }; 86 | 87 | so = o.subsection.option(form.DynamicList, 'group', _('Group')); 88 | 89 | for (const group of groups) { 90 | so.value(group); 91 | }; 92 | 93 | so = o.subsection.option(form.DynamicList, 'cgroup', _('CGroup')); 94 | 95 | for (const cgroup of cgroups) { 96 | so.value(cgroup); 97 | }; 98 | 99 | so = o.subsection.option(form.Flag, 'proxy', _('Proxy')); 100 | so.rmempty = false; 101 | 102 | s.tab('lan', _('LAN Proxy')); 103 | 104 | o = s.taboption('lan', form.Flag, 'lan_proxy', _('Enable')); 105 | 106 | o = s.taboption('lan', form.DynamicList, 'lan_inbound_interface', _('Inbound Interface')); 107 | o.retain = true; 108 | o.rmempty = false; 109 | o.depends('lan_proxy', '1'); 110 | 111 | for (const network of networks) { 112 | if (network.getName() === 'loopback') { 113 | continue; 114 | } 115 | o.value(network.getName()); 116 | } 117 | 118 | o = s.taboption('lan', form.SectionValue, '_lan_access_control', form.TableSection, 'lan_access_control', _('Access Control')); 119 | o.retain = true; 120 | o.depends('lan_proxy', '1'); 121 | 122 | o.subsection.addremove = true; 123 | o.subsection.anonymous = true; 124 | o.subsection.sortable = true; 125 | 126 | so = o.subsection.option(form.Flag, 'enabled', _('Enable')); 127 | so.default = '1'; 128 | so.rmempty = false; 129 | 130 | so = o.subsection.option(form.DynamicList, 'ip', 'IP'); 131 | 132 | for (const mac in hosts) { 133 | const host = hosts[mac]; 134 | for (const ip of host.ipaddrs) { 135 | const hint = host.name ?? mac; 136 | so.value(ip, hint ? '%s (%s)'.format(ip, hint) : ip); 137 | }; 138 | }; 139 | 140 | so = o.subsection.option(form.DynamicList, 'ip6', 'IP6'); 141 | 142 | for (const mac in hosts) { 143 | const host = hosts[mac]; 144 | for (const ip of host.ip6addrs) { 145 | const hint = host.name ?? mac; 146 | so.value(ip, hint ? '%s (%s)'.format(ip, hint) : ip); 147 | }; 148 | }; 149 | 150 | so = o.subsection.option(form.DynamicList, 'mac', 'MAC'); 151 | 152 | for (const mac in hosts) { 153 | const host = hosts[mac]; 154 | const hint = host.name ?? host.ipaddrs[0]; 155 | so.value(mac, hint ? '%s (%s)'.format(mac, hint) : mac); 156 | }; 157 | 158 | so = o.subsection.option(form.Flag, 'proxy', _('Proxy')); 159 | so.rmempty = false; 160 | 161 | s.tab('bypass', _('Bypass')); 162 | 163 | o = s.taboption('bypass', form.Flag, 'bypass_china_mainland_ip', _('Bypass China Mainland IP')); 164 | o.rmempty = false; 165 | 166 | o = s.taboption('bypass', form.Value, 'proxy_tcp_dport', _('Destination TCP Port to Proxy')); 167 | o.rmempty = false; 168 | o.value('0-65535', _('All Port')); 169 | o.value('21 22 80 110 143 194 443 465 853 993 995 8080 8443', _('Commonly Used Port')); 170 | 171 | o = s.taboption('bypass', form.Value, 'proxy_udp_dport', _('Destination UDP Port to Proxy')); 172 | o.rmempty = false; 173 | o.value('0-65535', _('All Port')); 174 | o.value('123 443 8443', _('Commonly Used Port')); 175 | 176 | o = s.taboption('bypass', form.DynamicList, 'bypass_dscp', _('Bypass DSCP')); 177 | o.datatype = 'range(0, 63)'; 178 | 179 | return m.render(); 180 | } 181 | }); 182 | -------------------------------------------------------------------------------- /luci-app-nikki/root/usr/share/luci/menu.d/luci-app-nikki.json: -------------------------------------------------------------------------------- 1 | { 2 | "admin/services/nikki": { 3 | "title": "Nikki", 4 | "action": { 5 | "type": "firstchild" 6 | }, 7 | "depends": { 8 | "acl": [ "luci-app-nikki" ], 9 | "uci": { "nikki": true } 10 | } 11 | }, 12 | "admin/services/nikki/config": { 13 | "title": "App Config", 14 | "order": 10, 15 | "action": { 16 | "type": "view", 17 | "path": "nikki/app" 18 | } 19 | }, 20 | "admin/services/nikki/profile": { 21 | "title": "Profile", 22 | "order": 20, 23 | "action": { 24 | "type": "view", 25 | "path": "nikki/profile" 26 | } 27 | }, 28 | "admin/services/nikki/mixin": { 29 | "title": "Mixin Config", 30 | "order": 30, 31 | "action": { 32 | "type": "view", 33 | "path": "nikki/mixin" 34 | } 35 | }, 36 | "admin/services/nikki/proxy": { 37 | "title": "Proxy Config", 38 | "order": 40, 39 | "action": { 40 | "type": "view", 41 | "path": "nikki/proxy" 42 | } 43 | }, 44 | "admin/services/nikki/editor": { 45 | "title": "Editor", 46 | "order": 50, 47 | "action": { 48 | "type": "view", 49 | "path": "nikki/editor" 50 | } 51 | }, 52 | "admin/services/nikki/log": { 53 | "title": "Log", 54 | "order": 60, 55 | "action": { 56 | "type": "view", 57 | "path": "nikki/log" 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /luci-app-nikki/root/usr/share/rpcd/acl.d/luci-app-nikki.json: -------------------------------------------------------------------------------- 1 | { 2 | "luci-app-nikki": { 3 | "description": "Grant access to nikki procedures", 4 | "read": { 5 | "uci": [ "nikki" ], 6 | "ubus": { 7 | "rc": [ "*" ], 8 | "luci.nikki": [ "*" ] 9 | }, 10 | "file": { 11 | "/etc/nikki/profiles/*.yaml": ["read"], 12 | "/etc/nikki/profiles/*.yml": ["read"], 13 | "/etc/nikki/subscriptions/*.yaml": ["read"], 14 | "/etc/nikki/subscriptions/*.yml": ["read"], 15 | "/etc/nikki/mixin.yaml": ["read"], 16 | "/etc/nikki/run/config.yaml": ["read"], 17 | "/etc/nikki/run/providers/rule/*": ["read"], 18 | "/etc/nikki/run/providers/proxy/*": ["read"], 19 | "/etc/nikki/nftables/reserved_ip.nft": ["read"], 20 | "/etc/nikki/nftables/reserved_ip6.nft": ["read"], 21 | "/var/log/nikki/*.log": ["read"] 22 | } 23 | }, 24 | "write": { 25 | "uci": [ "nikki" ], 26 | "file": { 27 | "/etc/nikki/profiles/*.yaml": ["write"], 28 | "/etc/nikki/profiles/*.yml": ["write"], 29 | "/etc/nikki/subscriptions/*.yaml": ["write"], 30 | "/etc/nikki/subscriptions/*.yml": ["write"], 31 | "/etc/nikki/mixin.yaml": ["write"], 32 | "/etc/nikki/run/config.yaml": ["write"], 33 | "/etc/nikki/run/providers/rule/*": ["write"], 34 | "/etc/nikki/run/providers/proxy/*": ["write"], 35 | "/etc/nikki/nftables/reserved_ip.nft": ["write"], 36 | "/etc/nikki/nftables/reserved_ip6.nft": ["write"], 37 | "/var/log/nikki/*.log": ["write"] 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /luci-app-nikki/root/usr/share/rpcd/ucode/luci.nikki: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ucode 2 | 3 | 'use strict'; 4 | 5 | import { access, popen, writefile } from 'fs'; 6 | import { get_users, get_groups, get_cgroups } from '/etc/nikki/ucode/include.uc'; 7 | 8 | const methods = { 9 | version: { 10 | call: function() { 11 | let process; 12 | let app = ''; 13 | if (system('command -v opkg') == 0) { 14 | process = popen('opkg list-installed luci-app-nikki | cut -d " " -f 3'); 15 | if (process) { 16 | app = trim(process.read('all')); 17 | process.close(); 18 | } 19 | } else if (system('command -v apk') == 0) { 20 | process = popen('apk list -I luci-app-nikki | cut -d " " -f 1 | cut -d "-" -f 4'); 21 | if (process) { 22 | app = trim(process.read('all')); 23 | process.close(); 24 | } 25 | } 26 | let core = ''; 27 | process = popen('mihomo -v | grep Mihomo | cut -d " " -f 3'); 28 | if (process) { 29 | core = trim(process.read('all')); 30 | process.close(); 31 | } 32 | return { app: app, core: core }; 33 | } 34 | }, 35 | profile: { 36 | args: { defaults: {} }, 37 | call: function(req) { 38 | let profile = {}; 39 | const defaults = req.args?.defaults ?? {}; 40 | const filepath = '/etc/nikki/run/config.yaml'; 41 | const tmpFilepath = '/var/run/nikki/profile.json'; 42 | if (access(filepath, 'r')) { 43 | writefile(tmpFilepath, defaults); 44 | const command = `yq -p yaml -o json eval-all 'select(fi == 0) *? select(fi == 1)' ${tmpFilepath} ${filepath}`; 45 | const process = popen(command); 46 | if (process) { 47 | profile = json(process); 48 | process.close(); 49 | } 50 | } 51 | return profile; 52 | } 53 | }, 54 | update_subscription: { 55 | args: { section_id: 'section_id' }, 56 | call: function(req) { 57 | let success = false; 58 | const section_id = req.args?.section_id; 59 | if (section_id) { 60 | success = system(['service', 'nikki', 'update_subscription', section_id]) == 0; 61 | } 62 | return { success: success }; 63 | } 64 | }, 65 | get_identifiers: { 66 | call: function() { 67 | const users = filter(get_users(), (x) => x != ''); 68 | const groups = filter(get_groups(), (x) => x != ''); 69 | const cgroups = filter(get_cgroups(), (x) => x != '' && x != 'nikki'); 70 | return { users: users, groups: groups, cgroups: cgroups }; 71 | } 72 | }, 73 | debug: { 74 | call: function() { 75 | const success = system('/etc/nikki/scripts/debug.sh > /var/log/nikki/debug.log') == 0; 76 | return { success: success }; 77 | } 78 | } 79 | }; 80 | 81 | return { 'luci.nikki': methods }; -------------------------------------------------------------------------------- /nikki/Makefile: -------------------------------------------------------------------------------- 1 | include $(TOPDIR)/rules.mk 2 | 3 | PKG_NAME:=nikki 4 | PKG_RELEASE:=1 5 | 6 | PKG_SOURCE_PROTO:=git 7 | PKG_SOURCE_URL:=https://github.com/MetaCubeX/mihomo.git 8 | PKG_SOURCE_DATE:=2025-05-31 9 | PKG_SOURCE_VERSION:=71a87056367cd4fe461a4c9989aa46036d82c85c 10 | PKG_MIRROR_HASH:=d1a7a77386a7d3cc9e0f949ae718a41b742f35630a685c75e365329607d6aed9 11 | 12 | PKG_LICENSE:=GPL3.0+ 13 | PKG_MAINTAINER:=Joseph Mory 14 | 15 | PKG_BUILD_DEPENDS:=golang/host 16 | PKG_BUILD_PARALLEL:=1 17 | PKG_BUILD_FLAGS:=no-mips16 18 | 19 | PKG_BUILD_VERSION:=alpha-71a8705 20 | PKG_BUILD_TIME:=$(shell date -u -Iseconds) 21 | 22 | GO_PKG:=github.com/metacubex/mihomo 23 | GO_PKG_LDFLAGS_X:=$(GO_PKG)/constant.Version=$(PKG_BUILD_VERSION) $(GO_PKG)/constant.BuildTime=$(PKG_BUILD_TIME) 24 | GO_PKG_TAGS:=with_gvisor 25 | 26 | include $(INCLUDE_DIR)/package.mk 27 | include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk 28 | 29 | define Package/nikki 30 | SECTION:=net 31 | CATEGORY:=Network 32 | TITLE:=A rule based proxy in Go. 33 | URL:=https://wiki.metacubex.one 34 | DEPENDS:=$(GO_ARCH_DEPENDS) +ca-bundle +curl +yq firewall4 +ip-full +kmod-inet-diag +kmod-nft-socket +kmod-nft-tproxy +kmod-tun 35 | PROVIDES:=nikki mihomo 36 | endef 37 | 38 | define Package/nikki/description 39 | A rule based proxy in Go. 40 | endef 41 | 42 | define Package/nikki/conffiles 43 | /etc/config/nikki 44 | /etc/nikki/mixin.yaml 45 | /etc/nikki/nftables/reserved_ip.nft 46 | /etc/nikki/nftables/reserved_ip6.nft 47 | endef 48 | 49 | define Package/nikki/install 50 | $(call GoPackage/Package/Install/Bin,$(1)) 51 | 52 | $(INSTALL_DIR) $(1)/etc/nikki 53 | $(INSTALL_DIR) $(1)/etc/nikki/ucode 54 | $(INSTALL_DIR) $(1)/etc/nikki/scripts 55 | $(INSTALL_DIR) $(1)/etc/nikki/nftables 56 | $(INSTALL_DIR) $(1)/etc/nikki/profiles 57 | $(INSTALL_DIR) $(1)/etc/nikki/subscriptions 58 | $(INSTALL_DIR) $(1)/etc/nikki/run 59 | $(INSTALL_DIR) $(1)/etc/nikki/run/providers 60 | $(INSTALL_DIR) $(1)/etc/nikki/run/providers/rule 61 | $(INSTALL_DIR) $(1)/etc/nikki/run/providers/proxy 62 | 63 | $(INSTALL_DATA) $(CURDIR)/files/mixin.yaml $(1)/etc/nikki/mixin.yaml 64 | 65 | $(INSTALL_BIN) $(CURDIR)/files/ucode/include.uc $(1)/etc/nikki/ucode/include.uc 66 | $(INSTALL_BIN) $(CURDIR)/files/ucode/mixin.uc $(1)/etc/nikki/ucode/mixin.uc 67 | $(INSTALL_BIN) $(CURDIR)/files/ucode/hijack.ut $(1)/etc/nikki/ucode/hijack.ut 68 | 69 | $(INSTALL_BIN) $(CURDIR)/files/scripts/include.sh $(1)/etc/nikki/scripts/include.sh 70 | $(INSTALL_BIN) $(CURDIR)/files/scripts/firewall_include.sh $(1)/etc/nikki/scripts/firewall_include.sh 71 | $(INSTALL_BIN) $(CURDIR)/files/scripts/debug.sh $(1)/etc/nikki/scripts/debug.sh 72 | 73 | $(INSTALL_BIN) $(CURDIR)/files/nftables/reserved_ip.nft $(1)/etc/nikki/nftables/reserved_ip.nft 74 | $(INSTALL_BIN) $(CURDIR)/files/nftables/reserved_ip6.nft $(1)/etc/nikki/nftables/reserved_ip6.nft 75 | $(INSTALL_BIN) $(CURDIR)/files/nftables/geoip_cn.nft $(1)/etc/nikki/nftables/geoip_cn.nft 76 | $(INSTALL_BIN) $(CURDIR)/files/nftables/geoip6_cn.nft $(1)/etc/nikki/nftables/geoip6_cn.nft 77 | 78 | $(INSTALL_DIR) $(1)/etc/config 79 | $(INSTALL_CONF) $(CURDIR)/files/nikki.conf $(1)/etc/config/nikki 80 | 81 | $(INSTALL_DIR) $(1)/etc/init.d 82 | $(INSTALL_BIN) $(CURDIR)/files/nikki.init $(1)/etc/init.d/nikki 83 | 84 | $(INSTALL_DIR) $(1)/etc/uci-defaults 85 | $(INSTALL_BIN) $(CURDIR)/files/uci-defaults/firewall.sh $(1)/etc/uci-defaults/99_firewall_nikki 86 | $(INSTALL_BIN) $(CURDIR)/files/uci-defaults/init.sh $(1)/etc/uci-defaults/99_init_nikki 87 | $(INSTALL_BIN) $(CURDIR)/files/uci-defaults/migrate.sh $(1)/etc/uci-defaults/99_migrate_nikki 88 | 89 | $(INSTALL_DIR) $(1)/lib/upgrade/keep.d 90 | $(INSTALL_DATA) $(CURDIR)/files/nikki.upgrade $(1)/lib/upgrade/keep.d/nikki 91 | endef 92 | 93 | define Package/nikki/postrm 94 | #!/bin/sh 95 | if [ -z $${IPKG_INSTROOT} ]; then 96 | uci -q batch <<-EOF > /dev/null 97 | del firewall.nikki 98 | commit firewall 99 | EOF 100 | fi 101 | endef 102 | 103 | define Build/Prepare 104 | $(Build/Prepare/Default) 105 | $(RM) -r $(PKG_BUILD_DIR)/rules/logic_test 106 | endef 107 | 108 | $(eval $(call GoBinPackage,nikki)) 109 | $(eval $(call BuildPackage,nikki)) 110 | -------------------------------------------------------------------------------- /nikki/files/mixin.yaml: -------------------------------------------------------------------------------- 1 | # Mixin File 2 | # You can set any mihomo profile's config at here, it will mixin to the profile. 3 | # 4 | # For example: 5 | # 6 | # global-client-fingerprint: chrome # set fingerprint for TLS transport 7 | # experimental: # experimental config 8 | # quic-go-disable-gso: false # disable quic-go GSO support 9 | # quic-go-disable-ecn: false # disable quic-go ECN support 10 | # dialer-ip4p-convert: false # IP4P support 11 | # proxies: # overwrite proxies 12 | # - name: "PROXY" 13 | # type: ss 14 | # server: proxy.example.com 15 | # port: 443 16 | # cipher: chacha20-ietf-poly1305 17 | # password: "password" 18 | # rules: # overwrite rules 19 | # - DOMAIN,google.com,PROXY 20 | # - DOMAIN-SUFFIX,google.com,PROXY 21 | # - DOMAIN-KEYWORD,google,PROXY 22 | # - DOMAIN-REGEX,^google.*com,PROXY 23 | # - GEOSITE,google,PROXY 24 | # - GEOSITE,cn,DIRECT 25 | # - IP-CIDR,8.8.8.8/32,DIRECT,no-resolve 26 | # - GEOIP,telegram,DIRECT 27 | # - GEOIP,cn,DIRECT 28 | # - Match,PROXY -------------------------------------------------------------------------------- /nikki/files/nftables/reserved_ip.nft: -------------------------------------------------------------------------------- 1 | #!/usr/sbin/nft -f 2 | 3 | table inet nikki { 4 | set reserved_ip { 5 | type ipv4_addr 6 | flags interval 7 | elements = { 8 | 0.0.0.0/8, 9 | 10.0.0.0/8, 10 | 127.0.0.0/8, 11 | 100.64.0.0/10, 12 | 169.254.0.0/16, 13 | 172.16.0.0/12, 14 | 192.168.0.0/16, 15 | 224.0.0.0/4, 16 | 240.0.0.0/4 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /nikki/files/nftables/reserved_ip6.nft: -------------------------------------------------------------------------------- 1 | #!/usr/sbin/nft -f 2 | 3 | table inet nikki { 4 | set reserved_ip6 { 5 | type ipv6_addr 6 | flags interval 7 | elements = { 8 | ::/128, 9 | ::1/128, 10 | ::ffff:0:0/96, 11 | 100::/64, 12 | 64:ff9b::/96, 13 | 2001::/32, 14 | 2001:10::/28, 15 | 2001:20::/28, 16 | 2001:db8::/32, 17 | 2002::/16, 18 | fc00::/7, 19 | fe80::/10, 20 | ff00::/8 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /nikki/files/nikki.conf: -------------------------------------------------------------------------------- 1 | config status 'status' 2 | 3 | config config 'config' 4 | option 'init' '1' 5 | option 'enabled' '0' 6 | option 'profile' 'subscription:subscription' 7 | option 'start_delay' '0' 8 | option 'scheduled_restart' '0' 9 | option 'cron_expression' '0 3 * * *' 10 | option 'test_profile' '1' 11 | 12 | config proxy 'proxy' 13 | option 'enabled' '1' 14 | option 'tcp_mode' 'redirect' 15 | option 'udp_mode' 'tun' 16 | option 'ipv4_dns_hijack' '1' 17 | option 'ipv6_dns_hijack' '1' 18 | option 'ipv4_proxy' '1' 19 | option 'ipv6_proxy' '1' 20 | option 'fake_ip_ping_hijack' '1' 21 | option 'router_proxy' '1' 22 | option 'lan_proxy' '1' 23 | list 'lan_inbound_interface' 'lan' 24 | list 'bypass_dscp' '4' 25 | option 'bypass_china_mainland_ip' '0' 26 | option 'proxy_tcp_dport' '0-65535' 27 | option 'proxy_udp_dport' '0-65535' 28 | 29 | config subscription 'subscription' 30 | option 'name' 'default' 31 | option 'url' 'http://example.com/default.yaml' 32 | option 'user_agent' 'clash' 33 | option 'prefer' 'remote' 34 | 35 | config mixin 'mixin' 36 | option 'log_level' 'warning' 37 | option 'mode' 'rule' 38 | option 'match_process' 'off' 39 | option 'ipv6' '1' 40 | option 'ui_url' 'https://github.com/Zephyruso/zashboard/releases/latest/download/dist-cdn-fonts.zip' 41 | option 'api_listen' '[::]:9090' 42 | option 'selection_cache' '1' 43 | option 'allow_lan' '1' 44 | option 'http_port' '8080' 45 | option 'socks_port' '1080' 46 | option 'mixed_port' '7890' 47 | option 'redir_port' '7891' 48 | option 'tproxy_port' '7892' 49 | option 'authentication' '1' 50 | option 'tun_device' 'nikki' 51 | option 'tun_stack' 'mixed' 52 | option 'tun_dns_hijack' '0' 53 | list 'tun_dns_hijacks' 'tcp://any:53' 54 | list 'tun_dns_hijacks' 'udp://any:53' 55 | option 'dns_listen' '[::]:1053' 56 | option 'dns_ipv6' '1' 57 | option 'dns_mode' 'fake-ip' 58 | option 'fake_ip_range' '198.18.0.1/16' 59 | option 'fake_ip_filter' '0' 60 | list 'fake_ip_filters' '+.lan' 61 | list 'fake_ip_filters' '+.local' 62 | option 'fake_ip_cache' '1' 63 | option 'hosts' '0' 64 | option 'dns_nameserver' '0' 65 | option 'dns_nameserver_policy' '0' 66 | option 'sniffer_force_domain_name' '0' 67 | option 'sniffer_ignore_domain_name' '0' 68 | option 'sniffer_sniff' '0' 69 | option 'rule' '0' 70 | option 'rule_provider' '0' 71 | option 'mixin_file_content' '0' 72 | 73 | config env 'env' 74 | option 'disable_safe_path_check' '0' 75 | option 'disable_loopback_detector' '0' 76 | option 'disable_quic_go_gso' '0' 77 | option 'disable_quic_go_ecn' '0' 78 | 79 | config router_access_control 80 | option 'enabled' '1' 81 | list 'user' 'dnsmasq' 82 | list 'user' 'ftp' 83 | list 'user' 'logd' 84 | list 'user' 'nobody' 85 | list 'user' 'ntp' 86 | list 'user' 'ubus' 87 | list 'group' 'dnsmasq' 88 | list 'group' 'ftp' 89 | list 'group' 'logd' 90 | list 'group' 'nogroup' 91 | list 'group' 'ntp' 92 | list 'group' 'ubus' 93 | list 'cgroup' 'adguardhome' 94 | list 'cgroup' 'aria2' 95 | list 'cgroup' 'dnsmasq' 96 | list 'cgroup' 'netbird' 97 | list 'cgroup' 'qbittorrent' 98 | list 'cgroup' 'sysntpd' 99 | list 'cgroup' 'tailscale' 100 | list 'cgroup' 'zerotier' 101 | option 'proxy' '0' 102 | 103 | config router_access_control 104 | option 'enabled' '1' 105 | option 'proxy' '1' 106 | 107 | config lan_access_control 108 | option 'enabled' '1' 109 | option 'proxy' '1' 110 | 111 | config authentication 112 | option 'enabled' '1' 113 | option 'username' 'nikki' 114 | option 'password' '' 115 | 116 | config hosts 117 | option 'enabled' '0' 118 | option 'domain_name' 'localhost' 119 | list 'ip' '127.0.0.1' 120 | list 'ip' '::1' 121 | 122 | config nameserver 123 | option 'enabled' '1' 124 | option 'type' 'default-nameserver' 125 | list 'nameserver' '223.5.5.5' 126 | list 'nameserver' '223.6.6.6' 127 | 128 | config nameserver 129 | option 'enabled' '0' 130 | option 'type' 'proxy-server-nameserver' 131 | list 'nameserver' 'https://223.5.5.5/dns-query' 132 | list 'nameserver' 'https://223.6.6.6/dns-query' 133 | 134 | config nameserver 135 | option 'enabled' '0' 136 | option 'type' 'direct-nameserver' 137 | list 'nameserver' 'https://223.5.5.5/dns-query' 138 | list 'nameserver' 'https://223.6.6.6/dns-query' 139 | 140 | config nameserver 141 | option 'enabled' '1' 142 | option 'type' 'nameserver' 143 | list 'nameserver' 'https://223.5.5.5/dns-query' 144 | list 'nameserver' 'https://223.6.6.6/dns-query' 145 | 146 | config nameserver_policy 147 | option 'enabled' '1' 148 | option 'matcher' 'geosite:private,cn' 149 | list 'nameserver' 'https://223.5.5.5/dns-query' 150 | list 'nameserver' 'https://223.6.6.6/dns-query' 151 | 152 | config nameserver_policy 153 | option 'enabled' '1' 154 | option 'matcher' 'geosite:geolocation-!cn' 155 | list 'nameserver' 'https://1.1.1.1/dns-query' 156 | list 'nameserver' 'https://8.8.8.8/dns-query' 157 | 158 | config sniff 159 | option 'enabled' '1' 160 | option 'protocol' 'HTTP' 161 | list 'port' '80' 162 | list 'port' '8080' 163 | option 'overwrite_destination' '1' 164 | 165 | config sniff 166 | option 'enabled' '1' 167 | option 'protocol' 'TLS' 168 | list 'port' '443' 169 | list 'port' '8443' 170 | option 'overwrite_destination' '1' 171 | 172 | config sniff 173 | option 'enabled' '1' 174 | option 'protocol' 'QUIC' 175 | list 'port' '443' 176 | list 'port' '8443' 177 | option 'overwrite_destination' '1' 178 | 179 | config editor 'editor' 180 | 181 | config log 'log' 182 | -------------------------------------------------------------------------------- /nikki/files/nikki.init: -------------------------------------------------------------------------------- 1 | #!/bin/sh /etc/rc.common 2 | 3 | START=99 4 | STOP=10 5 | USE_PROCD=1 6 | 7 | . "$IPKG_INSTROOT/lib/functions/network.sh" 8 | . "$IPKG_INSTROOT/etc/nikki/scripts/include.sh" 9 | 10 | extra_command 'update_subscription' 'Update subscription by section id' 11 | 12 | boot() { 13 | # prepare files 14 | prepare_files 15 | # load config 16 | config_load nikki 17 | # start delay 18 | local enabled start_delay 19 | config_get_bool enabled "config" "enabled" 0 20 | config_get start_delay "config" "start_delay" 0 21 | if [[ "$enabled" == 1 && "$start_delay" -gt 0 ]]; then 22 | log "App" "Start after $start_delay seconds." 23 | sleep "$start_delay" 24 | fi 25 | # start 26 | start 27 | } 28 | 29 | start_service() { 30 | # prepare files 31 | prepare_files 32 | # load config 33 | config_load nikki 34 | # check if enabled 35 | local enabled 36 | config_get_bool enabled "config" "enabled" 0 37 | if [ "$enabled" == 0 ]; then 38 | log "App" "Disabled." 39 | log "App" "Exit." 40 | return 41 | fi 42 | # start 43 | log "App" "Enabled." 44 | log "App" "Start." 45 | # get config 46 | ## app config 47 | local scheduled_restart cron_expression profile test_profile fast_reload 48 | config_get_bool scheduled_restart "config" "scheduled_restart" 0 49 | config_get cron_expression "config" "cron_expression" 50 | config_get profile "config" "profile" 51 | config_get_bool test_profile "config" "test_profile" 0 52 | config_get_bool fast_reload "config" "fast_reload" 0 53 | ## mixin config 54 | ### overwrite 55 | local overwrite_authentication overwrite_tun_dns_hijack overwrite_fake_ip_filter overwrite_hosts overwrite_dns_nameserver overwrite_dns_nameserver_policy overwrite_sniffer_sniff overwrite_sniffer_force_domain_name overwrite_sniffer_ignore_domain_name 56 | config_get_bool overwrite_authentication "mixin" "authentication" 0 57 | config_get_bool overwrite_tun_dns_hijack "mixin" "tun_dns_hijack" 0 58 | config_get_bool overwrite_fake_ip_filter "mixin" "fake_ip_filter" 0 59 | config_get_bool overwrite_hosts "mixin" "hosts" 0 60 | config_get_bool overwrite_dns_nameserver "mixin" "dns_nameserver" 0 61 | config_get_bool overwrite_dns_nameserver_policy "mixin" "dns_nameserver_policy" 0 62 | config_get_bool overwrite_sniffer_force_domain_name "mixin" "sniffer_force_domain_name" 0 63 | config_get_bool overwrite_sniffer_ignore_domain_name "mixin" "sniffer_ignore_domain_name" 0 64 | config_get_bool overwrite_sniffer_sniff "mixin" "sniffer_sniff" 0 65 | ### mixin file content 66 | local mixin_file_content 67 | config_get_bool mixin_file_content "mixin" "mixin_file_content" 0 68 | ## environment variable 69 | local disable_safe_path_check disable_loopback_detector disable_quic_go_gso disable_quic_go_ecn 70 | config_get_bool disable_safe_path_check "env" "disable_safe_path_check" 0 71 | config_get_bool disable_loopback_detector "env" "disable_loopback_detector" 0 72 | config_get_bool disable_quic_go_gso "env" "disable_quic_go_gso" 0 73 | config_get_bool disable_quic_go_ecn "env" "disable_quic_go_ecn" 0 74 | # get profile 75 | if [[ "$profile" == "file:"* ]]; then 76 | local profile_name; profile_name="${profile/file:/}" 77 | local profile_file; profile_file="$PROFILES_DIR/$profile_name" 78 | log "Profile" "Use file: $profile_name." 79 | if [ ! -f "$profile_file" ]; then 80 | log "Profile" "File not found." 81 | log "App" "Exit." 82 | return 83 | fi 84 | cp -f "$profile_file" "$RUN_PROFILE_PATH" 85 | elif [[ "$profile" == "subscription:"* ]]; then 86 | local subscription_section; subscription_section="${profile/subscription:/}" 87 | local subscription_name subscription_prefer 88 | config_get subscription_name "$subscription_section" "name" 89 | config_get subscription_prefer "$subscription_section" "prefer" "remote" 90 | log "Profile" "Use subscription: $subscription_name." 91 | local subscription_file; subscription_file="$SUBSCRIPTIONS_DIR/$subscription_section.yaml" 92 | if [ "$subscription_prefer" == "remote" ] || [[ "$subscription_prefer" == "local" && ! -f "$subscription_file" ]]; then 93 | update_subscription "$subscription_section" 94 | fi 95 | if [ ! -f "$subscription_file" ]; then 96 | log "Profile" "Subscription file not found." 97 | log "App" "Exit." 98 | return 99 | fi 100 | cp -f "$subscription_file" "$RUN_PROFILE_PATH" 101 | else 102 | log "Profile" "No profile/subscription selected." 103 | log "App" "Exit." 104 | return 105 | fi 106 | # mixin 107 | log "Mixin" "Mixin config." 108 | if [ "$overwrite_authentication" == 1 ]; then 109 | yq -M -i 'del(.authentication)' "$RUN_PROFILE_PATH" 110 | fi 111 | if [ "$overwrite_tun_dns_hijack" == 1 ]; then 112 | yq -M -i 'del(.tun.dns-hijack)' "$RUN_PROFILE_PATH" 113 | fi 114 | if [ "$overwrite_fake_ip_filter" == 1 ]; then 115 | yq -M -i 'del(.dns.fake-ip-filter)' "$RUN_PROFILE_PATH" 116 | fi 117 | if [ "$overwrite_hosts" == 1 ]; then 118 | yq -M -i 'del(.hosts)' "$RUN_PROFILE_PATH" 119 | fi 120 | if [ "$overwrite_dns_nameserver" == 1 ]; then 121 | yq -M -i 'del(.dns.default-nameserver) | del(.dns.proxy-server-nameserver) | del(.dns.direct-nameserver) | del(.dns.nameserver) | del(.dns.fallback) ' "$RUN_PROFILE_PATH" 122 | fi 123 | if [ "$overwrite_dns_nameserver_policy" == 1 ]; then 124 | yq -M -i 'del(.dns.nameserver-policy)' "$RUN_PROFILE_PATH" 125 | fi 126 | if [ "$overwrite_sniffer_force_domain_name" == 1 ]; then 127 | yq -M -i 'del(.sniffer.force-domain)' "$RUN_PROFILE_PATH" 128 | fi 129 | if [ "$overwrite_sniffer_ignore_domain_name" == 1 ]; then 130 | yq -M -i 'del(.sniffer.skip-domain)' "$RUN_PROFILE_PATH" 131 | fi 132 | if [ "$overwrite_sniffer_sniff" == 1 ]; then 133 | yq -M -i 'del(.sniffer.sniff)' "$RUN_PROFILE_PATH" 134 | fi 135 | if [ "$mixin_file_content" == 0 ]; then 136 | ucode -S "$MIXIN_UC" | yq -M -p json -o yaml | yq -M -i ea '... comments="" | . as $item ireduce ({}; . * $item ) | .rules = .nikki-rules + .rules | del(.nikki-rules)' "$RUN_PROFILE_PATH" - 137 | elif [ "$mixin_file_content" == 1 ]; then 138 | ucode -S "$MIXIN_UC" | yq -M -p json -o yaml | yq -M -i ea '... comments="" | . as $item ireduce ({}; . * $item ) | .rules = .nikki-rules + .rules | del(.nikki-rules)' "$RUN_PROFILE_PATH" "$MIXIN_FILE_PATH" - 139 | fi 140 | # test profile 141 | if [ "$test_profile" == 1 ]; then 142 | log "Profile" "Testing..." 143 | if ($PROG -d "$RUN_DIR" -t >> "$CORE_LOG_PATH" 2>&1); then 144 | log "Profile" "Test passed." 145 | else 146 | log "Profile" "Test failed." 147 | log "Profile" "Please check the core log to find out the problem." 148 | log "App" "Exit." 149 | return 150 | fi 151 | fi 152 | # start core 153 | log "Core" "Start." 154 | procd_open_instance nikki 155 | 156 | procd_set_param command /bin/sh -c "$PROG -d $RUN_DIR >> $CORE_LOG_PATH 2>&1" 157 | procd_set_param file "$RUN_PROFILE_PATH" 158 | procd_set_param env SKIP_SAFE_PATH_CHECK="$disable_safe_path_check" DISABLE_LOOPBACK_DETECTOR="$disable_loopback_detector" QUIC_GO_DISABLE_GSO="$disable_quic_go_gso" QUIC_GO_DISABLE_ECN="$disable_quic_go_ecn" 159 | if [ "$fast_reload" == 1 ]; then 160 | procd_set_param reload_signal HUP 161 | fi 162 | procd_set_param pidfile "$PID_FILE_PATH" 163 | procd_set_param respawn 164 | procd_set_param limits core="unlimited" nofile="1048576 1048576" 165 | 166 | procd_close_instance 167 | # cron 168 | if [[ "$scheduled_restart" == 1 && -n "$cron_expression" ]]; then 169 | log "App" "Set scheduled restart." 170 | echo "$cron_expression /etc/init.d/nikki restart #nikki" >> "/etc/crontabs/root" 171 | /etc/init.d/cron restart 172 | fi 173 | # set started flag 174 | touch "$STARTED_FLAG_PATH" 175 | } 176 | 177 | service_started() { 178 | # check if started 179 | if [ ! -f "$STARTED_FLAG_PATH" ]; then 180 | return 181 | fi 182 | # load config 183 | config_load nikki 184 | # check if proxy enabled 185 | local enabled 186 | config_get_bool enabled "proxy" "enabled" 0 187 | if [ "$enabled" == 0 ]; then 188 | log "Proxy" "Disabled." 189 | return 190 | fi 191 | # get config 192 | ## mixin 193 | ### tun 194 | local tun_device 195 | config_get tun_device "mixin" "tun_device" "nikki" 196 | ## proxy config 197 | ### general 198 | local tcp_mode udp_mode ipv4_proxy ipv6_proxy 199 | config_get tcp_mode "proxy" "tcp_mode" 200 | config_get udp_mode "proxy" "udp_mode" 201 | config_get_bool ipv4_proxy "proxy" "ipv4_proxy" 0 202 | config_get_bool ipv6_proxy "proxy" "ipv6_proxy" 0 203 | # prepare config 204 | local tproxy_enable; tproxy_enable=0 205 | if [[ "$tcp_mode" == "tproxy" || "$udp_mode" == "tproxy" ]]; then 206 | tproxy_enable=1 207 | fi 208 | local tun_enable; tun_enable=0 209 | if [[ "$tcp_mode" == "tun" || "$udp_mode" == "tun" ]]; then 210 | tun_enable=1 211 | fi 212 | # fix compatible with dockerd 213 | ## cgroupfs-mount 214 | ### when cgroupfs-mount is installed, cgroupv1 will mounted instead of cgroupv2, we need to create cgroup manually 215 | if (mount | grep -q -w "^cgroup"); then 216 | mkdir -p "/sys/fs/cgroup/net_cls/$CGROUP_NAME" 217 | echo "$CGROUP_ID" > "/sys/fs/cgroup/net_cls/$CGROUP_NAME/net_cls.classid" 218 | cat "$PID_FILE_PATH" > "/sys/fs/cgroup/net_cls/$CGROUP_NAME/cgroup.procs" 219 | fi 220 | ## kmod-br-netfilter 221 | ### when kmod-br-netfilter is loaded, bridge-nf-call-iptables and bridge-nf-call-ip6tables are set to 1, we need to set them to 0 if tproxy is enabled 222 | if [ "$tproxy_enable" == 1 ] && (lsmod | grep -q br_netfilter); then 223 | if [ "$ipv4_proxy" == 1 ]; then 224 | local bridge_nf_call_iptables; bridge_nf_call_iptables=$(sysctl -e -n net.bridge.bridge-nf-call-iptables) 225 | if [ "$bridge_nf_call_iptables" == 1 ]; then 226 | touch "$BRIDGE_NF_CALL_IPTABLES_FLAG_PATH" 227 | sysctl -q -w net.bridge.bridge-nf-call-iptables=0 228 | fi 229 | fi 230 | if [ "$ipv6_proxy" == 1 ]; then 231 | local bridge_nf_call_ip6tables; bridge_nf_call_ip6tables=$(sysctl -e -n net.bridge.bridge-nf-call-ip6tables) 232 | if [ "$bridge_nf_call_ip6tables" == 1 ]; then 233 | touch "$BRIDGE_NF_CALL_IP6TABLES_FLAG_PATH" 234 | sysctl -q -w net.bridge.bridge-nf-call-ip6tables=0 235 | fi 236 | fi 237 | fi 238 | # proxy 239 | log "Proxy" "Enabled." 240 | # wait for tun device online 241 | if [ "$tun_enable" == 1 ]; then 242 | log "Proxy" "Waiting for tun device online..." 243 | local tun_timeout; tun_timeout=15 244 | local tun_interval; tun_interval=1 245 | while [ "$tun_timeout" -gt 0 ]; do 246 | if (ip link show dev "$tun_device" > /dev/null 2>&1); then 247 | if [ "$(ip -json addr show dev "$tun_device" | tun_device="$tun_device" yq -M '.[] | select(.ifname = strenv(tun_device)) | .addr_info | length')" -gt 0 ]; then 248 | log "Proxy" "Tun device is online." 249 | break 250 | fi 251 | fi 252 | tun_timeout=$((tun_timeout - tun_interval)) 253 | sleep "$tun_interval" 254 | done 255 | if [ "$tun_timeout" -le 0 ]; then 256 | log "Proxy" "Waiting timeout, tun device is not online." 257 | log "App" "Exit." 258 | return 259 | fi 260 | fi 261 | # ip route and rule 262 | if [ "$tproxy_enable" == 1 ]; then 263 | if [ "$ipv4_proxy" == 1 ]; then 264 | ip -4 route add local default dev lo table "$TPROXY_ROUTE_TABLE" 265 | ip -4 rule add pref "$TPROXY_RULE_PREF" fwmark "$TPROXY_FW_MARK" table "$TPROXY_ROUTE_TABLE" 266 | fi 267 | if [ "$ipv6_proxy" == 1 ]; then 268 | ip -6 route add local default dev lo table "$TPROXY_ROUTE_TABLE" 269 | ip -6 rule add pref "$TPROXY_RULE_PREF" fwmark "$TPROXY_FW_MARK" table "$TPROXY_ROUTE_TABLE" 270 | fi 271 | fi 272 | if [ "$tun_enable" == 1 ]; then 273 | if [ "$ipv4_proxy" == 1 ]; then 274 | ip -4 route add unicast default dev "$tun_device" table "$TUN_ROUTE_TABLE" 275 | ip -4 rule add pref "$TUN_RULE_PREF" fwmark "$TUN_FW_MARK" table "$TUN_ROUTE_TABLE" 276 | fi 277 | if [ "$ipv6_proxy" == 1 ]; then 278 | ip -6 route add unicast default dev "$tun_device" table "$TUN_ROUTE_TABLE" 279 | ip -6 rule add pref "$TUN_RULE_PREF" fwmark "$TUN_FW_MARK" table "$TUN_ROUTE_TABLE" 280 | fi 281 | $FIREWALL_INCLUDE_SH 282 | fi 283 | # hijack 284 | utpl -D cgroup_name="$CGROUP_NAME" -D cgroup_id="$CGROUP_ID" -D tproxy_fw_mark="$TPROXY_FW_MARK" -D tun_fw_mark="$TUN_FW_MARK" -S "$HIJACK_UT" | nft -f - 285 | # check hijack 286 | if (nft list tables | grep -q nikki); then 287 | log "Proxy" "Hijack successful." 288 | else 289 | log "Proxy" "Hijack failed." 290 | log "App" "Exit." 291 | fi 292 | } 293 | 294 | service_stopped() { 295 | cleanup 296 | } 297 | 298 | reload_service() { 299 | cleanup 300 | start 301 | } 302 | 303 | service_triggers() { 304 | procd_add_reload_trigger "nikki" 305 | } 306 | 307 | cleanup() { 308 | # clear log 309 | clear_log 310 | # delete routing policy 311 | ip -4 rule del table "$TPROXY_ROUTE_TABLE" > /dev/null 2>&1 312 | ip -4 rule del table "$TUN_ROUTE_TABLE" > /dev/null 2>&1 313 | ip -6 rule del table "$TPROXY_ROUTE_TABLE" > /dev/null 2>&1 314 | ip -6 rule del table "$TUN_ROUTE_TABLE" > /dev/null 2>&1 315 | # delete routing table 316 | ip -4 route flush table "$TPROXY_ROUTE_TABLE" > /dev/null 2>&1 317 | ip -4 route flush table "$TUN_ROUTE_TABLE" > /dev/null 2>&1 318 | ip -6 route flush table "$TPROXY_ROUTE_TABLE" > /dev/null 2>&1 319 | ip -6 route flush table "$TUN_ROUTE_TABLE" > /dev/null 2>&1 320 | # delete hijack 321 | nft delete table inet nikki > /dev/null 2>&1 322 | local handles handle 323 | handles=$(nft --json list table inet fw4 | yq -M '.nftables[] | select(has("rule")) | .rule | select(.chain == "input" and .comment == "nikki") | .handle') 324 | for handle in $handles; do 325 | nft delete rule inet fw4 input handle "$handle" 326 | done 327 | handles=$(nft --json list table inet fw4 | yq -M '.nftables[] | select(has("rule")) | .rule | select(.chain == "forward" and .comment == "nikki") | .handle') 328 | for handle in $handles; do 329 | nft delete rule inet fw4 forward handle "$handle" 330 | done 331 | # delete started flag 332 | rm "$STARTED_FLAG_PATH" > /dev/null 2>&1 333 | # revert fix compatible with dockerd 334 | ## kmod-br-netfilter 335 | if (rm "$BRIDGE_NF_CALL_IPTABLES_FLAG_PATH" > /dev/null 2>&1); then 336 | sysctl -q -w net.bridge.bridge-nf-call-iptables=1 337 | fi 338 | if (rm "$BRIDGE_NF_CALL_IP6TABLES_FLAG_PATH" > /dev/null 2>&1); then 339 | sysctl -q -w net.bridge.bridge-nf-call-ip6tables=1 340 | fi 341 | # delete cron 342 | sed -i "/#nikki/d" "/etc/crontabs/root" > /dev/null 2>&1 343 | /etc/init.d/cron restart 344 | } 345 | 346 | update_subscription() { 347 | local subscription_section; subscription_section="$1" 348 | if [ -z "$subscription_section" ]; then 349 | return 350 | fi 351 | # load config 352 | config_load nikki 353 | # get subscription config 354 | local subscription_name subscription_url subscription_user_agent 355 | config_get subscription_name "$subscription_section" "name" 356 | config_get subscription_url "$subscription_section" "url" 357 | config_get subscription_user_agent "$subscription_section" "user_agent" 358 | # reset subscription info 359 | uci_remove "nikki" "$subscription_section" "expire" > /dev/null 2>&1 360 | uci_remove "nikki" "$subscription_section" "upload" > /dev/null 2>&1 361 | uci_remove "nikki" "$subscription_section" "download" > /dev/null 2>&1 362 | uci_remove "nikki" "$subscription_section" "total" > /dev/null 2>&1 363 | uci_remove "nikki" "$subscription_section" "used" > /dev/null 2>&1 364 | uci_remove "nikki" "$subscription_section" "avaliable" > /dev/null 2>&1 365 | uci_remove "nikki" "$subscription_section" "update" > /dev/null 2>&1 366 | uci_remove "nikki" "$subscription_section" "success" > /dev/null 2>&1 367 | # update subscription 368 | log "Profile" "Update subscription: $subscription_name." 369 | local success 370 | local subscription_header_tmpfile; subscription_header_tmpfile="$TEMP_DIR/$subscription_section.header" 371 | local subscription_tmpfile; subscription_tmpfile="$TEMP_DIR/$subscription_section.yaml" 372 | local subscription_file; subscription_file="$SUBSCRIPTIONS_DIR/$subscription_section.yaml" 373 | if (curl -s -f --connect-timeout 15 --retry 3 -L -X GET -A "$subscription_user_agent" -D "$subscription_header_tmpfile" -o "$subscription_tmpfile" "$subscription_url"); then 374 | log "Profile" "Subscription download successful." 375 | if (yq -p yaml -o yaml "$subscription_tmpfile" > /dev/null 2>&1); then 376 | log "Profile" "Subscription is valid." 377 | success=1 378 | else 379 | log "Profile" "Subscription is not valid." 380 | success=0 381 | fi 382 | else 383 | log "Profile" "Subscription download failed." 384 | success=0 385 | fi 386 | # check if success 387 | if [ "$success" == 1 ]; then 388 | log "Profile" "Subscription update successful." 389 | local subscription_expire subscription_upload subscription_download subscription_total subscription_used subscription_avaliable 390 | subscription_expire=$(grep -i "subscription-userinfo: " "$subscription_header_tmpfile" | grep -i -o -E "expire=[[:digit:]]+" | cut -d '=' -f 2) 391 | subscription_upload=$(grep -i "subscription-userinfo: " "$subscription_header_tmpfile" | grep -i -o -E "upload=[[:digit:]]+" | cut -d '=' -f 2) 392 | subscription_download=$(grep -i "subscription-userinfo: " "$subscription_header_tmpfile" | grep -i -o -E "download=[[:digit:]]+" | cut -d '=' -f 2) 393 | subscription_total=$(grep -i "subscription-userinfo: " "$subscription_header_tmpfile" | grep -i -o -E "total=[[:digit:]]+" | cut -d '=' -f 2) 394 | if [[ -n "$subscription_upload" && -n "$subscription_download" ]]; then 395 | subscription_used=$((subscription_upload + subscription_download)) 396 | if [ -n "$subscription_total" ]; then 397 | subscription_avaliable=$((subscription_total - subscription_used)) 398 | fi 399 | fi 400 | # update subscription info 401 | if [ -n "$subscription_expire" ]; then 402 | uci_set "nikki" "$subscription_section" "expire" "$(date "+%Y-%m-%d %H:%M:%S" -d "@$subscription_expire")" 403 | fi 404 | if [ -n "$subscription_upload" ]; then 405 | uci_set "nikki" "$subscription_section" "upload" "$(format_filesize "$subscription_upload")" 406 | fi 407 | if [ -n "$subscription_download" ]; then 408 | uci_set "nikki" "$subscription_section" "download" "$(format_filesize "$subscription_download")" 409 | fi 410 | if [ -n "$subscription_total" ]; then 411 | uci_set "nikki" "$subscription_section" "total" "$(format_filesize "$subscription_total")" 412 | fi 413 | if [ -n "$subscription_used" ]; then 414 | uci_set "nikki" "$subscription_section" "used" "$(format_filesize "$subscription_used")" 415 | fi 416 | if [ -n "$subscription_avaliable" ]; then 417 | uci_set "nikki" "$subscription_section" "avaliable" "$(format_filesize "$subscription_avaliable")" 418 | fi 419 | uci_set "nikki" "$subscription_section" "update" "$(date "+%Y-%m-%d %H:%M:%S")" 420 | uci_set "nikki" "$subscription_section" "success" "1" 421 | # update subscription file 422 | rm -f "$subscription_header_tmpfile" 423 | mv -f "$subscription_tmpfile" "$subscription_file" 424 | elif [ "$success" == 0 ]; then 425 | log "Profile" "Subscription update failed." 426 | # update subscription info 427 | uci_set "nikki" "$subscription_section" "success" "0" 428 | # remove tmpfile 429 | rm -f "$subscription_header_tmpfile" 430 | rm -f "$subscription_tmpfile" 431 | fi 432 | uci_commit "nikki" 433 | } 434 | -------------------------------------------------------------------------------- /nikki/files/nikki.upgrade: -------------------------------------------------------------------------------- 1 | /etc/nikki/profiles/ 2 | /etc/nikki/subscriptions/ 3 | /etc/nikki/mixin.yaml 4 | /etc/nikki/run/providers/rule/ 5 | /etc/nikki/run/providers/proxy/ 6 | /etc/nikki/nftables/reserved_ip.nft 7 | /etc/nikki/nftables/reserved_ip6.nft 8 | -------------------------------------------------------------------------------- /nikki/files/scripts/debug.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | . "$IPKG_INSTROOT/etc/nikki/scripts/include.sh" 4 | 5 | enabled=`uci get nikki.config.enabled` 6 | 7 | if [ "$enabled" == "0" ]; then 8 | uci set nikki.config.enabled=1 9 | uci commit nikki 10 | /etc/init.d/nikki restart 11 | fi 12 | 13 | echo \ 14 | " 15 | # Nikki Debug Info 16 | ## system 17 | \`\`\`shell 18 | ` 19 | cat /etc/openwrt_release 20 | ` 21 | \`\`\` 22 | ## kernel 23 | \`\`\` 24 | ` 25 | uname -a 26 | ` 27 | \`\`\` 28 | ## application 29 | \`\`\` 30 | ` 31 | if [ -x "/bin/opkg" ]; then 32 | opkg list-installed "nikki" 33 | opkg list-installed "luci-app-nikki" 34 | elif [ -x "/usr/bin/apk" ]; then 35 | apk list -I "nikki" 36 | apk list -I "luci-app-nikki" 37 | fi 38 | ` 39 | \`\`\` 40 | ## config 41 | \`\`\`json 42 | ` 43 | ucode -S -e ' 44 | import { cursor } from "uci"; 45 | 46 | const uci = cursor(); 47 | 48 | const config = uci.get_all("nikki"); 49 | const result = {}; 50 | 51 | for (let section_id in config) { 52 | const section = config[section_id]; 53 | const section_type = section[".type"]; 54 | if (result[section_type] == null) { 55 | result[section_type] = []; 56 | } 57 | push(result[section_type], section); 58 | } 59 | for (let section_type in result) { 60 | for (let section in result[section_type]) { 61 | delete section[".anonymous"]; 62 | delete section[".type"]; 63 | delete section[".name"]; 64 | delete section[".index"]; 65 | } 66 | } 67 | if (exists(result, "mixin")) { 68 | for (let x in result["mixin"]) { 69 | if (exists(x, "api_secret")) { 70 | x["api_secret"] = "*"; 71 | } 72 | } 73 | } 74 | if (exists(result, "authentication")) { 75 | for (let x in result["authentication"]) { 76 | if (exists(x, "password")) { 77 | x["password"] = "*"; 78 | } 79 | } 80 | } 81 | if (exists(result, "subscription")) { 82 | for (let x in result["subscription"]) { 83 | if (exists(x, "url")) { 84 | x["url"] = "*"; 85 | } 86 | } 87 | } 88 | if (exists(result, "lan_access_control")) { 89 | for (let x in result["lan_access_control"]) { 90 | if (exists(x, "ip")) { 91 | for (let i = 0; i < length(x["ip"]); i++) { 92 | x["ip"][i] = "*"; 93 | } 94 | } 95 | if (exists(x, "ip6")) { 96 | for (let i = 0; i < length(x["ip6"]); i++) { 97 | x["ip6"][i] = "*"; 98 | } 99 | } 100 | if (exists(x, "mac")) { 101 | for (let i = 0; i < length(x["mac"]); i++) { 102 | x["mac"][i] = "*"; 103 | } 104 | } 105 | } 106 | } 107 | delete result["status"]; 108 | delete result["editor"]; 109 | delete result["log"]; 110 | print(result); 111 | ' 112 | ` 113 | \`\`\` 114 | ## profile 115 | \`\`\`json 116 | ` 117 | ucode -S -e ' 118 | import { popen } from "fs"; 119 | 120 | function desensitize_proxies(proxies) { 121 | for (let x in proxies) { 122 | if (exists(x, "server")) { 123 | x["server"] = "*"; 124 | } 125 | if (exists(x, "servername")) { 126 | x["servername"] = "*"; 127 | } 128 | if (exists(x, "sni")) { 129 | x["sni"] = "*"; 130 | } 131 | if (exists(x, "port")) { 132 | x["port"] = "*"; 133 | } 134 | if (exists(x, "ports")) { 135 | x["ports"] = "*"; 136 | } 137 | if (exists(x, "port-range")) { 138 | x["port-range"] = "*"; 139 | } 140 | if (exists(x, "uuid")) { 141 | x["uuid"] = "*"; 142 | } 143 | if (exists(x, "private-key")) { 144 | x["private-key"] = "*"; 145 | } 146 | if (exists(x, "public-key")) { 147 | x["public-key"] = "*"; 148 | } 149 | if (exists(x, "token")) { 150 | x["token"] = "*"; 151 | } 152 | if (exists(x, "username")) { 153 | x["username"] = "*"; 154 | } 155 | if (exists(x, "password")) { 156 | x["password"] = "*"; 157 | } 158 | } 159 | } 160 | 161 | function desensitize_profile() { 162 | let profile = {}; 163 | const process = popen("yq -p yaml -o json /etc/nikki/run/config.yaml"); 164 | if (process) { 165 | profile = json(process); 166 | if (exists(profile, "secret")) { 167 | profile["secret"] = "*"; 168 | } 169 | if (exists(profile, "authentication")) { 170 | profile["authentication"] = []; 171 | } 172 | if (exists(profile, "proxy-providers")) { 173 | for (let x in profile["proxy-providers"]) { 174 | if (exists(profile["proxy-providers"][x], "url")) { 175 | profile["proxy-providers"][x]["url"] = "*"; 176 | } 177 | if (exists(profile["proxy-providers"][x], "payload")) { 178 | desensitize_proxies(profile["proxy-providers"][x]["payload"]); 179 | } 180 | } 181 | } 182 | if (exists(profile, "proxies")) { 183 | desensitize_proxies(profile["proxies"]); 184 | } 185 | process.close(); 186 | } 187 | return profile; 188 | } 189 | 190 | print(desensitize_profile()); 191 | ' 192 | ` 193 | \`\`\` 194 | ## ip rule 195 | \`\`\` 196 | ` 197 | ip rule list 198 | ` 199 | \`\`\` 200 | ## ip route 201 | \`\`\` 202 | TPROXY: 203 | ` 204 | ip route list table "$TPROXY_ROUTE_TABLE" 205 | ` 206 | 207 | TUN: 208 | ` 209 | ip route list table "$TUN_ROUTE_TABLE" 210 | ` 211 | \`\`\` 212 | ## ip6 rule 213 | \`\`\` 214 | ` 215 | ip -6 rule list 216 | ` 217 | \`\`\` 218 | ## ip6 route 219 | \`\`\` 220 | TPROXY: 221 | ` 222 | ip -6 route list table "$TPROXY_ROUTE_TABLE" 223 | ` 224 | 225 | TUN: 226 | ` 227 | ip -6 route list table "$TUN_ROUTE_TABLE" 228 | ` 229 | \`\`\` 230 | ## nftables 231 | \`\`\` 232 | ` 233 | nft list table inet nikki 234 | ` 235 | \`\`\` 236 | ## service 237 | \`\`\`json 238 | ` 239 | /etc/init.d/nikki info 240 | ` 241 | \`\`\` 242 | " 243 | 244 | if [ "$enabled" == "0" ]; then 245 | uci set nikki.config.enabled=0 246 | uci commit nikki 247 | /etc/init.d/nikki restart 248 | fi 249 | -------------------------------------------------------------------------------- /nikki/files/scripts/firewall_include.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | . "$IPKG_INSTROOT/lib/functions.sh" 4 | . "$IPKG_INSTROOT/etc/nikki/scripts/include.sh" 5 | 6 | config_load nikki 7 | config_get enabled "config" "enabled" 0 8 | config_get tcp_mode "proxy" "tcp_mode" 9 | config_get udp_mode "proxy" "udp_mode" 10 | config_get tun_device "mixin" "tun_device" 11 | 12 | if [ "$enabled" == 1 ] && [[ "$tcp_mode" == "tun" || "$udp_mode" == "tun" ]]; then 13 | nft insert rule inet fw4 input iifname "$tun_device" counter accept comment "nikki" 14 | nft insert rule inet fw4 forward oifname "$tun_device" counter accept comment "nikki" 15 | nft insert rule inet fw4 forward iifname "$tun_device" counter accept comment "nikki" 16 | fi 17 | 18 | exit 0 19 | -------------------------------------------------------------------------------- /nikki/files/scripts/include.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # routing 4 | TPROXY_FW_MARK="0x80" 5 | TUN_FW_MARK="0x81" 6 | TPROXY_RULE_PREF="1024" 7 | TUN_RULE_PREF="1025" 8 | TPROXY_ROUTE_TABLE="80" 9 | TUN_ROUTE_TABLE="81" 10 | CGROUP_ID="0x12061206" 11 | CGROUP_NAME="nikki" 12 | 13 | # paths 14 | PROG="/usr/bin/mihomo" 15 | HOME_DIR="/etc/nikki" 16 | PROFILES_DIR="$HOME_DIR/profiles" 17 | SUBSCRIPTIONS_DIR="$HOME_DIR/subscriptions" 18 | MIXIN_FILE_PATH="$HOME_DIR/mixin.yaml" 19 | RUN_DIR="$HOME_DIR/run" 20 | RUN_PROFILE_PATH="$RUN_DIR/config.yaml" 21 | PROVIDERS_DIR="$RUN_DIR/providers" 22 | RULE_PROVIDERS_DIR="$PROVIDERS_DIR/rule" 23 | PROXY_PROVIDERS_DIR="$PROVIDERS_DIR/proxy" 24 | 25 | # log 26 | LOG_DIR="/var/log/nikki" 27 | APP_LOG_PATH="$LOG_DIR/app.log" 28 | CORE_LOG_PATH="$LOG_DIR/core.log" 29 | 30 | # temp 31 | TEMP_DIR="/var/run/nikki" 32 | PID_FILE_PATH="$TEMP_DIR/nikki.pid" 33 | STARTED_FLAG_PATH="$TEMP_DIR/started.flag" 34 | BRIDGE_NF_CALL_IPTABLES_FLAG_PATH="$TEMP_DIR/bridge_nf_call_iptables.flag" 35 | BRIDGE_NF_CALL_IP6TABLES_FLAG_PATH="$TEMP_DIR/bridge_nf_call_ip6tables.flag" 36 | 37 | # ucode 38 | UCODE_DIR="$HOME_DIR/ucode" 39 | INCLUDE_UCODE="$UCODE_DIR/include.uc" 40 | MIXIN_UC="$UCODE_DIR/mixin.uc" 41 | HIJACK_UT="$UCODE_DIR/hijack.ut" 42 | 43 | # scripts 44 | SH_DIR="$HOME_DIR/scripts" 45 | INCLUDE_SH="$SH_DIR/include.sh" 46 | FIREWALL_INCLUDE_SH="$SH_DIR/firewall_include.sh" 47 | 48 | # nftables 49 | NFT_DIR="$HOME_DIR/nftables" 50 | RESERVED_IP_NFT="$NFT_DIR/reserved_ip.nft" 51 | RESERVED_IP6_NFT="$NFT_DIR/reserved_ip6.nft" 52 | GEOIP_CN_NFT="$NFT_DIR/geoip_cn.nft" 53 | GEOIP6_CN_NFT="$NFT_DIR/geoip6_cn.nft" 54 | 55 | # functions 56 | format_filesize() { 57 | local kb; kb=1024 58 | local mb; mb=$((kb * 1024)) 59 | local gb; gb=$((mb * 1024)) 60 | local tb; tb=$((gb * 1024)) 61 | local pb; pb=$((tb * 1024)) 62 | local size; size="$1" 63 | if [ -z "$size" ]; then 64 | echo "" 65 | elif [ "$size" -lt "$kb" ]; then 66 | echo "$size B" 67 | elif [ "$size" -lt "$mb" ]; then 68 | echo "$(awk "BEGIN {print $size / $kb}") KB" 69 | elif [ "$size" -lt "$gb" ]; then 70 | echo "$(awk "BEGIN {print $size / $mb}") MB" 71 | elif [ "$size" -lt "$tb" ]; then 72 | echo "$(awk "BEGIN {print $size / $gb}") GB" 73 | elif [ "$size" -lt "$pb" ]; then 74 | echo "$(awk "BEGIN {print $size / $tb}") TB" 75 | else 76 | echo "$(awk "BEGIN {print $size / $pb}") PB" 77 | fi 78 | } 79 | 80 | prepare_files() { 81 | if [ ! -d "$LOG_DIR" ]; then 82 | mkdir -p "$LOG_DIR" 83 | fi 84 | if [ ! -f "$APP_LOG_PATH" ]; then 85 | touch "$APP_LOG_PATH" 86 | fi 87 | if [ ! -f "$CORE_LOG_PATH" ]; then 88 | touch "$CORE_LOG_PATH" 89 | fi 90 | if [ ! -d "$TEMP_DIR" ]; then 91 | mkdir -p "$TEMP_DIR" 92 | fi 93 | } 94 | 95 | clear_log() { 96 | echo -n > "$APP_LOG_PATH" 97 | echo -n > "$CORE_LOG_PATH" 98 | } 99 | 100 | log() { 101 | echo "[$(date "+%Y-%m-%d %H:%M:%S")] [$1] $2" >> "$APP_LOG_PATH" 102 | } 103 | -------------------------------------------------------------------------------- /nikki/files/uci-defaults/firewall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | . "$IPKG_INSTROOT/etc/nikki/scripts/include.sh" 4 | 5 | uci -q batch <<-EOF > /dev/null 6 | del firewall.nikki 7 | set firewall.nikki=include 8 | set firewall.nikki.type=script 9 | set firewall.nikki.path=$FIREWALL_INCLUDE_SH 10 | set firewall.nikki.fw4_compatible=1 11 | commit firewall 12 | EOF 13 | -------------------------------------------------------------------------------- /nikki/files/uci-defaults/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | . "$IPKG_INSTROOT/etc/nikki/scripts/include.sh" 4 | 5 | # check nikki.config.init 6 | init=$(uci -q get nikki.config.init); [ -z "$init" ] && return 7 | 8 | # generate random string for api secret and authentication password 9 | random=$(awk 'BEGIN{srand(); print int(rand() * 1000000)}') 10 | 11 | # set nikki.mixin.api_secret 12 | uci set nikki.mixin.api_secret="$random" 13 | 14 | # set nikki.@authentication[0].password 15 | uci set nikki.@authentication[0].password="$random" 16 | 17 | # remove nikki.config.init 18 | uci del nikki.config.init 19 | 20 | # commit 21 | uci commit nikki 22 | 23 | # exit with 0 24 | exit 0 25 | -------------------------------------------------------------------------------- /nikki/files/uci-defaults/migrate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | . "$IPKG_INSTROOT/etc/nikki/scripts/include.sh" 4 | 5 | # since v1.18.0 6 | 7 | mixin_rule=$(uci -q get nikki.mixin.rule); [ -z "$mixin_rule" ] && uci set nikki.mixin.rule=0 8 | 9 | mixin_rule_provider=$(uci -q get nikki.mixin.rule_provider); [ -z "$mixin_rule_provider" ] && uci set nikki.mixin.rule_provider=0 10 | 11 | # since v1.19.0 12 | 13 | mixin_ui_path=$(uci -q get nikki.mixin.ui_path); [ -z "$mixin_ui_path" ] && uci set nikki.mixin.ui_path=ui 14 | 15 | uci show nikki | grep -E 'nikki.@rule\[[[:digit:]]+\].match=' | sed 's/nikki.@rule\[\([[:digit:]]\+\)\].match=.*/rename nikki.@rule[\1].match=matcher/' | uci batch 16 | 17 | # since v1.19.1 18 | 19 | proxy_fake_ip_ping_hijack=$(uci -q get nikki.proxy.fake_ip_ping_hijack); [ -z "$proxy_fake_ip_ping_hijack" ] && uci set nikki.proxy.fake_ip_ping_hijack=0 20 | 21 | # since v1.20.0 22 | 23 | mixin_api_port=$(uci -q get nikki.mixin.api_port); [ -n "$mixin_api_port" ] && { 24 | uci del nikki.mixin.api_port 25 | uci set nikki.mixin.api_listen=[::]:$mixin_api_port 26 | } 27 | 28 | mixin_dns_port=$(uci -q get nikki.mixin.dns_port); [ -n "$mixin_dns_port" ] && { 29 | uci del nikki.mixin.dns_port 30 | uci set nikki.mixin.dns_listen=[::]:$mixin_dns_port 31 | } 32 | 33 | # since v1.22.0 34 | 35 | proxy_transparent_proxy=$(uci -q get nikki.proxy.transparent_proxy); [ -n "$proxy_transparent_proxy" ] && { 36 | uci rename nikki.proxy.transparent_proxy=enabled 37 | uci rename nikki.proxy.tcp_transparent_proxy_mode=tcp_mode 38 | uci rename nikki.proxy.udp_transparent_proxy_mode=udp_mode 39 | 40 | uci add nikki router_access_control 41 | uci set nikki.@router_access_control[-1].enabled=1 42 | proxy_bypass_user=$(uci -q get nikki.proxy.bypass_user); [ -n "$proxy_bypass_user" ] && { 43 | for user in $proxy_bypass_user; do 44 | uci add_list nikki.@router_access_control[-1].user="$user" 45 | done 46 | } 47 | proxy_bypass_group=$(uci -q get nikki.proxy.bypass_group); [ -n "$proxy_bypass_group" ] && { 48 | for group in $proxy_bypass_group; do 49 | uci add_list nikki.@router_access_control[-1].group="$group" 50 | done 51 | } 52 | proxy_bypass_cgroup=$(uci -q get nikki.proxy.bypass_cgroup); [ -n "$proxy_bypass_cgroup" ] && { 53 | for cgroup in $proxy_bypass_cgroup; do 54 | uci add_list nikki.@router_access_control[-1].cgroup="$cgroup" 55 | done 56 | } 57 | uci set nikki.@router_access_control[-1].proxy=0 58 | 59 | uci add nikki router_access_control 60 | uci set nikki.@router_access_control[-1].enabled=1 61 | uci set nikki.@router_access_control[-1].proxy=1 62 | 63 | uci add_list nikki.proxy.lan_inbound_interface=lan 64 | 65 | proxy_access_control_mode=$(uci -q get nikki.proxy.access_control_mode) 66 | 67 | [ "$proxy_access_control_mode" != "all" ] && { 68 | proxy_acl_ip=$(uci -q get nikki.proxy.acl_ip); [ -n "$proxy_acl_ip" ] && { 69 | for ip in $proxy_acl_ip; do 70 | uci add nikki lan_access_control 71 | uci set nikki.@lan_access_control[-1].enabled=1 72 | uci add_list nikki.@lan_access_control[-1].ip="$ip" 73 | [ "$proxy_access_control_mode" == "allow" ] && uci set nikki.@lan_access_control[-1].proxy=1 74 | [ "$proxy_access_control_mode" == "block" ] && uci set nikki.@lan_access_control[-1].proxy=0 75 | done 76 | } 77 | proxy_acl_ip6=$(uci -q get nikki.proxy.acl_ip6); [ -n "$proxy_acl_ip6" ] && { 78 | for ip6 in $proxy_acl_ip6; do 79 | uci add nikki lan_access_control 80 | uci set nikki.@lan_access_control[-1].enabled=1 81 | uci add_list nikki.@lan_access_control[-1].ip6="$ip6" 82 | [ "$proxy_access_control_mode" == "allow" ] && uci set nikki.@lan_access_control[-1].proxy=1 83 | [ "$proxy_access_control_mode" == "block" ] && uci set nikki.@lan_access_control[-1].proxy=0 84 | done 85 | } 86 | proxy_acl_mac=$(uci -q get nikki.proxy.acl_mac); [ -n "$proxy_acl_mac" ] && { 87 | for mac in $proxy_acl_mac; do 88 | uci add nikki lan_access_control 89 | uci set nikki.@lan_access_control[-1].enabled=1 90 | uci add_list nikki.@lan_access_control[-1].mac="$mac" 91 | [ "$proxy_access_control_mode" == "allow" ] && uci set nikki.@lan_access_control[-1].proxy=1 92 | [ "$proxy_access_control_mode" == "block" ] && uci set nikki.@lan_access_control[-1].proxy=0 93 | done 94 | } 95 | } 96 | 97 | [ "$proxy_access_control_mode" != "allow" ] && { 98 | uci add nikki lan_access_control 99 | uci set nikki.@lan_access_control[-1].enabled=1 100 | uci set nikki.@lan_access_control[-1].proxy=1 101 | } 102 | 103 | uci del nikki.proxy.access_control_mode 104 | uci del nikki.proxy.acl_ip 105 | uci del nikki.proxy.acl_ip6 106 | uci del nikki.proxy.acl_mac 107 | uci del nikki.proxy.acl_interface 108 | uci del nikki.proxy.bypass_user 109 | uci del nikki.proxy.bypass_group 110 | uci del nikki.proxy.bypass_cgroup 111 | } 112 | 113 | # commit 114 | uci commit nikki 115 | 116 | # exit with 0 117 | exit 0 118 | -------------------------------------------------------------------------------- /nikki/files/ucode/hijack.ut: -------------------------------------------------------------------------------- 1 | #!/usr/bin/utpl 2 | 3 | {%- 4 | 'use strict'; 5 | 6 | import { cursor } from 'uci'; 7 | import { connect } from 'ubus'; 8 | import { uci_bool, uci_array, get_cgroups_version, get_users, get_groups, get_cgroups } from '/etc/nikki/ucode/include.uc'; 9 | 10 | const cgroups_version = get_cgroups_version(); 11 | 12 | const users = get_users(); 13 | const groups = get_groups(); 14 | const cgroups = get_cgroups(); 15 | 16 | const uci = cursor(); 17 | const ubus = connect(); 18 | 19 | uci.load('nikki'); 20 | 21 | const redir_port = uci.get('nikki', 'mixin', 'redir_port'); 22 | const tproxy_port = uci.get('nikki', 'mixin', 'tproxy_port'); 23 | 24 | const dns_listen = uci.get('nikki', 'mixin', 'dns_listen'); 25 | const dns_port = substr(dns_listen, rindex(dns_listen, ':') + 1); 26 | const fake_ip_range = uci.get('nikki', 'mixin', 'fake_ip_range'); 27 | 28 | const tun_device = uci.get('nikki', 'mixin', 'tun_device'); 29 | 30 | const tcp_mode = uci.get('nikki', 'proxy', 'tcp_mode'); 31 | const udp_mode = uci.get('nikki', 'proxy', 'udp_mode'); 32 | const ipv4_dns_hijack = uci_bool(uci.get('nikki', 'proxy', 'ipv4_dns_hijack')); 33 | const ipv6_dns_hijack = uci_bool(uci.get('nikki', 'proxy', 'ipv6_dns_hijack')); 34 | const ipv4_proxy = uci_bool(uci.get('nikki', 'proxy', 'ipv4_proxy')); 35 | const ipv6_proxy = uci_bool(uci.get('nikki', 'proxy', 'ipv6_proxy')); 36 | const fake_ip_ping_hijack = uci_bool(uci.get('nikki', 'proxy', 'fake_ip_ping_hijack')); 37 | 38 | const router_proxy = uci_bool(uci.get('nikki', 'proxy', 'router_proxy')); 39 | const router_access_control = []; 40 | uci.foreach('nikki', 'router_access_control', (access_control) => { 41 | access_control['enabled'] = uci_bool(access_control['enabled']); 42 | access_control['user'] = filter(uci_array(access_control['user']), (x) => index(users, x) >= 0); 43 | access_control['group'] = filter(uci_array(access_control['group']), (x) => index(groups, x) >= 0); 44 | access_control['cgroup'] = filter(uci_array(access_control['cgroup']), (x) => index(cgroups, x) >= 0); 45 | access_control['proxy'] = uci_bool(access_control['proxy']); 46 | push(router_access_control, access_control); 47 | }); 48 | 49 | const lan_proxy = uci_bool(uci.get('nikki', 'proxy', 'lan_proxy')); 50 | const lan_inbound_interface = uci_array(uci.get('nikki', 'proxy', 'lan_inbound_interface')); 51 | const lan_inbound_device = []; 52 | for (let interface in lan_inbound_interface) { 53 | const device = ubus.call('network.interface', 'status', {'interface': interface})?.l3_device ?? ''; 54 | if (device != '') { 55 | push(lan_inbound_device, device); 56 | } 57 | } 58 | const lan_access_control = []; 59 | uci.foreach('nikki', 'lan_access_control', (access_control) => { 60 | access_control['enabled'] = uci_bool(access_control['enabled']); 61 | access_control['ip'] = uci_array(access_control['ip']); 62 | access_control['ip6'] = uci_array(access_control['ip6']); 63 | access_control['mac'] = uci_array(access_control['mac']); 64 | access_control['proxy'] = uci_bool(access_control['proxy']); 65 | push(lan_access_control, access_control); 66 | }); 67 | 68 | const bypass_dscp = uci_array(uci.get('nikki', 'proxy', 'bypass_dscp')); 69 | const bypass_china_mainland_ip = uci_bool(uci.get('nikki', 'proxy', 'bypass_china_mainland_ip')); 70 | const proxy_tcp_dport = split((uci.get('nikki', 'proxy', 'proxy_tcp_dport') ?? '0-65535'), ' '); 71 | const proxy_udp_dport = split((uci.get('nikki', 'proxy', 'proxy_udp_dport') ?? '0-65535'), ' '); 72 | 73 | const dns_hijack_nfproto = []; 74 | if (ipv4_dns_hijack) { 75 | push(dns_hijack_nfproto, 'ipv4'); 76 | } 77 | if (ipv6_dns_hijack) { 78 | push(dns_hijack_nfproto, 'ipv6'); 79 | } 80 | 81 | const proxy_nfproto = []; 82 | if (ipv4_proxy) { 83 | push(proxy_nfproto, 'ipv4'); 84 | } 85 | if (ipv6_proxy) { 86 | push(proxy_nfproto, 'ipv6'); 87 | } 88 | 89 | const proxy_dport = []; 90 | for (let port in proxy_tcp_dport) { 91 | push(proxy_dport, `tcp . ${port}`); 92 | } 93 | for (let port in proxy_udp_dport) { 94 | push(proxy_dport, `udp . ${port}`); 95 | } 96 | -%} 97 | 98 | table inet nikki { 99 | set dns_hijack_nfproto { 100 | type nf_proto 101 | flags interval 102 | {% if (length(dns_hijack_nfproto) > 0): %} 103 | elements = { 104 | {% for (let nfproto in dns_hijack_nfproto): %} 105 | {{ nfproto }}, 106 | {% endfor %} 107 | } 108 | {% endif %} 109 | } 110 | 111 | set proxy_nfproto { 112 | type nf_proto 113 | flags interval 114 | {% if (length(proxy_nfproto) > 0): %} 115 | elements = { 116 | {% for (let nfproto in proxy_nfproto): %} 117 | {{ nfproto }}, 118 | {% endfor %} 119 | } 120 | {% endif %} 121 | } 122 | 123 | set reserved_ip { 124 | type ipv4_addr 125 | flags interval 126 | auto-merge 127 | } 128 | 129 | set reserved_ip6 { 130 | type ipv6_addr 131 | flags interval 132 | auto-merge 133 | } 134 | 135 | set lan_inbound_device { 136 | type ifname 137 | flags interval 138 | auto-merge 139 | {% if (length(lan_inbound_device) > 0): %} 140 | elements = { 141 | {% for (let device in lan_inbound_device): %} 142 | "{{ device }}", 143 | {% endfor %} 144 | } 145 | {% endif %} 146 | } 147 | 148 | set china_ip { 149 | type ipv4_addr 150 | flags interval 151 | } 152 | 153 | set china_ip6 { 154 | type ipv6_addr 155 | flags interval 156 | } 157 | 158 | set proxy_dport { 159 | type inet_proto . inet_service 160 | flags interval 161 | auto-merge 162 | {% if (length(proxy_dport) > 0): %} 163 | elements = { 164 | {% for (let dport in proxy_dport): %} 165 | {{ dport }}, 166 | {% endfor %} 167 | } 168 | {% endif %} 169 | } 170 | 171 | set bypass_dscp { 172 | type dscp 173 | flags interval 174 | auto-merge 175 | {% if (length(bypass_dscp) > 0): %} 176 | elements = { 177 | {% for (let dscp in bypass_dscp): %} 178 | {{ dscp }}, 179 | {% endfor %} 180 | } 181 | {% endif %} 182 | } 183 | 184 | {% if (router_proxy): %} 185 | chain router_dns_hijack { 186 | {% for (let access_control in router_access_control): %} 187 | {% if (access_control['enabled']): %} 188 | {% if (length(access_control['user']) == 0 && length(access_control['group']) == 0 && length(access_control['cgroup']) == 0): %} 189 | meta l4proto { tcp, udp } th dport 53 counter {% if (access_control.proxy == '1'): %} redirect to :{{ dns_port }} {% else %} return {% endif %} 190 | 191 | {% else %} 192 | {% if (length(access_control['user']) > 0): %} 193 | meta l4proto { tcp, udp } meta skuid { {% for (let user in access_control['user']): %} {{ user }}, {% endfor %} } th dport 53 counter {% if (access_control.proxy == '1'): %} redirect to :{{ dns_port }} {% else %} return {% endif %} 194 | 195 | {% endif %} 196 | {% if (length(access_control['group']) > 0): %} 197 | meta l4proto { tcp, udp } meta skgid { {% for (let group in access_control['group']): %} {{ group }}, {% endfor %} } th dport 53 counter {% if (access_control.proxy == '1'): %} redirect to :{{ dns_port }} {% else %} return {% endif %} 198 | 199 | {% endif %} 200 | {% if (cgroups_version == 2 && length(access_control['cgroup']) > 0): %} 201 | meta l4proto { tcp, udp } socket cgroupv2 level 2 { {% for (let cgroup in access_control['cgroup']): %} services/{{ cgroup }}, {% endfor %} } th dport 53 counter {% if (access_control.proxy == '1'): %} redirect to :{{ dns_port }} {% else %} return {% endif %} 202 | 203 | {% endif %} 204 | {% endif %} 205 | {% endif %} 206 | {% endfor %} 207 | } 208 | 209 | chain router_redirect { 210 | {% for (let access_control in router_access_control): %} 211 | {% if (access_control['enabled']): %} 212 | {% if (length(access_control['user']) == 0 && length(access_control['group']) == 0 && length(access_control['cgroup']) == 0): %} 213 | meta l4proto tcp counter {% if (access_control.proxy == '1'): %} redirect to :{{ redir_port }} {% else %} return {% endif %} 214 | 215 | {% else %} 216 | {% if (length(access_control['user']) > 0): %} 217 | meta l4proto tcp meta skuid { {% for (let user in access_control['user']): %} {{ user }}, {% endfor %} } counter {% if (access_control.proxy == '1'): %} redirect to :{{ redir_port }} {% else %} return {% endif %} 218 | 219 | {% endif %} 220 | {% if (length(access_control['group']) > 0): %} 221 | meta l4proto tcp meta skgid { {% for (let group in access_control['group']): %} {{ group }}, {% endfor %} } counter {% if (access_control.proxy == '1'): %} redirect to :{{ redir_port }} {% else %} return {% endif %} 222 | 223 | {% endif %} 224 | {% if (cgroups_version == 2 && length(access_control['cgroup']) > 0): %} 225 | meta l4proto tcp socket cgroupv2 level 2 { {% for (let cgroup in access_control['cgroup']): %} services/{{ cgroup }}, {% endfor %} } counter {% if (access_control.proxy == '1'): %} redirect to :{{ redir_port }} {% else %} return {% endif %} 226 | 227 | {% endif %} 228 | {% endif %} 229 | {% endif %} 230 | {% endfor %} 231 | } 232 | 233 | chain router_tproxy { 234 | {% for (let access_control in router_access_control): %} 235 | {% if (access_control['enabled']): %} 236 | {% if (length(access_control['user']) == 0 && length(access_control['group']) == 0 && length(access_control['cgroup']) == 0): %} 237 | meta l4proto { tcp, udp } {% if (access_control.proxy == '1'): %} meta mark set {{ tproxy_fw_mark }} counter accept {% else %} counter return {% endif %} 238 | 239 | {% else %} 240 | {% if (length(access_control['user']) > 0): %} 241 | meta l4proto { tcp, udp } meta skuid { {% for (let user in access_control['user']): %} {{ user }}, {% endfor %} } {% if (access_control.proxy == '1'): %} meta mark set {{ tproxy_fw_mark }} counter accept {% else %} counter return {% endif %} 242 | 243 | {% endif %} 244 | {% if (length(access_control['group']) > 0): %} 245 | meta l4proto { tcp, udp } meta skgid { {% for (let group in access_control['group']): %} {{ group }}, {% endfor %} } {% if (access_control.proxy == '1'): %} meta mark set {{ tproxy_fw_mark }} counter accept {% else %} counter return {% endif %} 246 | 247 | {% endif %} 248 | {% if (cgroups_version == 2 && length(access_control['cgroup']) > 0): %} 249 | meta l4proto { tcp, udp } socket cgroupv2 level 2 { {% for (let cgroup in access_control['cgroup']): %} services/{{ cgroup }}, {% endfor %} } {% if (access_control.proxy == '1'): %} meta mark set {{ tproxy_fw_mark }} counter accept {% else %} counter return {% endif %} 250 | 251 | {% endif %} 252 | {% endif %} 253 | {% endif %} 254 | {% endfor %} 255 | } 256 | 257 | chain router_tun { 258 | {% for (let access_control in router_access_control): %} 259 | {% if (access_control['enabled']): %} 260 | {% if (length(access_control['user']) == 0 && length(access_control['group']) == 0 && length(access_control['cgroup']) == 0): %} 261 | meta l4proto { tcp, udp } {% if (access_control.proxy == '1'): %} meta mark set {{ tun_fw_mark }} counter accept {% else %} counter return {% endif %} 262 | 263 | {% else %} 264 | {% if (length(access_control['user']) > 0): %} 265 | meta l4proto { tcp, udp } meta skuid { {% for (let user in access_control['user']): %} {{ user }}, {% endfor %} } {% if (access_control.proxy == '1'): %} meta mark set {{ tun_fw_mark }} counter accept {% else %} counter return {% endif %} 266 | 267 | {% endif %} 268 | {% if (length(access_control['group']) > 0): %} 269 | meta l4proto { tcp, udp } meta skgid { {% for (let group in access_control['group']): %} {{ group }}, {% endfor %} } {% if (access_control.proxy == '1'): %} meta mark set {{ tun_fw_mark }} counter accept {% else %} counter return {% endif %} 270 | 271 | {% endif %} 272 | {% if (cgroups_version == 2 && length(access_control['cgroup']) > 0): %} 273 | meta l4proto { tcp, udp } socket cgroupv2 level 2 { {% for (let cgroup in access_control['cgroup']): %} services/{{ cgroup }}, {% endfor %} } {% if (access_control.proxy == '1'): %} meta mark set {{ tun_fw_mark }} counter accept {% else %} counter return {% endif %} 274 | 275 | {% endif %} 276 | {% endif %} 277 | {% endif %} 278 | {% endfor %} 279 | } 280 | {% endif %} 281 | 282 | {% if (lan_proxy): %} 283 | chain lan_dns_hijack { 284 | {% for (let access_control in lan_access_control): %} 285 | {% if (access_control['enabled']): %} 286 | {% if (length(access_control['ip']) == 0 && length(access_control['ip6']) == 0 && length(access_control['mac']) == 0): %} 287 | meta l4proto { tcp, udp } th dport 53 counter {% if (access_control.proxy == '1'): %} redirect to :{{ dns_port }} {% else %} return {% endif %} 288 | 289 | {% else %} 290 | {% if (length(access_control['ip']) > 0): %} 291 | meta l4proto { tcp, udp } ip saddr { {% for (let ip in access_control['ip']): %} {{ ip }}, {% endfor %} } th dport 53 counter {% if (access_control.proxy == '1'): %} redirect to :{{ dns_port }} {% else %} return {% endif %} 292 | 293 | {% endif %} 294 | {% if (length(access_control['ip6']) > 0): %} 295 | meta l4proto { tcp, udp } ip6 saddr { {% for (let ip6 in access_control['ip6']): %} {{ ip6 }}, {% endfor %} } th dport 53 counter {% if (access_control.proxy == '1'): %} redirect to :{{ dns_port }} {% else %} return {% endif %} 296 | 297 | {% endif %} 298 | {% if (length(access_control['mac']) > 0): %} 299 | meta l4proto { tcp, udp } ether saddr { {% for (let mac in access_control['mac']): %} {{ mac }}, {% endfor %} } th dport 53 counter {% if (access_control.proxy == '1'): %} redirect to :{{ dns_port }} {% else %} return {% endif %} 300 | 301 | {% endif %} 302 | {% endif %} 303 | {% endif %} 304 | {% endfor %} 305 | } 306 | 307 | chain lan_redirect { 308 | {% for (let access_control in lan_access_control): %} 309 | {% if (access_control['enabled']): %} 310 | {% if (length(access_control['ip']) == 0 && length(access_control['ip6']) == 0 && length(access_control['mac']) == 0): %} 311 | meta l4proto tcp counter {% if (access_control.proxy == '1'): %} redirect to :{{ redir_port }} {% else %} counter return {% endif %} 312 | 313 | {% else %} 314 | {% if (length(access_control['ip']) > 0): %} 315 | meta l4proto tcp ip saddr { {% for (let ip in access_control['ip']): %} {{ ip }}, {% endfor %} } counter {% if (access_control.proxy == '1'): %} redirect to :{{ redir_port }} {% else %} return {% endif %} 316 | 317 | {% endif %} 318 | {% if (length(access_control['ip6']) > 0): %} 319 | meta l4proto tcp ip6 saddr { {% for (let ip6 in access_control['ip6']): %} {{ ip6 }}, {% endfor %} } counter {% if (access_control.proxy == '1'): %} redirect to :{{ redir_port }} {% else %} return {% endif %} 320 | 321 | {% endif %} 322 | {% if (length(access_control['mac']) > 0): %} 323 | meta l4proto tcp ether saddr { {% for (let mac in access_control['mac']): %} {{ mac }}, {% endfor %} } counter {% if (access_control.proxy == '1'): %} redirect to :{{ redir_port }} {% else %} return {% endif %} 324 | 325 | {% endif %} 326 | {% endif %} 327 | {% endif %} 328 | {% endfor %} 329 | } 330 | 331 | chain lan_tproxy { 332 | {% for (let access_control in lan_access_control): %} 333 | {% if (access_control['enabled']): %} 334 | {% if (length(access_control['ip']) == 0 && length(access_control['ip6']) == 0 && length(access_control['mac']) == 0): %} 335 | meta l4proto { tcp, udp } {% if (access_control.proxy == '1'): %} meta mark set {{ tproxy_fw_mark }} tproxy to :{{ tproxy_port }} counter accept {% else %} counter return {% endif %} 336 | 337 | {% else %} 338 | {% if (length(access_control['ip']) > 0): %} 339 | meta l4proto { tcp, udp } ip saddr { {% for (let ip in access_control['ip']): %} {{ ip }}, {% endfor %} } {% if (access_control.proxy == '1'): %} meta mark set {{ tproxy_fw_mark }} tproxy ip to :{{ tproxy_port }} counter accept {% else %} counter return {% endif %} 340 | 341 | {% endif %} 342 | {% if (length(access_control['ip6']) > 0): %} 343 | meta l4proto { tcp, udp } ip6 saddr { {% for (let ip6 in access_control['ip6']): %} {{ ip6 }}, {% endfor %} } {% if (access_control.proxy == '1'): %} meta mark set {{ tproxy_fw_mark }} tproxy ip6 to :{{ tproxy_port }} counter accept {% else %} counter return {% endif %} 344 | 345 | {% endif %} 346 | {% if (length(access_control['mac']) > 0): %} 347 | meta l4proto { tcp, udp } ether saddr { {% for (let mac in access_control['mac']): %} {{ mac }}, {% endfor %} } {% if (access_control.proxy == '1'): %} meta mark set {{ tproxy_fw_mark }} tproxy to :{{ tproxy_port }} counter accept {% else %} counter return {% endif %} 348 | 349 | {% endif %} 350 | {% endif %} 351 | {% endif %} 352 | {% endfor %} 353 | } 354 | 355 | chain lan_tun { 356 | {% for (let access_control in lan_access_control): %} 357 | {% if (access_control['enabled']): %} 358 | {% if (length(access_control['ip']) == 0 && length(access_control['ip6']) == 0 && length(access_control['mac']) == 0): %} 359 | meta l4proto { tcp, udp } {% if (access_control.proxy == '1'): %} meta mark set {{ tun_fw_mark }} counter accept {% else %}counter return {% endif %} 360 | 361 | {% else %} 362 | {% if (length(access_control['ip']) > 0): %} 363 | meta l4proto { tcp, udp } ip saddr { {% for (let ip in access_control['ip']): %} {{ ip }}, {% endfor %} } {% if (access_control.proxy == '1'): %} meta mark set {{ tun_fw_mark }} counter accept {% else %}counter return {% endif %} 364 | 365 | {% endif %} 366 | {% if (length(access_control['ip6']) > 0): %} 367 | meta l4proto { tcp, udp } ip6 saddr { {% for (let ip6 in access_control['ip6']): %} {{ ip6 }}, {% endfor %} } {% if (access_control.proxy == '1'): %} meta mark set {{ tun_fw_mark }} counter accept {% else %}counter return {% endif %} 368 | 369 | {% endif %} 370 | {% if (length(access_control['mac']) > 0): %} 371 | meta l4proto { tcp, udp } ether saddr { {% for (let mac in access_control['mac']): %} {{ mac }}, {% endfor %} } {% if (access_control.proxy == '1'): %} meta mark set {{ tun_fw_mark }} counter accept {% else %}counter return {% endif %} 372 | 373 | {% endif %} 374 | {% endif %} 375 | {% endif %} 376 | {% endfor %} 377 | } 378 | {% endif %} 379 | 380 | {% if (router_proxy): %} 381 | chain nat_output { 382 | type nat hook output priority filter; policy accept; 383 | {% if (cgroups_version == 1): %} 384 | meta cgroup {{ cgroup_id }} counter return 385 | {% elif (cgroups_version == 2): %} 386 | socket cgroupv2 level 2 services/{{ cgroup_name }} counter return 387 | {% endif %} 388 | meta nfproto @dns_hijack_nfproto jump router_dns_hijack 389 | {% if (tcp_mode == 'redirect'): %} 390 | fib daddr type { local, multicast, broadcast, anycast } counter return 391 | ct direction reply counter return 392 | ip daddr @reserved_ip counter return 393 | ip6 daddr @reserved_ip6 counter return 394 | ip daddr @china_ip counter return 395 | ip6 daddr @china_ip6 counter return 396 | meta nfproto ipv4 meta l4proto . th dport != @proxy_dport ip daddr != {{ fake_ip_range }} counter return 397 | meta nfproto ipv6 meta l4proto . th dport != @proxy_dport counter return 398 | meta l4proto { tcp, udp } ip dscp @bypass_dscp ip daddr != {{ fake_ip_range }} counter return 399 | meta l4proto { tcp, udp } ip6 dscp @bypass_dscp counter return 400 | meta nfproto @proxy_nfproto jump router_redirect 401 | {% endif %} 402 | {% if (fake_ip_ping_hijack): %} 403 | ip protocol icmp icmp type echo-request ip daddr {{ fake_ip_range }} counter redirect 404 | {% endif %} 405 | } 406 | 407 | chain mangle_output { 408 | type route hook output priority mangle; policy accept; 409 | {% if (cgroups_version == 1): %} 410 | meta cgroup {{ cgroup_id }} counter return 411 | {% elif (cgroups_version == 2): %} 412 | socket cgroupv2 level 2 services/{{ cgroup_name }} counter return 413 | {% endif %} 414 | fib daddr type { local, multicast, broadcast, anycast } counter return 415 | ct direction reply counter return 416 | ip daddr @reserved_ip counter return 417 | ip6 daddr @reserved_ip6 counter return 418 | ip daddr @china_ip counter return 419 | ip6 daddr @china_ip6 counter return 420 | meta nfproto ipv4 meta l4proto . th dport != @proxy_dport ip daddr != {{ fake_ip_range }} counter return 421 | meta nfproto ipv6 meta l4proto . th dport != @proxy_dport counter return 422 | meta l4proto { tcp, udp } ip dscp @bypass_dscp ip daddr != {{ fake_ip_range }} counter return 423 | meta l4proto { tcp, udp } ip6 dscp @bypass_dscp counter return 424 | meta nfproto @dns_hijack_nfproto meta l4proto { tcp, udp } th dport 53 counter return 425 | {% if (tcp_mode == 'tproxy'): %} 426 | meta nfproto @proxy_nfproto meta l4proto tcp jump router_tproxy 427 | {% elif (tcp_mode == 'tun'): %} 428 | meta nfproto @proxy_nfproto meta l4proto tcp jump router_tun 429 | {% endif %} 430 | {% if (udp_mode == 'tproxy'): %} 431 | meta nfproto @proxy_nfproto meta l4proto udp jump router_tproxy 432 | {% elif (udp_mode == 'tun'): %} 433 | meta nfproto @proxy_nfproto meta l4proto udp jump router_tun 434 | {% endif %} 435 | } 436 | 437 | chain mangle_prerouting_router { 438 | type filter hook prerouting priority mangle - 1; policy accept; 439 | {% if (tcp_mode == 'tproxy' || udp_mode == 'tproxy'): %} 440 | iifname lo meta l4proto { tcp, udp } meta mark {{ tproxy_fw_mark }} tproxy to :{{ tproxy_port }} counter accept 441 | {% endif %} 442 | {% if (tcp_mode == 'tun' || udp_mode == 'tun'): %} 443 | iifname "{{ tun_device }}" meta l4proto { icmp, tcp, udp } counter accept 444 | {% endif %} 445 | } 446 | {% endif %} 447 | 448 | {% if (lan_proxy): %} 449 | chain dstnat { 450 | type nat hook prerouting priority dstnat + 1; policy accept; 451 | iifname @lan_inbound_device meta nfproto @dns_hijack_nfproto jump lan_dns_hijack 452 | {% if (tcp_mode == 'redirect'): %} 453 | fib daddr type { local, multicast, broadcast, anycast } counter return 454 | ct direction reply counter return 455 | ip daddr @reserved_ip counter return 456 | ip6 daddr @reserved_ip6 counter return 457 | ip daddr @china_ip counter return 458 | ip6 daddr @china_ip6 counter return 459 | meta nfproto ipv4 meta l4proto . th dport != @proxy_dport ip daddr != {{ fake_ip_range }} counter return 460 | meta nfproto ipv6 meta l4proto . th dport != @proxy_dport counter return 461 | meta l4proto { tcp, udp } ip dscp @bypass_dscp ip daddr != {{ fake_ip_range }} counter return 462 | meta l4proto { tcp, udp } ip6 dscp @bypass_dscp counter return 463 | iifname @lan_inbound_device meta nfproto @proxy_nfproto jump lan_redirect 464 | {% endif %} 465 | {% if (fake_ip_ping_hijack): %} 466 | ip protocol icmp icmp type echo-request ip daddr {{ fake_ip_range }} counter redirect 467 | {% endif %} 468 | } 469 | 470 | chain mangle_prerouting_lan { 471 | type filter hook prerouting priority mangle; policy accept; 472 | fib daddr type { local, multicast, broadcast, anycast } counter return 473 | ct direction reply counter return 474 | ip daddr @reserved_ip counter return 475 | ip6 daddr @reserved_ip6 counter return 476 | ip daddr @china_ip counter return 477 | ip6 daddr @china_ip6 counter return 478 | meta nfproto ipv4 meta l4proto . th dport != @proxy_dport ip daddr != {{ fake_ip_range }} counter return 479 | meta nfproto ipv6 meta l4proto . th dport != @proxy_dport counter return 480 | meta l4proto { tcp, udp } ip dscp @bypass_dscp ip daddr != {{ fake_ip_range }} counter return 481 | meta l4proto { tcp, udp } ip6 dscp @bypass_dscp counter return 482 | meta nfproto @dns_hijack_nfproto meta l4proto { tcp, udp } th dport 53 counter return 483 | {% if (tcp_mode == 'tproxy'): %} 484 | iifname @lan_inbound_device meta nfproto @proxy_nfproto meta l4proto tcp jump lan_tproxy 485 | {% elif (tcp_mode == 'tun'): %} 486 | iifname @lan_inbound_device meta nfproto @proxy_nfproto meta l4proto tcp jump lan_tun 487 | {% endif %} 488 | {% if (udp_mode == 'tproxy'): %} 489 | iifname @lan_inbound_device meta nfproto @proxy_nfproto meta l4proto udp jump lan_tproxy 490 | {% elif (udp_mode == 'tun'): %} 491 | iifname @lan_inbound_device meta nfproto @proxy_nfproto meta l4proto udp jump lan_tun 492 | {% endif %} 493 | } 494 | {% endif %} 495 | } 496 | 497 | include "/etc/nikki/nftables/reserved_ip.nft" 498 | include "/etc/nikki/nftables/reserved_ip6.nft" 499 | 500 | {% if (bypass_china_mainland_ip): %} 501 | include "/etc/nikki/nftables/geoip_cn.nft" 502 | include "/etc/nikki/nftables/geoip6_cn.nft" 503 | {% endif %} -------------------------------------------------------------------------------- /nikki/files/ucode/include.uc: -------------------------------------------------------------------------------- 1 | import { readfile, lsdir, lstat } from 'fs'; 2 | import { connect } from 'ubus'; 3 | 4 | export function uci_bool(obj) { 5 | return obj == null ? null : obj == '1'; 6 | }; 7 | 8 | export function uci_int(obj) { 9 | return obj == null ? null : int(obj); 10 | }; 11 | 12 | export function uci_array(obj) { 13 | if (obj == null) { 14 | return []; 15 | } 16 | if (type(obj) == 'array') { 17 | return uniq(obj); 18 | } 19 | return [obj]; 20 | }; 21 | 22 | export function trim_all(obj) { 23 | if (obj == null) { 24 | return null; 25 | } 26 | if (type(obj) == 'string') { 27 | if (length(obj) == 0) { 28 | return null; 29 | } 30 | return obj; 31 | } 32 | if (type(obj) == 'array') { 33 | if (length(obj) == 0) { 34 | return null; 35 | } 36 | return obj; 37 | } 38 | if (type(obj) == 'object') { 39 | const obj_keys = keys(obj); 40 | for (let key in obj_keys) { 41 | obj[key] = trim_all(obj[key]); 42 | if (obj[key] == null) { 43 | delete obj[key]; 44 | } 45 | } 46 | if (length(keys(obj)) == 0) { 47 | return null; 48 | } 49 | return obj; 50 | } 51 | return obj; 52 | }; 53 | 54 | export function get_cgroups_version() { 55 | return system('mount | grep -q -w "^cgroup"') == 0 ? 1 : 2; 56 | }; 57 | 58 | export function get_users() { 59 | return map(split(readfile('/etc/passwd'), '\n'), (x) => split(x, ':')[0]); 60 | }; 61 | 62 | export function get_groups() { 63 | return map(split(readfile('/etc/group'), '\n'), (x) => split(x, ':')[0]); 64 | }; 65 | 66 | export function get_cgroups() { 67 | const ubus = connect(); 68 | const services = ubus.call('service', 'list'); 69 | const result = []; 70 | for (let name in services) { 71 | if (length(services[name]['instances']) > 0) { 72 | push(result, name); 73 | } 74 | } 75 | return result; 76 | }; -------------------------------------------------------------------------------- /nikki/files/ucode/mixin.uc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ucode 2 | 3 | 'use strict'; 4 | 5 | import { cursor } from 'uci'; 6 | import { connect } from 'ubus'; 7 | import { uci_bool, uci_int, uci_array, trim_all } from '/etc/nikki/ucode/include.uc'; 8 | 9 | const uci = cursor(); 10 | const ubus = connect(); 11 | 12 | const config = {}; 13 | 14 | config['log-level'] = uci.get('nikki', 'mixin', 'log_level'); 15 | config['mode'] = uci.get('nikki', 'mixin', 'mode'); 16 | config['find-process-mode'] = uci.get('nikki', 'mixin', 'match_process'); 17 | config['interface-name'] = ubus.call('network.interface', 'status', {'interface': uci.get('nikki', 'mixin', 'outbound_interface')})?.l3_device; 18 | config['ipv6'] = uci_bool(uci.get('nikki', 'mixin', 'ipv6')); 19 | config['unified-delay'] = uci_bool(uci.get('nikki', 'mixin', 'unify_delay')); 20 | config['tcp-concurrent'] = uci_bool(uci.get('nikki', 'mixin', 'tcp_concurrent')); 21 | config['disable-keep-alive'] = uci_bool(uci.get('nikki', 'mixin', 'disable_tcp_keep_alive')); 22 | config['keep-alive-idle'] = uci_int(uci.get('nikki', 'mixin', 'tcp_keep_alive_idle')); 23 | config['keep-alive-interval'] = uci_int(uci.get('nikki', 'mixin', 'tcp_keep_alive_interval')); 24 | config['global-client-fingerprint'] = uci.get('nikki', 'mixin', 'global_client_fingerprint'); 25 | 26 | config['external-ui'] = uci.get('nikki', 'mixin', 'ui_path'); 27 | config['external-ui-name'] = uci.get('nikki', 'mixin', 'ui_name'); 28 | config['external-ui-url'] = uci.get('nikki', 'mixin', 'ui_url'); 29 | config['external-controller'] = uci.get('nikki', 'mixin', 'api_listen'); 30 | config['secret'] = uci.get('nikki', 'mixin', 'api_secret'); 31 | 32 | config['allow-lan'] = uci_bool(uci.get('nikki', 'mixin', 'allow_lan')); 33 | config['port'] = uci_int(uci.get('nikki', 'mixin', 'http_port')); 34 | config['socks-port'] = uci_int(uci.get('nikki', 'mixin', 'socks_port')); 35 | config['mixed-port'] = uci_int(uci.get('nikki', 'mixin', 'mixed_port')); 36 | config['redir-port'] = uci_int(uci.get('nikki', 'mixin', 'redir_port')); 37 | config['tproxy-port'] = uci_int(uci.get('nikki', 'mixin', 'tproxy_port')); 38 | 39 | if (uci_bool(uci.get('nikki', 'mixin', 'authentication'))) { 40 | config['authentication'] = []; 41 | uci.foreach('nikki', 'authentication', (section) => { 42 | if (!uci_bool(section.enabled)) { 43 | return; 44 | } 45 | push(config['authentication'], `${section.username}:${section.password}`); 46 | }); 47 | } 48 | 49 | config['tun'] = {}; 50 | if (uci.get('nikki', 'proxy', 'tcp_mode') == 'tun' || uci.get('nikki', 'proxy', 'udp_mode') == 'tun') { 51 | config['tun']['enable'] = true; 52 | config['tun']['auto-route'] = false; 53 | config['tun']['auto-redirect'] = false; 54 | config['tun']['auto-detect-interface'] = false; 55 | config['tun']['device'] = uci.get('nikki', 'mixin', 'tun_device'); 56 | config['tun']['stack'] = uci.get('nikki', 'mixin', 'tun_stack'); 57 | config['tun']['mtu'] = uci_int(uci.get('nikki', 'mixin', 'tun_mtu')); 58 | config['tun']['gso'] = uci_bool(uci.get('nikki', 'mixin', 'tun_gso')); 59 | config['tun']['gso-max-size'] = uci_int(uci.get('nikki', 'mixin', 'tun_gso_max_size')); 60 | config['tun']['endpoint-independent-nat'] = uci_bool(uci.get('nikki', 'mixin', 'tun_endpoint_independent_nat')); 61 | if (uci_bool(uci.get('nikki', 'mixin', 'tun_dns_hijack'))) { 62 | config['tun']['dns-hijack'] = uci_array(uci.get('nikki', 'mixin', 'tun_dns_hijacks')); 63 | } 64 | } else { 65 | config['tun']['enable'] = false; 66 | } 67 | 68 | config['dns'] = {}; 69 | config['dns']['enable'] = true; 70 | config['dns']['listen'] = uci.get('nikki', 'mixin', 'dns_listen'); 71 | config['dns']['ipv6'] = uci_bool(uci.get('nikki', 'mixin', 'dns_ipv6')); 72 | config['dns']['enhanced-mode'] = uci.get('nikki', 'mixin', 'dns_mode'); 73 | config['dns']['fake-ip-range'] = uci.get('nikki', 'mixin', 'fake_ip_range'); 74 | if (uci_bool(uci.get('nikki', 'mixin', 'fake_ip_filter'))) { 75 | config['dns']['fake-ip-filter'] = uci_array(uci.get('nikki', 'mixin', 'fake_ip_filters')); 76 | } 77 | config['dns']['fake-ip-filter-mode'] = uci.get('nikki', 'mixin', 'fake_ip_filter_mode'); 78 | 79 | config['dns']['respect-rules'] = uci_bool(uci.get('nikki', 'mixin', 'dns_respect_rules')); 80 | config['dns']['prefer-h3'] = uci_bool(uci.get('nikki', 'mixin', 'dns_doh_prefer_http3')); 81 | config['dns']['use-system-hosts'] = uci_bool(uci.get('nikki', 'mixin', 'dns_system_hosts')); 82 | config['dns']['use-hosts'] = uci_bool(uci.get('nikki', 'mixin', 'dns_hosts')); 83 | if (uci_bool(uci.get('nikki', 'mixin', 'hosts'))) { 84 | config['hosts'] = {}; 85 | uci.foreach('nikki', 'hosts', (section) => { 86 | if (!uci_bool(section.enabled)) { 87 | return; 88 | } 89 | config['hosts'][section.domain_name] = uci_array(section.ip); 90 | }); 91 | } 92 | if (uci_bool(uci.get('nikki', 'mixin', 'dns_nameserver'))) { 93 | config['dns']['default-nameserver'] = []; 94 | config['dns']['proxy-server-nameserver'] = []; 95 | config['dns']['direct-nameserver'] = []; 96 | config['dns']['nameserver'] = []; 97 | config['dns']['fallback'] = []; 98 | uci.foreach('nikki', 'nameserver', (section) => { 99 | if (!uci_bool(section.enabled)) { 100 | return; 101 | } 102 | push(config['dns'][section.type], ...uci_array(section.nameserver)); 103 | }) 104 | } 105 | if (uci_bool(uci.get('nikki', 'mixin', 'dns_nameserver_policy'))) { 106 | config['dns']['nameserver-policy'] = {}; 107 | uci.foreach('nikki', 'nameserver_policy', (section) => { 108 | if (!uci_bool(section.enabled)) { 109 | return; 110 | } 111 | config['dns']['nameserver-policy'][section.matcher] = uci_array(section.nameserver); 112 | }); 113 | } 114 | 115 | config['sniffer'] = {}; 116 | config['sniffer']['enable'] = uci_bool(uci.get('nikki', 'mixin', 'sniffer')); 117 | config['sniffer']['force-dns-mapping'] = uci_bool(uci.get('nikki', 'mixin', 'sniffer_sniff_dns_mapping')); 118 | config['sniffer']['parse-pure-ip'] = uci_bool(uci.get('nikki', 'mixin', 'sniffer_sniff_pure_ip')); 119 | if (uci_bool(uci.get('nikki', 'mixin', 'sniffer_force_domain_name'))) { 120 | config['sniffer']['force-domain'] = uci_array(uci.get('nikki', 'mixin', 'sniffer_force_domain_names')); 121 | } 122 | if (uci_bool(uci.get('nikki', 'mixin', 'sniffer_ignore_domain_name'))) { 123 | config['sniffer']['skip-domain'] = uci_array(uci.get('nikki', 'mixin', 'sniffer_ignore_domain_names')); 124 | } 125 | if (uci_bool(uci.get('nikki', 'mixin', 'sniffer_sniff'))) { 126 | config['sniffer']['sniff'] = {}; 127 | config['sniffer']['sniff']['HTTP'] = {}; 128 | config['sniffer']['sniff']['TLS'] = {}; 129 | config['sniffer']['sniff']['QUIC'] = {}; 130 | uci.foreach('nikki', 'sniff', (section) => { 131 | if (!uci_bool(section.enabled)) { 132 | return; 133 | } 134 | config['sniffer']['sniff'][section.protocol]['port'] = uci_array(section.port); 135 | config['sniffer']['sniff'][section.protocol]['override-destination'] = uci_bool(section.overwrite_destination); 136 | }); 137 | } 138 | 139 | config['profile'] = {}; 140 | config['profile']['store-selected'] = uci_bool(uci.get('nikki', 'mixin', 'selection_cache')); 141 | config['profile']['store-fake-ip'] = uci_bool(uci.get('nikki', 'mixin', 'fake_ip_cache')); 142 | 143 | if (uci_bool(uci.get('nikki', 'mixin', 'rule_provider'))) { 144 | config['rule-providers'] = {}; 145 | uci.foreach('nikki', 'rule_provider', (section) => { 146 | if (!uci_bool(section.enabled)) { 147 | return; 148 | } 149 | if (section.type == 'http') { 150 | config['rule-providers'][section.name] = { 151 | type: section.type, 152 | url: section.url, 153 | proxy: section.node, 154 | size_limit: section.file_size_limit, 155 | format: section.file_format, 156 | behavior: section.behavior, 157 | interval: section.update_interval, 158 | } 159 | } else if (section.type == 'file') { 160 | config['rule-providers'][section.name] = { 161 | type: section.type, 162 | path: section.file_path, 163 | format: section.file_format, 164 | behavior: section.behavior, 165 | } 166 | } 167 | }) 168 | } 169 | if (uci_bool(uci.get('nikki', 'mixin', 'rule'))) { 170 | config['nikki-rules'] = []; 171 | uci.foreach('nikki', 'rule', (section) => { 172 | if (!uci_bool(section.enabled)) { 173 | return; 174 | } 175 | const rule = [ section.type, section.matcher, section.node, uci_bool(section.no_resolve) ? 'no_resolve' : null ]; 176 | push(config['nikki-rules'], join(',', filter(rule, (item) => item != null && item != ''))); 177 | }) 178 | } 179 | 180 | const geoip_format = uci.get('nikki', 'mixin', 'geoip_format'); 181 | config['geodata-mode'] = geoip_format == null ? null : geoip_format == 'dat'; 182 | config['geodata-loader'] = uci.get('nikki', 'mixin', 'geodata_loader'); 183 | config['geox-url'] = {}; 184 | config['geox-url']['geosite'] = uci.get('nikki', 'mixin', 'geosite_url'); 185 | config['geox-url']['mmdb'] = uci.get('nikki', 'mixin', 'geoip_mmdb_url'); 186 | config['geox-url']['geoip'] = uci.get('nikki', 'mixin', 'geoip_dat_url'); 187 | config['geox-url']['asn'] = uci.get('nikki', 'mixin', 'geoip_asn_url'); 188 | config['geo-auto-update'] = uci_bool(uci.get('nikki', 'mixin', 'geox_auto_update')); 189 | config['geo-update-interval'] = uci_int(uci.get('nikki', 'mixin', 'geox_update_interval')); 190 | 191 | print(trim_all(config)); -------------------------------------------------------------------------------- /uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # uninstall 4 | if [ -x "/bin/opkg" ]; then 5 | opkg remove luci-i18n-nikki-zh-cn 6 | opkg remove luci-app-nikki 7 | opkg remove nikki 8 | elif [ -x "/usr/bin/apk" ]; then 9 | apk del luci-i18n-nikki-zh-cn 10 | apk del luci-app-nikki 11 | apk del nikki 12 | fi 13 | # remove config 14 | rm -f /etc/config/nikki 15 | # remove files 16 | rm -rf /etc/nikki 17 | --------------------------------------------------------------------------------