├── .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 |     [](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 | [](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 |
--------------------------------------------------------------------------------