├── image ├── status.png └── setting.png ├── luci-app-tailscale-community ├── root │ └── usr │ │ └── share │ │ ├── luci │ │ └── menu.d │ │ │ └── luci-app-tailscale-community.json │ │ └── rpcd │ │ ├── acl.d │ │ └── luci-app-tailscale-community.json │ │ └── ucode │ │ └── tailscale.uc ├── Makefile ├── po │ ├── templates │ │ └── community.pot │ └── zh_Hans │ │ └── community.po └── htdocs │ └── luci-static │ └── resources │ └── view │ └── tailscale.js ├── README.CN.md ├── README.md ├── .github └── workflows │ └── build.yml └── LICENSE /image/status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tokisaki-Galaxy/luci-app-tailscale-community/HEAD/image/status.png -------------------------------------------------------------------------------- /image/setting.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tokisaki-Galaxy/luci-app-tailscale-community/HEAD/image/setting.png -------------------------------------------------------------------------------- /luci-app-tailscale-community/root/usr/share/luci/menu.d/luci-app-tailscale-community.json: -------------------------------------------------------------------------------- 1 | { 2 | "admin/services/tailscale": { 3 | "title": "Tailscale", 4 | "order": 90, 5 | "action": { 6 | "type": "view", 7 | "path": "tailscale" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /luci-app-tailscale-community/Makefile: -------------------------------------------------------------------------------- 1 | include $(TOPDIR)/rules.mk 2 | 3 | LUCI_TITLE:=LuCI support for Tailscale 4 | LUCI_URL:=https://github.com/tokisaki-galaxy/luci-app-tailscale-community 5 | PKG_DESCRIPTION:=Provides a LuCI Web management interface for Tailscale, allowing viewing status, configuring nodes and daemons. 6 | PKG_MAINTAINER:=Tokisaki-Galaxy 7 | LUCI_DEPENDS:=+tailscale +ip +luci-base 8 | LUCI_PKGARCH:=all 9 | 10 | include $(TOPDIR)/feeds/luci/luci.mk 11 | 12 | # call BuildPackage - OpenWrt buildroot signature 13 | -------------------------------------------------------------------------------- /luci-app-tailscale-community/root/usr/share/rpcd/acl.d/luci-app-tailscale-community.json: -------------------------------------------------------------------------------- 1 | { 2 | "luci-app-tailscale-community": { 3 | "description": "Allow user access to tailscale", 4 | "read": { 5 | "ubus": { 6 | "tailscale": [ 7 | "get_status", 8 | "get_settings", 9 | "get_subroutes" 10 | ] 11 | }, 12 | "uci": [ "tailscale" ] 13 | }, 14 | "write": { 15 | "ubus": { 16 | "tailscale": [ 17 | "do_login", 18 | "do_logout", 19 | "setup_firewall", 20 | "set_settings" 21 | ] 22 | }, 23 | "uci": [ "tailscale" ] 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /README.CN.md: -------------------------------------------------------------------------------- 1 | 2 | # LuCI App for Tailscale (社区版) 3 | 4 |

5 | 构建状态 6 | 许可证 7 | OpenWrt 版本 8 |

9 | 10 |

11 | 12 | Powered by GitHub Actions 13 | 14 | 15 | GitHub issues 16 | 17 | 18 | GitHub stars 19 | 20 |

21 | 22 |

23 | 简体中文 24 | English 25 |

26 | 27 | 一个社区维护的 LuCI 应用,用于在 OpenWrt 上管理 Tailscale。此应用提供了一个友好的 Web 界面,让您可以直接从 LuCI 查看 Tailscale 状态和配置其设置。 28 | 29 | ## 功能特性 30 | 31 | - **状态仪表盘**: 32 | - 查看 Tailscale 服务的运行状态。 33 | - 显示设备的 Tailscale IPv4 和 IPv6 地址。 34 | - 查看您的 Tailnet 域名。 35 | - 详细的网络设备(节点)列表,包括: 36 | - 在线/离线状态。 37 | - 主机名和 DNS 名称。 38 | - Tailscale IP 地址。 39 | - 操作系统。 40 | - 连接类型(例如,直连、中继)。 41 | - 离线设备的最后在线时间。 42 | 43 | - **节点设置**: 44 | - 使用 `tailscale set` 命令即时应用设置,无需重启服务。 45 | - 切换 `接受路由`。 46 | - 切换 `作为出口节点`。 47 | - 配置 `广播路由`。 48 | - 设置要使用的特定 `出口节点`。 49 | - 在使用出口节点时切换 `允许访问 LAN`。 50 | - 启用/禁用子网路由的 SNAT。 51 | - 启用/禁用内置 SSH 服务器。 52 | - 切换 `Shields Up` 模式。 53 | - 设置自定义主机名。 54 | 55 | - **守护进程环境设置**: 56 | - 为 Tailscale 守护进程配置环境变量(需要重启服务)。 57 | - 为有问题的网络设置自定义 MTU。 58 | - 为资源受限的设备启用内存优化模式。 59 | 60 | ## 界面截图 61 | 62 | **状态页面** 63 | ![状态页面截图](image/status.png) 64 | 65 | **设置页面** 66 | ![设置页面截图](image/setting.png) 67 | 68 | ## 安装 69 | 70 | ### 前提条件 71 | 72 | 您的 OpenWrt 设备上必须已安装 `tailscale` 和 `ip` 软件包。 73 | 74 | ```bash 75 | opkg update 76 | opkg install tailscale ip 77 | ``` 78 | 79 | ### 安装 LuCI 应用 80 | 81 | 1. 从 [Github Release](https://github.com/Tokisaki-Galaxy/luci-app-tailscale-community/releases) 下载最新稳定的 `.ipk` 软件包。 82 | - 如果有特殊需求,也可以从[GitHub Actions artifacts](https://github.com/actions) 下载最新的基于调试用途的 `.ipk` 软件包。 83 | 2. 将 `.ipk` 文件传输到您的 OpenWrt 路由器(例如,使用 `scp`)。 84 | 3. 使用 `opkg` 安装软件包: 85 | 86 | ```bash 87 | opkg install luci-app-tailscale-community_*.ipk 88 | ``` 89 | 90 | 安装后,您应该能在 LuCI 的“服务”选项卡下找到“Tailscale”菜单。 91 | 92 | ## 从源码构建 93 | 94 | 您也可以使用 OpenWrt SDK 自行构建此软件包。构建过程在 [`.github/workflows/build.yml`](.github/workflows/build.yml) 文件中定义,可作为参考。 95 | 96 | 1. 克隆 OpenWrt SDK。 97 | 2. 将此仓库克隆到 SDK 的 `package/` 目录下。 98 | 3. 运行 `make menuconfig` 并在 `LuCI` -> `Applications` 下选择 `luci-app-tailscale-community`。 99 | 4. 运行 `make` 编译软件包。 100 | 101 | ## 许可证 102 | 103 | 本项目采用 Apache 2.0 许可证。详情请参阅 [LICENSE](LICENSE) 文件。# luci-app-tailscale-community 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LuCI App for Tailscale (Community) 2 | 3 |

4 | 构建状态 5 | 许可证 6 | OpenWrt 版本 7 |

8 | 9 |

10 | 11 | Powered by GitHub Actions 12 | 13 | 14 | GitHub issues 15 | 16 | 17 | GitHub stars 18 | 19 |

20 | 21 |

22 | 简体中文 23 | English 24 |

25 | 26 | A community-maintained LuCI application for managing Tailscale on OpenWrt. This app provides a user-friendly web interface to view Tailscale status and configure its settings directly from LuCI. 27 | 28 | ## Features 29 | 30 | - **Status Dashboard**: 31 | - View the running status of the Tailscale service. 32 | - Display your device's Tailscale IPv4 and IPv6 addresses. 33 | - See your Tailnet name. 34 | - A detailed list of all network devices (peers), including: 35 | - Online/Offline status. 36 | - Hostname and DNS name. 37 | - Tailscale IPs. 38 | - Operating System. 39 | - Connection type (e.g., Direct, Relay). 40 | - Last seen time for offline devices. 41 | 42 | - **Node Settings**: 43 | - Instantly apply settings using the `tailscale set` command without a service restart. 44 | - Toggle `Accept Routes`. 45 | - Toggle `Advertise as Exit Node`. 46 | - Configure `Advertise Routes`. 47 | - Set a specific `Exit Node` to use. 48 | - Toggle `Allow LAN Access` when using an exit node. 49 | - Enable/disable SNAT for subnet routes. 50 | - Enable/disable the built-in SSH server. 51 | - Toggle `Shields Up` mode. 52 | - Set a custom hostname. 53 | 54 | - **Daemon Environment Settings**: 55 | - Configure environment variables for the Tailscale daemon (requires a service restart). 56 | - Set a custom MTU for problematic networks. 57 | - Enable a memory reduction mode for resource-constrained devices. 58 | 59 | ## Screenshots 60 | 61 | **Status Page** 62 | ![Status Page Screenshot](image/status.png) 63 | 64 | **Settings Page** 65 | ![Settings Page Screenshot](image/setting.png) 66 | 67 | ## Installation 68 | 69 | ### Prerequisites 70 | 71 | You must have the `tailscale` and `ip` packages installed on your OpenWrt device. 72 | 73 | ```bash 74 | opkg update 75 | opkg install tailscale ip 76 | ``` 77 | 78 | ### Install the LuCI App 79 | 80 | 1. download the latest and stable `. ipk` software package from [Github Release](https://github.com/tokisaki-galaxy/Luci-app-tailscale-community/releases). 81 | - If you have special requirements, you can also download the latest `. ipk` software package for debugging purposes from [Github Actions Artifacts](https://github.com/actions). 82 | 2. Transfer the `.ipk` file to your OpenWrt router (e.g., using `scp`). 83 | 3. Install the package using `opkg`: 84 | 85 | ```bash 86 | opkg install luci-app-tailscale-community_*.ipk 87 | ``` 88 | 89 | After installation, you should find the "Tailscale" menu under the "Services" tab in LuCI. 90 | 91 | ## Building from Source 92 | 93 | You can also build the package yourself using the OpenWrt SDK. The build process is defined in the [`.github/workflows/build.yml`](.github/workflows/build.yml) file, which can be used as a reference. 94 | 95 | 1. Clone the OpenWrt SDK. 96 | 2. Clone this repository into the `package/` directory of the SDK. 97 | 3. Run `make menuconfig` and select `luci-app-tailscale-community` under `LuCI` -> `Applications`. 98 | 4. Run `make` to compile the package. 99 | 100 | ## License 101 | 102 | This project is licensed under the Apache 2.0 License. See the [LICENSE](LICENSE) file for details. 103 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build OpenWrt Package 2 | 3 | on: 4 | push: 5 | branches: [ "main", "master" ] 6 | tags: [ "v*" ] 7 | pull_request: 8 | branches: [ "main", "master" ] 9 | workflow_dispatch: 10 | 11 | env: 12 | OPENWRT_VERSION: v24.10.3 13 | PACKAGE_NAME: luci-app-tailscale-community 14 | SDK_DIR: /home/runner/work/openwrt-sdk 15 | 16 | permissions: 17 | contents: write 18 | 19 | jobs: 20 | build: 21 | name: Build for ${{ matrix.target.name }} 22 | runs-on: ubuntu-latest 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | target: 27 | - name: x86_64 28 | path: x86/64 29 | 30 | steps: 31 | # 步骤 1: 检出代码 32 | - name: Checkout package repository 33 | uses: actions/checkout@v4 34 | 35 | # 步骤 2: 安装依赖 36 | - name: Install dependencies 37 | run: | 38 | sudo apt-get update 39 | sudo apt-get install -y build-essential rsync unzip zstd 40 | 41 | # 步骤 3: 缓存 OpenWrt SDK 42 | # 缓存已下载和初步设置好的 SDK 目录。 43 | - name: Cache OpenWrt SDK 44 | id: cache-sdk 45 | uses: actions/cache@v4 46 | with: 47 | # 需要缓存的目录路径 48 | path: ${{ env.SDK_DIR }} 49 | # 缓存键:结合操作系统、目标架构和 OpenWrt 版本,确保环境变化时缓存失效 50 | key: ${{ runner.os }}-sdk-${{ matrix.target.name }}-${{ env.OPENWRT_VERSION }} 51 | 52 | # 步骤 4: 准备 SDK (仅在缓存未命中时运行) 53 | # 这个组合步骤包含了下载、解压 SDK 和更新 feeds,因为这些都是 SDK 准备过程的一部分,应该一起被缓存。 54 | - name: Download, extract OpenWrt SDK and update feeds 55 | if: steps.cache-sdk.outputs.cache-hit != 'true' 56 | run: | 57 | # 从环境变量中移除 'v' 前缀以匹配版本号 58 | release_version="${OPENWRT_VERSION#v}" 59 | sdk_url_prefix="https://downloads.openwrt.org/releases/${release_version}/targets/${{ matrix.target.path }}" 60 | 61 | # 查找 SDK 文件名 62 | sdk_filename=$(curl -s "$sdk_url_prefix/" | \ 63 | grep -o '' | \ 64 | sed -e 's/.*//' | \ 65 | head -n 1) 66 | 67 | if [ -z "$sdk_filename" ]; then 68 | echo "::error::Could not find SDK file at ${sdk_url_prefix}/" 69 | echo "Available files:" 70 | curl -s "$sdk_url_prefix/" | grep -o '[^<]*' | sed -e 's/\(.*\)/\1\t\2/' 71 | exit 1 72 | fi 73 | 74 | echo "Downloading SDK: ${sdk_url_prefix}/${sdk_filename}" 75 | wget -q "${sdk_url_prefix}/${sdk_filename}" 76 | 77 | echo "Extracting SDK..." 78 | # 解压 SDK 并重命名为固定的目录名,以匹配缓存路径 79 | tar -xf "$sdk_filename" 80 | mv $(find . -maxdepth 1 -type d -name 'openwrt-sdk-*') ${{ env.SDK_DIR }} 81 | 82 | echo "Updating feeds..." 83 | cd ${{ env.SDK_DIR }} 84 | ./scripts/feeds update -a 85 | ./scripts/feeds install -a 86 | cd .. 87 | 88 | # 步骤 5: 准备构建环境 (包含智能版本控制) 89 | - name: Prepare build environment 90 | working-directory: ${{ env.SDK_DIR }} 91 | run: | 92 | rm -rf package/${PACKAGE_NAME} 93 | mkdir -p package/${PACKAGE_NAME} 94 | src_dir="${{ github.workspace }}" 95 | 96 | echo "Searching for Makefile in: $src_dir" 97 | # 查找 Makefile,最大深度3,按路径长度排序,优先取最短路径的(通常是根目录的) 98 | # 过滤掉路径中包含 /bin/ 或 /obj/ 的干扰项 99 | FOUND_MK=$(find "$src_dir" -maxdepth 3 -name Makefile -not -path "*/.*" | awk '{ print length, $0 }' | sort -n | cut -d" " -f2- | head -n 1) 100 | 101 | if [ -z "$FOUND_MK" ]; then 102 | echo "::error::Could not find any Makefile in the repository!" 103 | ls -R "$src_dir" 104 | exit 1 105 | fi 106 | 107 | echo "Found primary Makefile at: $FOUND_MK" 108 | pkg_src_dir=$(dirname "$FOUND_MK") 109 | echo "Determined package source root: $pkg_src_dir" 110 | 111 | rsync -a --delete "${pkg_src_dir}/" ./package/${PACKAGE_NAME}/ 112 | 113 | # 验证复制结果 114 | TARGET_MK="./package/${PACKAGE_NAME}/Makefile" 115 | if [ ! -f "$TARGET_MK" ]; then 116 | echo "::error::Failed to migrate code. Makefile missing at $TARGET_MK" 117 | ls -R package/${PACKAGE_NAME} 118 | exit 1 119 | fi 120 | echo "Code migration successful. Structure verified." 121 | 122 | # 检查 Makefile 中是否已存在 PKG_VERSION 定义 123 | if grep -q "^PKG_VERSION:=" "$TARGET_MK"; then 124 | echo "Static PKG_VERSION found in Makefile. Using upstream version definition." 125 | echo "::notice::You should not manually define PKG_VERSION or PKG_RELEASE in the Makefile. You should try using dynamic versioning." 126 | else 127 | echo "No static PKG_VERSION found. Applying dynamic versioning." 128 | 129 | NEW_VER="" 130 | NEW_REL="" 131 | 132 | if [ "${{ github.ref_type }}" = "tag" ]; then 133 | # === 场景 A: Release Tag === 134 | NEW_VER=$(echo "${{ github.ref_name }}" | sed 's/^v//') 135 | NEW_REL="1" 136 | echo "Build triggered by Tag: Using Version=$NEW_VER, Release=$NEW_REL" 137 | else 138 | # === 场景 B: 普通 Commit (调试构建) === 139 | # Version = 日期 (YYYY.MM.DD) 140 | NEW_VER=$(date +"%Y.%m.%d") 141 | 142 | # Release = BuildNumber.git-ShortHash 143 | SHORT_SHA=$(git -C "$src_dir" rev-parse --short HEAD) 144 | NEW_REL="${{ github.run_number }}.git-$SHORT_SHA" 145 | echo "Build triggered by Commit: Using Version=$NEW_VER, Release=$NEW_REL" 146 | fi 147 | 148 | # 动态注入版本号 149 | sed -i "s|include \$(TOPDIR)/rules.mk|include \$(TOPDIR)/rules.mk\nPKG_VERSION:=$NEW_VER\nPKG_RELEASE:=$NEW_REL|" "$TARGET_MK" 150 | fi 151 | 152 | translation_pkg=${PACKAGE_NAME#luci-app-} 153 | mkdir -p ./bin/packages 154 | find ./bin/packages -type f -name "${PACKAGE_NAME}*.ipk" -delete 155 | find ./bin/packages -type f -name "luci-i18n-${translation_pkg}-*.ipk" -delete 156 | { 157 | echo "CONFIG_PACKAGE_${PACKAGE_NAME}=m" 158 | echo "CONFIG_PACKAGE_luci-i18n-${translation_pkg}-zh-cn=m" 159 | echo "CONFIG_LUCI_LANG_zh_Hans=y" 160 | } > .config 161 | make defconfig 162 | 163 | # 步骤 6: 编译软件包 (IPK) 164 | - name: Compile the package (IPK) 165 | working-directory: ${{ env.SDK_DIR }} 166 | run: | 167 | make package/${PACKAGE_NAME}/compile V=s -j$(($(nproc) + 1)) 168 | 169 | mkdir -p output_ipk 170 | translation_pkg=${PACKAGE_NAME#luci-app-} 171 | find ./bin/packages -type f \( -name "${PACKAGE_NAME}*.ipk" -o -name "luci-i18n-${translation_pkg}-*.ipk" \) -exec cp {} output_ipk/ \; 172 | 173 | # 步骤 7: 编译软件包 (APK) 174 | - name: Compile the package (APK) 175 | working-directory: ${{ env.SDK_DIR }} 176 | run: | 177 | make package/${PACKAGE_NAME}/clean 178 | 179 | translation_pkg=${PACKAGE_NAME#luci-app-} 180 | { 181 | echo "CONFIG_PACKAGE_${PACKAGE_NAME}=m" 182 | echo "CONFIG_PACKAGE_luci-i18n-${translation_pkg}-zh-cn=m" 183 | echo "CONFIG_LUCI_LANG_zh_Hans=y" 184 | echo "CONFIG_USE_APK=y" 185 | echo "CONFIG_USE_OPKG=n" 186 | } > .config 187 | make defconfig 188 | 189 | if ! grep -q '^CONFIG_USE_APK=y' .config; then 190 | echo "::warning::CONFIG_USE_APK not available in this SDK. Skipping APK build." 191 | exit 0 192 | fi 193 | 194 | make package/${PACKAGE_NAME}/compile V=s -j$(($(nproc) + 1)) 195 | 196 | mkdir -p output_apk 197 | find ./bin/packages -type f \( -name "${PACKAGE_NAME}*.apk" -o -name "luci-i18n-${translation_pkg}-*.apk" \) -exec cp {} output_apk/ \; 198 | 199 | # 步骤 8: 收集和准备文件 200 | - name: Collect artifacts 201 | id: collect_artifacts 202 | working-directory: ${{ env.SDK_DIR }} 203 | run: | 204 | rm -rf upload 205 | mkdir -p upload 206 | 207 | if [ -d "output_ipk" ]; then 208 | echo "Copying IPK files..." 209 | cp output_ipk/*.ipk upload/ 2>/dev/null || true 210 | fi 211 | if [ -d "output_apk" ]; then 212 | echo "Copying APK files..." 213 | cp output_apk/*.apk upload/ 2>/dev/null || true 214 | fi 215 | 216 | if [ -z "$(ls -A upload)" ]; then 217 | echo "::error::No packages found!" 218 | ls -R ./bin/ 219 | exit 1 220 | fi 221 | 222 | echo "Found packages:" 223 | ls -l upload/ 224 | 225 | echo "path=upload" >> $GITHUB_OUTPUT 226 | 227 | # 步骤 9: 上传构建产物 228 | - name: Upload artifacts 229 | uses: actions/upload-artifact@v4 230 | with: 231 | name: ${{ env.PACKAGE_NAME }}-${{ matrix.target.name }} 232 | path: ${{ env.SDK_DIR }}/${{ steps.collect_artifacts.outputs.path }} 233 | if-no-files-found: error 234 | 235 | release: 236 | name: Publish Release 237 | runs-on: ubuntu-latest 238 | needs: build 239 | if: startsWith(github.ref, 'refs/tags/') 240 | steps: 241 | - name: Download build artifacts 242 | uses: actions/download-artifact@v4 243 | with: 244 | path: release-assets 245 | 246 | - name: Create GitHub Release 247 | uses: softprops/action-gh-release@v2 248 | with: 249 | tag_name: ${{ github.ref_name }} 250 | name: ${{ github.ref_name }} 251 | generate_release_notes: true 252 | files: | 253 | release-assets/**/*.ipk 254 | release-assets/**/*.apk 255 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /luci-app-tailscale-community/root/usr/share/rpcd/ucode/tailscale.uc: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ucode 2 | 3 | 'use strict'; 4 | 5 | import { access, popen, readfile, writefile, unlink } from 'fs'; 6 | import { cursor } from 'uci'; 7 | 8 | const uci = cursor(); 9 | 10 | function exec(command) { 11 | let stdout_content = ''; 12 | let p = popen(command, 'r'); 13 | sleep(100); 14 | if (p == null) { 15 | return { code: -1, stdout: '', stderr: `Failed to execute: ${command}` }; 16 | } 17 | for (let line = p.read('line'); length(line); line = p.read('line')) { 18 | stdout_content = stdout_content+line; 19 | } 20 | stdout_content = rtrim(stdout_content); 21 | stdout_content = split(stdout_content, '\n'); 22 | 23 | let exit_code = p.close(); 24 | let stderr_content = ''; 25 | if (exit_code != 0) { 26 | stderr_content = stdout_content; 27 | } 28 | return { code: exit_code, stdout: stdout_content, stderr: stderr_content }; 29 | } 30 | 31 | function shell_quote(s) { 32 | if (s == null || s == '') return "''"; 33 | return "'" + replace(s, "'", "'\\''") + "'"; 34 | } 35 | 36 | const methods = {}; 37 | 38 | methods.get_status = { 39 | call: function() { 40 | let data = { 41 | status: '', 42 | version: '', 43 | TUNMode: '', 44 | health: '', 45 | ipv4: "Not running", 46 | ipv6: null, 47 | domain_name: '', 48 | peers: [] 49 | }; 50 | if (access('/usr/sbin/tailscale')==true || access('/usr/bin/tailscale')==true){ }else{ 51 | data.status = 'not_installed'; 52 | return data; 53 | } 54 | 55 | let status_json_output = exec('tailscale status --json'); 56 | let peer_map = {}; 57 | if (status_json_output.code == 0 && length(status_json_output.stdout) > 0) { 58 | try { 59 | let status_data = json(join('',status_json_output.stdout)); 60 | data.version = status_data?.Version || 'Unknown'; 61 | data.health = status_data?.Health || ''; 62 | data.TUNMode = status_data?.TUN || 'true'; 63 | if (status_data?.BackendState == 'Running') { data.status = 'running'; } 64 | if (status_data?.BackendState == 'NeedsLogin') { data.status = 'logout'; } 65 | 66 | data.ipv4 = status_data?.Self?.TailscaleIPs?.[0] || 'No IP assigned'; 67 | data.ipv6 = status_data?.Self?.TailscaleIPs?.[1] || null; 68 | data.domain_name = status_data?.CurrentTailnet?.Name || ''; 69 | 70 | // peers list 71 | for (let p in status_data?.Peer) { 72 | p = status_data.Peer[p]; 73 | peer_map[p.ID] = { 74 | ip: join('
', p?.TailscaleIPs) || '', 75 | hostname: split(p?.DNSName || '','.')[0] || '', 76 | ostype: p?.OS, 77 | online: p?.Online, 78 | linkadress: (!p?.CurAddr) ? p?.Relay : p?.CurAddr, 79 | lastseen: p?.LastSeen, 80 | tx: p?.TxBytes || '', 81 | rx: p?.RxBytes || '' 82 | }; 83 | } 84 | } catch (e) { /* ignore */ } 85 | } 86 | 87 | data.peers = peer_map; 88 | return data; 89 | } 90 | }; 91 | 92 | methods.get_settings = { 93 | call: function() { 94 | let settings = {}; 95 | uci.load('tailscale'); 96 | let state_file_path = uci.get('tailscale', 'settings', 'state_file') || "/var/lib/tailscale/tailscaled.state"; 97 | if (access(state_file_path)) { 98 | try { 99 | let state_content = readfile(state_file_path); 100 | if (state_content != null) { 101 | let state_data = json(state_content); 102 | let profiles_b64 = state_data?._profiles; 103 | if (!profiles_b64) return settings; 104 | 105 | let profiles_data = json(b64dec(profiles_b64)); 106 | let profiles_key = null; 107 | for (let key in profiles_data) { 108 | profiles_key = key; 109 | break; 110 | } 111 | profiles_key = 'profile-'+profiles_key; 112 | 113 | let status_data = json(b64dec(state_data?.[profiles_key])); 114 | if (status_data != null) { 115 | settings.accept_routes = status_data?.RouteAll || false; 116 | settings.advertise_exit_node = status_data?.AdvertiseExitNode || false; 117 | settings.advertise_routes = status_data?.AdvertiseRoutes || []; 118 | settings.exit_node = status_data?.ExitNodeID || ""; 119 | settings.exit_node_allow_lan_access = status_data?.ExitNodeAllowLANAccess || false; 120 | settings.shields_up = status_data?.ShieldsUp || false; 121 | settings.ssh = status_data?.RunSSH || false; 122 | settings.runwebclient = status_data?.RunWebClient || false; 123 | settings.nosnat = status_data?.NoSNAT || false; 124 | settings.disable_magic_dns = !status_data?.CorpDNS || false; 125 | settings.fw_mode = split(uci.get('tailscale', 'settings', 'fw_mode'),' ')[0] || 'nftables'; 126 | } 127 | } 128 | } catch (e) { /* ignore */ } 129 | } 130 | return settings; 131 | } 132 | }; 133 | 134 | methods.set_settings = { 135 | args: { form_data: {} }, 136 | call: function(request) { 137 | const form_data = request.args.form_data; 138 | if (form_data == null || length(form_data) == 0) { 139 | return { error: 'Missing or invalid form_data parameter. Please provide settings data.' }; 140 | } 141 | let args = ['set']; 142 | 143 | push(args,'--accept-routes=' + (form_data.accept_routes == '1')); 144 | push(args,'--advertise-exit-node=' + (form_data.advertise_exit_node == '1')); 145 | push(args,'--exit-node-allow-lan-access=' + (form_data.exit_node_allow_lan_access == '1')); 146 | push(args,'--ssh=' + (form_data.ssh == '1')); 147 | push(args,'--accept-dns=' + (form_data.disable_magic_dns != '1')); 148 | push(args,'--shields-up=' + (form_data.shields_up == '1')); 149 | push(args,'--webclient=' + (form_data.runwebclient == '1')); 150 | push(args,'--snat-subnet-routes=' + (form_data.nosnat != '1')); 151 | push(args,'--advertise-routes ' + (shell_quote(join(',',form_data.advertise_routes)) || '\"\"')); 152 | push(args,'--exit-node ' + (shell_quote(form_data.exit_node) || '\"\"')); 153 | push(args,'--hostname ' + (shell_quote(form_data.hostname) || '\"\"')); 154 | 155 | let cmd_array = 'tailscale '+join(' ', args); 156 | let set_result = exec(cmd_array); 157 | if (set_result.code != 0) { 158 | return { error: 'Failed to apply node settings: ' + set_result.stderr }; 159 | } 160 | 161 | uci.load('tailscale'); 162 | for (let key in form_data) { 163 | uci.set('tailscale', 'settings', key, form_data[key]); 164 | } 165 | uci.save('tailscale'); 166 | uci.commit('tailscale'); 167 | 168 | // process reduce memory https://github.com/GuNanOvO/openwrt-tailscale 169 | // some new versions of Tailscale may not work well with this method 170 | //if (form_data.daemon_mtu != "" || form_data.daemon_reduce_memory != "") { 171 | // popen('/bin/sh -c ". ' + env_script_path + ' && /etc/init.d/tailscale restart" &'); 172 | //} 173 | return { success: true }; 174 | } 175 | }; 176 | 177 | methods.do_login = { 178 | args: { form_data: {} }, 179 | call: function(request) { 180 | const form_data = request.args.form_data; 181 | let loginargs = []; 182 | if (form_data == null || length(form_data) == 0) { 183 | return { error: 'Missing or invalid form_data parameter. Please provide login data.' }; 184 | } 185 | 186 | let status=methods.get_status.call(); 187 | if (status.status != 'logout') { 188 | return { error: 'Tailscale is already logged in and running.' }; 189 | } 190 | 191 | // --- 1. Prepare and Run Login Command (Once) --- 192 | const loginserver = trim(form_data.loginserver) || ''; 193 | const loginserver_authkey = trim(form_data.loginserver_authkey) || ''; 194 | 195 | if (loginserver!='') { 196 | push(loginargs,'--login-server '+shell_quote(loginserver)); 197 | if (loginserver_authkey!='') { 198 | push(loginargs,'--auth-key '+shell_quote(loginserver_authkey)); 199 | } 200 | } 201 | 202 | // Run the command in the background using /bin/sh -c to handle the '&' correctly 203 | let login_cmd = 'tailscale login '+join(' ', loginargs); 204 | popen('/bin/sh -c "' + login_cmd + ' &"', 'r'); 205 | 206 | // --- 2. Loop to Check Status for URL --- 207 | let max_attempts = 15; 208 | let interval = 2000; 209 | 210 | for (let i = 0; i < max_attempts; i++) { 211 | let tresult = exec('tailscale status'); 212 | for (let line in tresult.stdout) { 213 | let trline = trim(line); 214 | if (index(trline, 'http') != -1) { 215 | let parts = split(trline, ' '); 216 | for (let part in parts) { 217 | if (index(part, 'http') != -1) { 218 | return { url: part }; 219 | } 220 | } 221 | } 222 | } 223 | sleep(interval); 224 | } 225 | return { error: 'Could not retrieve login URL from tailscale command after 30 seconds.' }; 226 | } 227 | }; 228 | 229 | methods.do_logout = { 230 | call: function() { 231 | let status=methods.get_status.call(); 232 | if (status.status != 'running') { 233 | return { error: 'Tailscale is not running. Cannot perform logout.' }; 234 | } 235 | 236 | let logout_result = exec('tailscale logout'); 237 | if (logout_result.code != 0) { 238 | return { error: 'Failed to logout: ' + logout_result.stderr }; 239 | } 240 | return { success: true }; 241 | } 242 | }; 243 | 244 | methods.get_subroutes = { 245 | call: function() { 246 | try { 247 | let cmd = 'ip -j route'; 248 | let result = exec(cmd); 249 | let subnets = []; 250 | 251 | if (result.code == 0 && length(result.stdout) > 0) { 252 | let routes_json = json(join('',result.stdout)); 253 | 254 | for (let route in routes_json) { 255 | // We need to filter out local subnets 256 | // 1. 'dst' (target address) is not' default' (default gateway) 257 | // 2. 'scope' is' link' (indicating directly connected network) 258 | // 3. It is an IPv4 address (simple judgment: including'.') 259 | if (route?.dst && route.dst != 'default' && route?.scope == 'link' && index(route.dst,'.') != -1) { 260 | push(subnets,route.dst); 261 | } 262 | } 263 | } 264 | return { routes: subnets }; 265 | } 266 | catch(e) { 267 | return { routes: [] }; 268 | } 269 | } 270 | }; 271 | 272 | methods.setup_firewall = { 273 | call: function() { 274 | try { 275 | uci.load('network'); 276 | uci.load('firewall'); 277 | 278 | let changed_network = false; 279 | let changed_firewall = false; 280 | 281 | // 1. config Network Interface 282 | let net_ts = uci.get('network', 'tailscale'); 283 | if (net_ts == null) { 284 | uci.set('network', 'tailscale', 'interface'); 285 | uci.set('network', 'tailscale', 'proto', 'none'); 286 | uci.set('network', 'tailscale', 'device', 'tailscale0'); 287 | changed_network = true; 288 | } else { 289 | let current_dev = uci.get('network', 'tailscale', 'device'); 290 | if (current_dev != 'tailscale0') { 291 | uci.set('network', 'tailscale', 'device', 'tailscale0'); 292 | changed_network = true; 293 | } 294 | } 295 | 296 | // 2. config Firewall Zone 297 | let fw_all = uci.get_all('firewall'); 298 | let ts_zone_section = null; 299 | let fwd_lan_to_ts = false; 300 | let fwd_ts_to_lan = false; 301 | 302 | for (let sec_key in fw_all) { 303 | let s = fw_all[sec_key]; 304 | if (s['.type'] == 'zone' && s['name'] == 'tailscale') { 305 | ts_zone_section = sec_key; 306 | } 307 | if (s['.type'] == 'forwarding') { 308 | if (s.src == 'lan' && s.dest == 'tailscale') fwd_lan_to_ts = true; 309 | if (s.src == 'tailscale' && s.dest == 'lan') fwd_ts_to_lan = true; 310 | } 311 | } 312 | 313 | if (ts_zone_section == null) { 314 | let zid = uci.add('firewall', 'zone'); 315 | uci.set('firewall', zid, 'name', 'tailscale'); 316 | uci.set('firewall', zid, 'input', 'ACCEPT'); 317 | uci.set('firewall', zid, 'output', 'ACCEPT'); 318 | uci.set('firewall', zid, 'forward', 'ACCEPT'); 319 | uci.set('firewall', zid, 'masq', '1'); 320 | uci.set('firewall', zid, 'mtu_fix', '1'); 321 | uci.set('firewall', zid, 'network', ['tailscale']); 322 | changed_firewall = true; 323 | } else { 324 | let nets = uci.get('firewall', ts_zone_section, 'network'); 325 | let net_list = []; 326 | let has_ts_net = false; 327 | 328 | if (type(nets) == 'array') { 329 | net_list = nets; 330 | } else if (type(nets) == 'string') { 331 | net_list = [nets]; 332 | } 333 | 334 | // check if 'tailscale' is already in the list 335 | for (let n in net_list) { 336 | if (net_list[n] == 'tailscale') { 337 | has_ts_net = true; 338 | break; 339 | } 340 | } 341 | 342 | if (!has_ts_net) { 343 | push(net_list, 'tailscale'); 344 | uci.set('firewall', ts_zone_section, 'network', net_list); 345 | changed_firewall = true; 346 | } 347 | } 348 | 349 | // 3. config Forwarding 350 | if (!fwd_lan_to_ts) { 351 | let fid = uci.add('firewall', 'forwarding'); 352 | uci.set('firewall', fid, 'src', 'lan'); 353 | uci.set('firewall', fid, 'dest', 'tailscale'); 354 | changed_firewall = true; 355 | } 356 | 357 | if (!fwd_ts_to_lan) { 358 | let fid = uci.add('firewall', 'forwarding'); 359 | uci.set('firewall', fid, 'src', 'tailscale'); 360 | uci.set('firewall', fid, 'dest', 'lan'); 361 | changed_firewall = true; 362 | } 363 | 364 | // 4. save 365 | if (changed_network) { 366 | uci.save('network'); 367 | uci.commit('network'); 368 | exec('/etc/init.d/network reload'); 369 | } 370 | 371 | if (changed_firewall) { 372 | uci.save('firewall'); 373 | uci.commit('firewall'); 374 | exec('/etc/init.d/firewall reload'); 375 | } 376 | 377 | return { 378 | success: true, 379 | changed_network: changed_network, 380 | changed_firewall: changed_firewall, 381 | message: (changed_network || changed_firewall) ? 'Tailscale firewall/interface configuration applied.' : 'Tailscale firewall/interface already configured.' 382 | }; 383 | 384 | } catch (e) { 385 | return { error: 'Exception in setup_firewall: ' + e + '\nStack: ' + (e.stacktrace || '') }; 386 | } 387 | } 388 | }; 389 | 390 | return { 'tailscale': methods }; 391 | -------------------------------------------------------------------------------- /luci-app-tailscale-community/po/templates/community.pot: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "Content-Type: text/plain; charset=UTF-8" 3 | 4 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:35 5 | msgid "(Experimental) Reduce Memory Usage" 6 | msgstr "" 7 | 8 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:20 9 | msgid "Accept Routes" 10 | msgstr "" 11 | 12 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:401 13 | msgid "Account Settings" 14 | msgstr "" 15 | 16 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:21 17 | msgid "Advertise Exit Node" 18 | msgstr "" 19 | 20 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:376 21 | msgid "Advertise Routes" 22 | msgstr "" 23 | 24 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:376 25 | msgid "" 26 | "Advertise subnet routes behind this device. Select from the detected subnets " 27 | "below or enter custom routes (comma-separated)." 28 | msgstr "" 29 | 30 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:23 31 | msgid "Allow LAN Access" 32 | msgstr "" 33 | 34 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:20 35 | msgid "Allow accepting routes announced by other nodes." 36 | msgstr "" 37 | 38 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:27 39 | msgid "Allow connecting to this device through the SSH function of Tailscale." 40 | msgstr "" 41 | 42 | #: applications/luci-app-tailscale-community/root/usr/share/rpcd/acl.d/luci-app-tailscale-community.json:3 43 | msgid "Allow user access to tailscale" 44 | msgstr "" 45 | 46 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:522 47 | msgid "Applying changes..." 48 | msgstr "" 49 | 50 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:475 51 | msgid "Are you sure you want to log out?" 52 | msgstr "" 53 | 54 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:384 55 | msgid "Auto Configure Firewall" 56 | msgstr "" 57 | 58 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:482 59 | msgid "Cancel" 60 | msgstr "" 61 | 62 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:426 63 | msgid "Click to Log out account on this device." 64 | msgstr "" 65 | 66 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:405 67 | msgid "Click to get a login URL for this device." 68 | msgstr "" 69 | 70 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:167 71 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:364 72 | msgid "Collecting data ..." 73 | msgstr "" 74 | 75 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:501 76 | msgid "Confirm Logout" 77 | msgstr "" 78 | 79 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:254 80 | msgid "Connection Info" 81 | msgstr "" 82 | 83 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:438 84 | msgid "" 85 | "Could not open a new tab. Please check if your browser or an extension " 86 | "blocked the pop-up." 87 | msgstr "" 88 | 89 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:410 90 | msgid "Custom Login Server" 91 | msgstr "" 92 | 93 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:418 94 | msgid "Custom Login Server Auth Key" 95 | msgstr "" 96 | 97 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:21 98 | msgid "Declare this device as an Exit Node." 99 | msgstr "" 100 | 101 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28 102 | msgid "Disable MagicDNS" 103 | msgstr "" 104 | 105 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:25 106 | msgid "Disable SNAT" 107 | msgstr "" 108 | 109 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:25 110 | msgid "" 111 | "Disable Source NAT (SNAT) for traffic to advertised routes. Most users " 112 | "should leave this unchecked." 113 | msgstr "" 114 | 115 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:227 116 | msgid "Disabled" 117 | msgstr "" 118 | 119 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:427 120 | msgid "Disconnect from Tailscale and expire current node key." 121 | msgstr "" 122 | 123 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:27 124 | msgid "Enable Tailscale SSH" 125 | msgstr "" 126 | 127 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:24 128 | msgid "Enable Web Interface" 129 | msgstr "" 130 | 131 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:227 132 | msgid "Enabled" 133 | msgstr "" 134 | 135 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:35 136 | msgid "" 137 | "Enabling this option can reduce memory usage, but it may sacrifice some " 138 | "performance (set GOGC=10)." 139 | msgstr "" 140 | 141 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:463 142 | msgid "Error" 143 | msgstr "" 144 | 145 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:537 146 | msgid "Error applying settings: %s" 147 | msgstr "" 148 | 149 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:148 150 | msgid "Error caching DERP region map: %s" 151 | msgstr "" 152 | 153 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:151 154 | msgid "Error fetching DERP region map: %s" 155 | msgstr "" 156 | 157 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:121 158 | msgid "Error reading cached DERP region map: %s" 159 | msgstr "" 160 | 161 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:533 162 | msgid "Error saving settings: %s" 163 | msgstr "" 164 | 165 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:22 166 | msgid "Exit Node" 167 | msgstr "" 168 | 169 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:385 170 | msgid "" 171 | "Experimental: applies minimal firewall and interface setup for Tailscale. It " 172 | "will create/patch network.tailscale (proto none, device tailscale0), add a " 173 | "firewall zone \"tailscale\" with ACCEPT/ACCEPT/ACCEPT, masq, mtu_fix, and " 174 | "ensure forwarding tailscale<->lan. It reloads network/firewall only if " 175 | "changes are made." 176 | msgstr "" 177 | 178 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:24 179 | msgid "" 180 | "Expose a web interface on port 5252 for managing this node over Tailscale." 181 | msgstr "" 182 | 183 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:394 184 | msgid "Failed to configure firewall: %s" 185 | msgstr "" 186 | 187 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:464 188 | msgid "Failed to get login URL. You may close this tab." 189 | msgstr "" 190 | 191 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:469 192 | msgid "Failed to get login URL: %s" 193 | msgstr "" 194 | 195 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:465 196 | msgid "Failed to get login URL: Invalid response from server." 197 | msgstr "" 198 | 199 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:543 200 | msgid "Failed to save settings: %s" 201 | msgstr "" 202 | 203 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:19 204 | msgid "Firewall Mode" 205 | msgstr "" 206 | 207 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:391 208 | msgid "Firewall configuration applied." 209 | msgstr "" 210 | 211 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:373 212 | msgid "General Settings" 213 | msgstr "" 214 | 215 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:251 216 | msgid "Hostname" 217 | msgstr "" 218 | 219 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:406 220 | msgid "" 221 | "If the timeout is displayed, you can refresh the page and click Login again." 222 | msgstr "" 223 | 224 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:420 225 | msgid "" 226 | "If you are using custom login server but not providing an Auth Key, will " 227 | "redirect to the login page without pre-filling the key." 228 | msgstr "" 229 | 230 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:91 231 | msgid "Invalid Date" 232 | msgstr "" 233 | 234 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:94 235 | msgid "Just now" 236 | msgstr "" 237 | 238 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:206 239 | msgid "LOGGED OUT" 240 | msgstr "" 241 | 242 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:257 243 | msgid "Last Seen" 244 | msgstr "" 245 | 246 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:412 247 | msgid "Leave blank for default Tailscale control plane." 248 | msgstr "" 249 | 250 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:488 251 | msgid "Logging out..." 252 | msgstr "" 253 | 254 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:404 255 | msgid "Login" 256 | msgstr "" 257 | 258 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:425 259 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:498 260 | msgid "Logout" 261 | msgstr "" 262 | 263 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:495 264 | msgid "Logout failed: %s" 265 | msgstr "" 266 | 267 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:88 268 | msgid "N/A" 269 | msgstr "" 270 | 271 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:217 272 | msgid "NOT RUNNING" 273 | msgstr "" 274 | 275 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:299 276 | msgid "Network Devices" 277 | msgstr "" 278 | 279 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:246 280 | msgid "No peer devices found." 281 | msgstr "" 282 | 283 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:89 284 | msgid "Now" 285 | msgstr "" 286 | 287 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:253 288 | msgid "OS" 289 | msgstr "" 290 | 291 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:279 292 | msgid "Offline" 293 | msgstr "" 294 | 295 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:279 296 | msgid "Online" 297 | msgstr "" 298 | 299 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:411 300 | msgid "" 301 | "Optional: Specify a custom control server URL (e.g., a Headscale instance, " 302 | "https://example.com)." 303 | msgstr "" 304 | 305 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:419 306 | msgid "" 307 | "Optional: Specify an authentication key for the custom control server. Leave " 308 | "blank if not required." 309 | msgstr "" 310 | 311 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:208 312 | msgid "Please use the login button in the settings below to authenticate." 313 | msgstr "" 314 | 315 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:448 316 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:454 317 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:488 318 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:522 319 | msgid "Please wait." 320 | msgstr "" 321 | 322 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:225 323 | msgid "RUNNING" 324 | msgstr "" 325 | 326 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:255 327 | msgid "RX" 328 | msgstr "" 329 | 330 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:448 331 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:454 332 | msgid "Requesting Login URL..." 333 | msgstr "" 334 | 335 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:445 336 | msgid "Requesting Tailscale login URL... Please wait." 337 | msgstr "" 338 | 339 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:19 340 | msgid "" 341 | "Select the firewall backend for Tailscale to use. Requires service restart " 342 | "to take effect." 343 | msgstr "" 344 | 345 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:196 346 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:204 347 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:216 348 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:225 349 | msgid "Service Status" 350 | msgstr "" 351 | 352 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:369 353 | msgid "Settings" 354 | msgstr "" 355 | 356 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:26 357 | msgid "Shields Up" 358 | msgstr "" 359 | 360 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:22 361 | msgid "Specify an exit node. Leave it blank and it will not be used." 362 | msgstr "" 363 | 364 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:250 365 | msgid "Status" 366 | msgstr "" 367 | 368 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:492 369 | msgid "Successfully logged out." 370 | msgstr "" 371 | 372 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:197 373 | msgid "TAILSCALE NOT FOUND" 374 | msgstr "" 375 | 376 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:227 377 | msgid "TUN Mode" 378 | msgstr "" 379 | 380 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:256 381 | msgid "TX" 382 | msgstr "" 383 | 384 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:230 385 | msgid "Tailnet Name" 386 | msgstr "" 387 | 388 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:343 389 | #: applications/luci-app-tailscale-community/root/usr/share/luci/menu.d/luci-app-tailscale-community.json:3 390 | msgid "Tailscale" 391 | msgstr "" 392 | 393 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:173 394 | msgid "Tailscale Health Check: %s" 395 | msgstr "" 396 | 397 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:252 398 | msgid "Tailscale IP" 399 | msgstr "" 400 | 401 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:228 402 | msgid "Tailscale IPv4" 403 | msgstr "" 404 | 405 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:229 406 | msgid "Tailscale IPv6" 407 | msgstr "" 408 | 409 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:444 410 | msgid "Tailscale Login" 411 | msgstr "" 412 | 413 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:343 414 | msgid "" 415 | "Tailscale is a mesh VPN solution that makes it easy to connect your devices " 416 | "securely. This configuration page allows you to manage Tailscale settings on " 417 | "your OpenWrt device." 418 | msgstr "" 419 | 420 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:528 421 | msgid "Tailscale settings applied successfully." 422 | msgstr "" 423 | 424 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:446 425 | msgid "This can take up to 30 seconds." 426 | msgstr "" 427 | 428 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:476 429 | msgid "" 430 | "This will disconnect this device from your Tailnet and require you to re-" 431 | "authenticate." 432 | msgstr "" 433 | 434 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:121 435 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:148 436 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:151 437 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:469 438 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:495 439 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:533 440 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:537 441 | msgid "Unknown error" 442 | msgstr "" 443 | 444 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28 445 | msgid "Use system DNS instead of MagicDNS." 446 | msgstr "" 447 | 448 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:226 449 | msgid "Version" 450 | msgstr "" 451 | 452 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:26 453 | msgid "" 454 | "When enabled, blocks all inbound connections from the Tailscale network." 455 | msgstr "" 456 | 457 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:23 458 | msgid "When using the exit node, access to the local LAN is allowed." 459 | msgstr "" 460 | 461 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:97 462 | msgid "ago" 463 | msgstr "" 464 | -------------------------------------------------------------------------------- /luci-app-tailscale-community/po/zh_Hans/community.po: -------------------------------------------------------------------------------- 1 | msgid "" 2 | msgstr "Content-Type: text/plain; charset=UTF-8\n" 3 | 4 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:35 5 | msgid "(Experimental) Reduce Memory Usage" 6 | msgstr "(实验性) 减少内存使用" 7 | 8 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:20 9 | msgid "Accept Routes" 10 | msgstr "接受路由" 11 | 12 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:401 13 | msgid "Account Settings" 14 | msgstr "账户设置" 15 | 16 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:21 17 | msgid "Advertise Exit Node" 18 | msgstr "通告出口节点" 19 | 20 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:376 21 | msgid "Advertise Routes" 22 | msgstr "通告路由" 23 | 24 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:376 25 | msgid "" 26 | "Advertise subnet routes behind this device. Select from the detected subnets " 27 | "below or enter custom routes (comma-separated)." 28 | msgstr "" 29 | "通告此设备后的子网路由。从下面的子网中选择,或输入自定义路由 (逗号分隔)。" 30 | 31 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:23 32 | msgid "Allow LAN Access" 33 | msgstr "允许局域网访问" 34 | 35 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:20 36 | msgid "Allow accepting routes announced by other nodes." 37 | msgstr "允许接受由其他节点通告的路由。" 38 | 39 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:27 40 | msgid "Allow connecting to this device through the SSH function of Tailscale." 41 | msgstr "允许通过 Tailscale 的 SSH 功能连接到此设备。" 42 | 43 | #: applications/luci-app-tailscale-community/root/usr/share/rpcd/acl.d/luci-app-tailscale-community.json:3 44 | msgid "Allow user access to tailscale" 45 | msgstr "允许用户访问 Tailscale" 46 | 47 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:522 48 | msgid "Applying changes..." 49 | msgstr "正在应用更改..." 50 | 51 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:475 52 | msgid "Are you sure you want to log out?" 53 | msgstr "您确定要登出吗?" 54 | 55 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:384 56 | msgid "Auto Configure Firewall" 57 | msgstr "自动配置防火墙" 58 | 59 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:482 60 | msgid "Cancel" 61 | msgstr "取消" 62 | 63 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:426 64 | msgid "Click to Log out account on this device." 65 | msgstr "点击以登出此设备上的账户。" 66 | 67 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:405 68 | msgid "Click to get a login URL for this device." 69 | msgstr "点击获取此设备的登录 URL。" 70 | 71 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:167 72 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:364 73 | msgid "Collecting data ..." 74 | msgstr "正在收集数据..." 75 | 76 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:501 77 | msgid "Confirm Logout" 78 | msgstr "确认登出" 79 | 80 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:254 81 | msgid "Connection Info" 82 | msgstr "连接信息" 83 | 84 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:438 85 | msgid "" 86 | "Could not open a new tab. Please check if your browser or an extension " 87 | "blocked the pop-up." 88 | msgstr "无法打开新标签页。请检查您的浏览器或扩展程序是否阻止了弹出窗口。" 89 | 90 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:410 91 | msgid "Custom Login Server" 92 | msgstr "自定义登录服务器" 93 | 94 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:418 95 | msgid "Custom Login Server Auth Key" 96 | msgstr "自定义登录服务器认证密钥" 97 | 98 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:21 99 | msgid "Declare this device as an Exit Node." 100 | msgstr "将此设备声明为出口节点。" 101 | 102 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28 103 | msgid "Disable MagicDNS" 104 | msgstr "禁用 MagicDNS" 105 | 106 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:25 107 | msgid "Disable SNAT" 108 | msgstr "禁用 SNAT" 109 | 110 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:25 111 | msgid "" 112 | "Disable Source NAT (SNAT) for traffic to advertised routes. Most users " 113 | "should leave this unchecked." 114 | msgstr "为通告路由的流量禁用源地址转换 (SNAT)。大多数用户应保持此项不勾选。" 115 | 116 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:227 117 | msgid "Disabled" 118 | msgstr "已禁用" 119 | 120 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:427 121 | msgid "Disconnect from Tailscale and expire current node key." 122 | msgstr "从 Tailscale 断开连接并使当前节点密钥过期。" 123 | 124 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:27 125 | msgid "Enable Tailscale SSH" 126 | msgstr "启用 Tailscale SSH" 127 | 128 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:24 129 | msgid "Enable Web Interface" 130 | msgstr "启用 Web 界面" 131 | 132 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:227 133 | msgid "Enabled" 134 | msgstr "已启用" 135 | 136 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:35 137 | msgid "" 138 | "Enabling this option can reduce memory usage, but it may sacrifice some " 139 | "performance (set GOGC=10)." 140 | msgstr "启用此选项可以减少内存使用,但可能会牺牲一些性能 (设置 GOGC=10)。" 141 | 142 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:463 143 | msgid "Error" 144 | msgstr "错误" 145 | 146 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:537 147 | msgid "Error applying settings: %s" 148 | msgstr "应用设置时出错: %s" 149 | 150 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:148 151 | msgid "Error caching DERP region map: %s" 152 | msgstr "缓存 DERP 区域地图时出错: %s" 153 | 154 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:151 155 | msgid "Error fetching DERP region map: %s" 156 | msgstr "获取 DERP 区域地图时出错: %s" 157 | 158 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:121 159 | msgid "Error reading cached DERP region map: %s" 160 | msgstr "读取缓存的 DERP 区域地图时出错: %s" 161 | 162 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:533 163 | msgid "Error saving settings: %s" 164 | msgstr "保存设置时出错: %s" 165 | 166 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:22 167 | msgid "Exit Node" 168 | msgstr "出口节点" 169 | 170 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:385 171 | msgid "" 172 | "Experimental: applies minimal firewall and interface setup for Tailscale. It " 173 | "will create/patch network.tailscale (proto none, device tailscale0), add a " 174 | "firewall zone \"tailscale\" with ACCEPT/ACCEPT/ACCEPT, masq, mtu_fix, and " 175 | "ensure forwarding tailscale<->lan. It reloads network/firewall only if " 176 | "changes are made." 177 | msgstr "实验性功能:为Tailscale应用所必须最小的防火墙设置。它将创建/修补" 178 | "network.tailscale (proto none,device tailscale0),添加" 179 | "ACCEPT/ACCEPT/ACCEPT、masq、mtu_fix的防火墙区域“tailscale”,并转发tailscale<->lan。" 180 | "反正总之如果你不知道这个是干什么的,而且你tailscale网络又有问题,说明你需要点这个。" 181 | 182 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:24 183 | msgid "" 184 | "Expose a web interface on port 5252 for managing this node over Tailscale." 185 | msgstr "在端口 5252 上暴露一个 Web 界面,用于通过 Tailscale 管理此节点。" 186 | 187 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:394 188 | msgid "Failed to configure firewall: %s" 189 | msgstr "获取防火墙设置失败: %s" 190 | 191 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:464 192 | msgid "Failed to get login URL. You may close this tab." 193 | msgstr "获取登录 URL 失败。您可以关闭此标签页。" 194 | 195 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:469 196 | msgid "Failed to get login URL: %s" 197 | msgstr "获取登录 URL 失败: %s" 198 | 199 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:465 200 | msgid "Failed to get login URL: Invalid response from server." 201 | msgstr "获取登录 URL 失败: 服务器响应无效。" 202 | 203 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:543 204 | msgid "Failed to save settings: %s" 205 | msgstr "保存设置失败: %s" 206 | 207 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:19 208 | msgid "Firewall Mode" 209 | msgstr "防火墙模式" 210 | 211 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:391 212 | msgid "Firewall configuration applied." 213 | msgstr "已应用防火墙配置" 214 | 215 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:373 216 | msgid "General Settings" 217 | msgstr "常规设置" 218 | 219 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:251 220 | msgid "Hostname" 221 | msgstr "主机名" 222 | 223 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:406 224 | msgid "" 225 | "If the timeout is displayed, you can refresh the page and click Login again." 226 | msgstr "如果显示超时,您可以刷新页面并再次点击登录。" 227 | 228 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:420 229 | msgid "" 230 | "If you are using custom login server but not providing an Auth Key, will " 231 | "redirect to the login page without pre-filling the key." 232 | msgstr "" 233 | "如果您使用自定义登录服务器但未提供认证密钥,将重定向到登录页面而不会预先填充" 234 | "密钥。" 235 | 236 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:91 237 | msgid "Invalid Date" 238 | msgstr "无效日期" 239 | 240 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:94 241 | msgid "Just now" 242 | msgstr "刚才" 243 | 244 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:206 245 | msgid "LOGGED OUT" 246 | msgstr "已登出" 247 | 248 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:257 249 | msgid "Last Seen" 250 | msgstr "上次在线" 251 | 252 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:412 253 | msgid "Leave blank for default Tailscale control plane." 254 | msgstr "留空以使用默认的 Tailscale 控制平面。" 255 | 256 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:488 257 | msgid "Logging out..." 258 | msgstr "正在登出..." 259 | 260 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:404 261 | msgid "Login" 262 | msgstr "登录" 263 | 264 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:425 265 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:498 266 | msgid "Logout" 267 | msgstr "登出" 268 | 269 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:495 270 | msgid "Logout failed: %s" 271 | msgstr "登出失败: %s" 272 | 273 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:88 274 | msgid "N/A" 275 | msgstr "N/A" 276 | 277 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:217 278 | msgid "NOT RUNNING" 279 | msgstr "未运行" 280 | 281 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:299 282 | msgid "Network Devices" 283 | msgstr "网络设备" 284 | 285 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:246 286 | msgid "No peer devices found." 287 | msgstr "未找到对等设备。" 288 | 289 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:89 290 | msgid "Now" 291 | msgstr "现在" 292 | 293 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:253 294 | msgid "OS" 295 | msgstr "操作系统" 296 | 297 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:279 298 | msgid "Offline" 299 | msgstr "离线" 300 | 301 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:279 302 | msgid "Online" 303 | msgstr "在线" 304 | 305 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:411 306 | msgid "" 307 | "Optional: Specify a custom control server URL (e.g., a Headscale instance, " 308 | "https://example.com)." 309 | msgstr "" 310 | "可选:指定一个自定义控制服务器 URL (例如,一个 Headscale 实例,https://" 311 | "example.com)。" 312 | 313 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:419 314 | msgid "" 315 | "Optional: Specify an authentication key for the custom control server. Leave " 316 | "blank if not required." 317 | msgstr "可选:为自定义控制服务器指定一个认证密钥。如果不需要,请留空。" 318 | 319 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:208 320 | msgid "Please use the login button in the settings below to authenticate." 321 | msgstr "请使用下方设置中的登录按钮进行认证。" 322 | 323 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:448 324 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:454 325 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:488 326 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:522 327 | msgid "Please wait." 328 | msgstr "请稍候。" 329 | 330 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:225 331 | msgid "RUNNING" 332 | msgstr "正在运行" 333 | 334 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:255 335 | msgid "RX" 336 | msgstr "接收" 337 | 338 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:448 339 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:454 340 | msgid "Requesting Login URL..." 341 | msgstr "正在请求登录 URL..." 342 | 343 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:445 344 | msgid "Requesting Tailscale login URL... Please wait." 345 | msgstr "正在请求 Tailscale 登录 URL... 请稍候。" 346 | 347 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:19 348 | msgid "" 349 | "Select the firewall backend for Tailscale to use. Requires service restart " 350 | "to take effect." 351 | msgstr "选择 Tailscale 使用的防火墙后端。需要重启服务才能生效。" 352 | 353 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:196 354 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:204 355 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:216 356 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:225 357 | msgid "Service Status" 358 | msgstr "服务状态" 359 | 360 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:369 361 | msgid "Settings" 362 | msgstr "设置" 363 | 364 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:26 365 | msgid "Shields Up" 366 | msgstr "开启防护 (Shields Up)" 367 | 368 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:22 369 | msgid "Specify an exit node. Leave it blank and it will not be used." 370 | msgstr "指定一个出口节点。留空则不使用。" 371 | 372 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:250 373 | msgid "Status" 374 | msgstr "状态" 375 | 376 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:492 377 | msgid "Successfully logged out." 378 | msgstr "登出成功。" 379 | 380 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:197 381 | msgid "TAILSCALE NOT FOUND" 382 | msgstr "未找到 TAILSCALE" 383 | 384 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:227 385 | msgid "TUN Mode" 386 | msgstr "TUN 模式" 387 | 388 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:256 389 | msgid "TX" 390 | msgstr "发送" 391 | 392 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:230 393 | msgid "Tailnet Name" 394 | msgstr "Tailnet 名称" 395 | 396 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:343 397 | #: applications/luci-app-tailscale-community/root/usr/share/luci/menu.d/luci-app-tailscale-community.json:3 398 | msgid "Tailscale" 399 | msgstr "Tailscale" 400 | 401 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:173 402 | msgid "Tailscale Health Check: %s" 403 | msgstr "Tailscale 健康检查: %s" 404 | 405 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:252 406 | msgid "Tailscale IP" 407 | msgstr "Tailscale IP" 408 | 409 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:228 410 | msgid "Tailscale IPv4" 411 | msgstr "Tailscale IPv4" 412 | 413 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:229 414 | msgid "Tailscale IPv6" 415 | msgstr "Tailscale IPv6" 416 | 417 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:444 418 | msgid "Tailscale Login" 419 | msgstr "Tailscale 登录" 420 | 421 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:343 422 | msgid "" 423 | "Tailscale is a mesh VPN solution that makes it easy to connect your devices " 424 | "securely. This configuration page allows you to manage Tailscale settings on " 425 | "your OpenWrt device." 426 | msgstr "" 427 | "Tailscale 是一个网状 VPN 解决方案,可以轻松地安全连接您的设备。此配置页面允许" 428 | "您在 OpenWrt 设备上管理 Tailscale 设置。" 429 | 430 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:528 431 | msgid "Tailscale settings applied successfully." 432 | msgstr "Tailscale 设置已成功应用。" 433 | 434 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:446 435 | msgid "This can take up to 30 seconds." 436 | msgstr "此过程最多可能需要 30 秒。" 437 | 438 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:476 439 | msgid "" 440 | "This will disconnect this device from your Tailnet and require you to re-" 441 | "authenticate." 442 | msgstr "这将使此设备从您的 Tailnet 断开连接,并需要您重新进行身份验证。" 443 | 444 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:121 445 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:148 446 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:151 447 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:469 448 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:495 449 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:533 450 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:537 451 | msgid "Unknown error" 452 | msgstr "未知错误" 453 | 454 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:28 455 | msgid "Use system DNS instead of MagicDNS." 456 | msgstr "" 457 | 458 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:226 459 | msgid "Version" 460 | msgstr "版本" 461 | 462 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:26 463 | msgid "" 464 | "When enabled, blocks all inbound connections from the Tailscale network." 465 | msgstr "启用后,将阻止来自 Tailscale 网络的所有入站连接。" 466 | 467 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:23 468 | msgid "When using the exit node, access to the local LAN is allowed." 469 | msgstr "使用出口节点时,允许访问本地局域网。" 470 | 471 | #: applications/luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js:97 472 | msgid "ago" 473 | msgstr "前" 474 | -------------------------------------------------------------------------------- /luci-app-tailscale-community/htdocs/luci-static/resources/view/tailscale.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 'require view'; 3 | 'require form'; 4 | 'require rpc'; 5 | 'require ui'; 6 | 'require uci'; 7 | 'require tools.widgets as widgets'; 8 | 9 | const callGetStatus = rpc.declare({ object: 'tailscale', method: 'get_status' }); 10 | const callGetSettings = rpc.declare({ object: 'tailscale', method: 'get_settings' }); 11 | const callSetSettings = rpc.declare({ object: 'tailscale', method: 'set_settings', params: ['form_data'] }); 12 | const callDoLogin = rpc.declare({ object: 'tailscale', method: 'do_login', params: ['form_data'] }); 13 | const callDoLogout = rpc.declare({ object: 'tailscale', method: 'do_logout' }); 14 | const callGetSubroutes = rpc.declare({ object: 'tailscale', method: 'get_subroutes' }); 15 | const callSetupFirewall = rpc.declare({ object: 'tailscale', method: 'setup_firewall' }); 16 | let map; 17 | 18 | const tailscaleSettingsConf = [ 19 | [form.ListValue, 'fw_mode', _('Firewall Mode'), _('Select the firewall backend for Tailscale to use. Requires service restart to take effect.'), {values: ['nftables','iptables'],rmempty: false}], 20 | [form.Flag, 'accept_routes', _('Accept Routes'), _('Allow accepting routes announced by other nodes.'), { rmempty: false }], 21 | [form.Flag, 'advertise_exit_node', _('Advertise Exit Node'), _('Declare this device as an Exit Node.'), { rmempty: false }], 22 | [form.Value, 'exit_node', _('Exit Node'), _('Specify an exit node. Leave it blank and it will not be used.'), { rmempty: true }], 23 | [form.Flag, 'exit_node_allow_lan_access', _('Allow LAN Access'), _('When using the exit node, access to the local LAN is allowed.'), { rmempty: false }], 24 | [form.Flag, 'runwebclient', _('Enable Web Interface'), _('Expose a web interface on port 5252 for managing this node over Tailscale.'), { rmempty: false }], 25 | [form.Flag, 'nosnat', _('Disable SNAT'), _('Disable Source NAT (SNAT) for traffic to advertised routes. Most users should leave this unchecked.'), { rmempty: false }], 26 | [form.Flag, 'shields_up', _('Shields Up'), _('When enabled, blocks all inbound connections from the Tailscale network.'), { rmempty: false }], 27 | [form.Flag, 'ssh', _('Enable Tailscale SSH'), _('Allow connecting to this device through the SSH function of Tailscale.'), { rmempty: false }], 28 | [form.Flag, 'disable_magic_dns', _('Disable MagicDNS'), _('Use system DNS instead of MagicDNS.'), { rmempty: false }] 29 | ]; 30 | 31 | const accountConf = []; // dynamic created in render function 32 | 33 | const daemonConf = [ 34 | //[form.Value, 'daemon_mtu', _('Daemon MTU'), _('Set a custom MTU for the Tailscale daemon. Leave blank to use the default value.'), { datatype: 'uinteger', placeholder: '1280' }, { rmempty: false }], 35 | [form.Flag, 'daemon_reduce_memory', _('(Experimental) Reduce Memory Usage'), _('Enabling this option can reduce memory usage, but it may sacrifice some performance (set GOGC=10).'), { rmempty: false }] 36 | ]; 37 | 38 | const derpMapUrl = 'https://controlplane.tailscale.com/derpmap/default'; 39 | let regionCodeMap = {}; 40 | 41 | // this function copy from luci-app-frpc. thx 42 | function setParams(o, params) { 43 | if (!params) return; 44 | 45 | for (const [key, val] of Object.entries(params)) { 46 | if (key === 'values') { 47 | [].concat(val).forEach(v => 48 | o.value.apply(o, Array.isArray(v) ? v : [v]) 49 | ); 50 | } else if (key === 'depends') { 51 | const arr = Array.isArray(val) ? val : [val]; 52 | o.deps = arr.map(dep => Object.assign({}, ...o.deps, dep)); 53 | } else { 54 | o[key] = val; 55 | } 56 | } 57 | 58 | if (params.datatype === 'bool') 59 | Object.assign(o, { enabled: 'true', disabled: 'false' }); 60 | } 61 | 62 | // this function copy from luci-app-frpc. thx 63 | function defTabOpts(s, t, opts, params) { 64 | for (let i = 0; i < opts.length; i++) { 65 | const opt = opts[i]; 66 | const o = s.taboption(t, opt[0], opt[1], opt[2], opt[3]); 67 | setParams(o, opt[4]); 68 | setParams(o, params); 69 | } 70 | } 71 | 72 | function getRunningStatus() { 73 | return L.resolveDefault(callGetStatus(), { running: false }).then(function (res) { 74 | return res; 75 | }); 76 | } 77 | 78 | function formatBytes(bytes) { 79 | const bytes_num = parseInt(bytes, 10); 80 | if (isNaN(bytes_num) || bytes_num === 0) return '-'; 81 | const k = 1000; 82 | const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; 83 | const i = Math.floor(Math.log(bytes_num) / Math.log(k)); 84 | return parseFloat((bytes_num / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; 85 | } 86 | 87 | function formatLastSeen(d) { 88 | if (!d) return _('N/A'); 89 | if (d === '0001-01-01T00:00:00Z') return _('Now'); 90 | const t = new Date(d); 91 | if (isNaN(t)) return _('Invalid Date'); 92 | const diff = (Date.now() - t) / 1000; 93 | if (diff < 0) return t.toLocaleString(); 94 | if (diff < 60) return _('Just now'); 95 | 96 | const mins = diff / 60, hrs = mins / 60, days = hrs / 24; 97 | const fmt = (n, s, p) => `${Math.floor(n)} ${Math.floor(n) === 1 ? _(s) : _(p)} ${_('ago')}`; 98 | 99 | if (mins < 60) return fmt(mins, 'minute', 'minutes'); 100 | if (hrs < 24) return fmt(hrs, 'hour', 'hours'); 101 | if (days < 30) return fmt(days, 'day', 'days'); 102 | 103 | return t.toISOString().slice(0, 10); 104 | } 105 | 106 | async function initializeRegionMap() { 107 | const cacheKey = 'tailscale_derp_map_cache'; 108 | const ttl = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds 109 | 110 | try { 111 | const cachedItem = localStorage.getItem(cacheKey); 112 | if (cachedItem) { 113 | const cached = JSON.parse(cachedItem); 114 | // Check if the cached data is still valid (not expired) 115 | if (Date.now() - cached.timestamp < ttl) { 116 | regionCodeMap = cached.data; 117 | return; 118 | } 119 | } 120 | } catch (e) { 121 | ui.addTimeLimitedNotification(null, [ E('p', _('Error reading cached DERP region map: %s').format(e.message || _('Unknown error'))) ], 7000, 'error'); 122 | } 123 | 124 | // If no valid cache, fetch from the network 125 | try { 126 | const response = await fetch(derpMapUrl); 127 | if (!response.ok) { 128 | return; 129 | } 130 | const data = await response.json(); 131 | const newRegionMap = {}; 132 | for (const regionId in data.Regions) { 133 | const region = data.Regions[regionId]; 134 | const code = (region.RegionCode || '').toLowerCase(); 135 | const name = region.RegionName || region.RegionCode || `Region ${regionId}`; 136 | newRegionMap[code] = name; 137 | } 138 | regionCodeMap = newRegionMap; 139 | 140 | // Save the newly fetched data to the cache 141 | try { 142 | const itemToCache = { 143 | timestamp: Date.now(), 144 | data: regionCodeMap 145 | }; 146 | localStorage.setItem(cacheKey, JSON.stringify(itemToCache)); 147 | } catch (e) { 148 | ui.addTimeLimitedNotification(null, [ E('p', _('Error caching DERP region map: %s').format(e.message || _('Unknown error'))) ], 7000, 'error'); 149 | } 150 | } catch (error) { 151 | ui.addTimeLimitedNotification(null, [ E('p', _('Error fetching DERP region map: %s').format(error.message || _('Unknown error'))) ], 7000, 'error'); 152 | } 153 | } 154 | 155 | function formatConnectionInfo(info) { 156 | if (!info) { return '-'; } 157 | if (typeof info === 'string' && info.length === 3) { 158 | const lowerCaseInfo = info.toLowerCase(); 159 | return regionCodeMap[lowerCaseInfo] || info; 160 | } 161 | return info; 162 | } 163 | 164 | function renderStatus(status) { 165 | // If status object is not yet available, show a loading message. 166 | if (!status || !status.hasOwnProperty('status')) { 167 | return E('em', {}, _('Collecting data ...')); 168 | } 169 | 170 | const notificationId = 'tailscale_health_notification'; 171 | let notificationElement = document.getElementById(notificationId); 172 | if (status.health != '') { 173 | const message = _('Tailscale Health Check: %s').format(status.health); 174 | if (notificationElement) { 175 | notificationElement.textContent = message; 176 | } 177 | else { 178 | let newNotificationContent = E('p', { 'id': notificationId }, message); 179 | ui.addNotification(null, newNotificationContent, 'info'); 180 | } 181 | }else{ 182 | try{ 183 | notificationElement.parentNode.parentNode.remove(); 184 | }catch(e){} 185 | } 186 | 187 | if (Object.keys(regionCodeMap).length === 0) { 188 | initializeRegionMap(); 189 | } 190 | 191 | // --- Part 1: Handle non-running states --- 192 | 193 | // State: Tailscale binary not found. 194 | if (status.status == 'not_installed') { 195 | return E('dl', { 'class': 'cbi-value' }, [ 196 | E('dt', {}, _('Service Status')), 197 | E('dd', {}, E('span', { 'style': 'color:red;' }, E('strong', {}, _('TAILSCALE NOT FOUND')))) 198 | ]); 199 | } 200 | 201 | // State: Logged out, requires user action. 202 | if (status.status == 'logout') { 203 | return E('dl', { 'class': 'cbi-value' }, [ 204 | E('dt', {}, _('Service Status')), 205 | E('dd', {}, [ 206 | E('span', { 'style': 'color:orange;' }, E('strong', {}, _('LOGGED OUT'))), 207 | E('br'), 208 | E('span', {}, _('Please use the login button in the settings below to authenticate.')) 209 | ]) 210 | ]); 211 | } 212 | 213 | // State: Service is installed but not running. 214 | if (status.status != 'running') { 215 | return E('dl', { 'class': 'cbi-value' }, [ 216 | E('dt', {}, _('Service Status')), 217 | E('dd', {}, E('span', { 'style': 'color:red;' }, E('strong', {}, _('NOT RUNNING')))) 218 | ]); 219 | } 220 | 221 | // --- Part 2: Render the full status display for a running service --- 222 | 223 | // A helper array to define the data for the main status table. 224 | const statusData = [ 225 | { label: _('Service Status'), value: E('span', { 'style': 'color:green;' }, E('strong', {}, _('RUNNING'))) }, 226 | { label: _('Version'), value: status.version || 'N/A' }, 227 | { label: _('TUN Mode'), value: status.TUNMode ? _('Enabled') : _('Disabled') }, 228 | { label: _('Tailscale IPv4'), value: status.ipv4 || 'N/A' }, 229 | { label: _('Tailscale IPv6'), value: status.ipv6 || 'N/A' }, 230 | { label: _('Tailnet Name'), value: status.domain_name || 'N/A' } 231 | ]; 232 | 233 | // Build the horizontal status table using the data array. 234 | const statusTable = E('table', { 'style': 'width: 100%; border-spacing: 0 5px;' }, [ 235 | E('tr', {}, statusData.map(item => E('td', { 'style': 'padding-right: 20px;' }, E('strong', {}, item.label)))), 236 | E('tr', {}, statusData.map(item => E('td', { 'style': 'padding-right: 20px;' }, item.value))) 237 | ]); 238 | 239 | // --- Part 3: Render the Peers/Network Devices table --- 240 | 241 | const peers = status.peers; 242 | let peersContent; 243 | 244 | if (!peers || Object.keys(peers).length === 0) { 245 | // Display a message if no peers are found. 246 | peersContent = E('p', {}, _('No peer devices found.')); 247 | } else { 248 | // Define headers for the peers table. 249 | const peerTableHeaders = [ 250 | { text: _('Status'), style: 'width: 80px;' }, 251 | { text: _('Hostname') }, 252 | { text: _('Tailscale IP') }, 253 | { text: _('OS') }, 254 | { text: _('Connection Info') }, 255 | { text: _('RX') }, 256 | { text: _('TX') }, 257 | { text: _('Last Seen') } 258 | ]; 259 | 260 | // Build the peers table. 261 | peersContent = E('table', { 'class': 'cbi-table' }, [ 262 | // Table Header Row 263 | E('tr', { 'class': 'cbi-table-header' }, peerTableHeaders.map(header => { 264 | let th_style = 'padding-right: 20px; text-align: left;'; 265 | if (header.style) { 266 | th_style += header.style; 267 | } 268 | return E('th', { 'class': 'cbi-table-cell', 'style': th_style }, header.text); 269 | })), 270 | 271 | // Table Body Rows (one for each peer) 272 | ...Object.entries(peers).map(([peerid, peer]) => { 273 | const td_style = 'padding-right: 20px;'; 274 | 275 | return E('tr', { 'class': 'cbi-rowstyle-1' }, [ 276 | E('td', { 'class': 'cbi-value-field', 'style': td_style }, 277 | E('span', { 278 | 'style': `color:${peer.online ? 'green' : 'gray'};`, 279 | 'title': peer.online ? _('Online') : _('Offline') 280 | }, peer.online ? '●' : '○') 281 | ), 282 | E('td', { 'class': 'cbi-value-field', 'style': td_style }, E('strong', {}, peer.hostname)), 283 | E('td', { 'class': 'cbi-value-field', 'style': td_style }, peer.ip || 'N/A'), 284 | E('td', { 'class': 'cbi-value-field', 'style': td_style }, peer.ostype || 'N/A'), 285 | E('td', { 'class': 'cbi-value-field', 'style': td_style }, formatConnectionInfo(peer.linkadress || '-')), 286 | E('td', { 'class': 'cbi-value-field', 'style': td_style }, formatBytes(peer.rx)), 287 | E('td', { 'class': 'cbi-value-field', 'style': td_style }, formatBytes(peer.tx)), 288 | E('td', { 'class': 'cbi-value-field', 'style': td_style }, formatLastSeen(peer.lastseen)) 289 | ]); 290 | }) 291 | ]); 292 | } 293 | 294 | // Combine all parts into a single DocumentFragment. 295 | // Using E() without a tag name creates a fragment, which is perfect for grouping elements. 296 | return E([ 297 | statusTable, 298 | E('div', { 'style': 'margin-top: 25px;' }, [ 299 | E('h4', {}, _('Network Devices')), 300 | peersContent 301 | ]) 302 | ]); 303 | } 304 | 305 | return view.extend({ 306 | load() { 307 | return Promise.all([ 308 | L.resolveDefault(callGetStatus(), { running: '', peers: [] }), 309 | L.resolveDefault(callGetSettings(), { accept_routes: false }), 310 | L.resolveDefault(callGetSubroutes(), { routes: [] }) 311 | ]) 312 | .then(function([status, settings_from_rpc, subroutes]) { 313 | return uci.load('tailscale').then(function() { 314 | if (uci.get('tailscale', 'settings') === null) { 315 | // No existing settings found; initialize UCI with RPC settings 316 | uci.add('tailscale', 'settings', 'settings'); 317 | uci.set('tailscale', 'settings', 'fw_mode', 'nftables'); 318 | uci.set('tailscale', 'settings', 'accept_routes', (settings_from_rpc.accept_routes ? '1' : '0')); 319 | uci.set('tailscale', 'settings', 'advertise_exit_node', ((settings_from_rpc.advertise_exit_node || false) ? '1' : '0')); 320 | uci.set('tailscale', 'settings', 'advertise_routes', (settings_from_rpc.advertise_routes || []).join(', ')); 321 | uci.set('tailscale', 'settings', 'exit_node', settings_from_rpc.exit_node || ''); 322 | uci.set('tailscale', 'settings', 'exit_node_allow_lan_access', ((settings_from_rpc.exit_node_allow_lan_access || false) ? '1' : '0')); 323 | uci.set('tailscale', 'settings', 'ssh', ((settings_from_rpc.ssh || false) ? '1' : '0')); 324 | uci.set('tailscale', 'settings', 'shields_up', ((settings_from_rpc.shields_up || false) ? '1' : '0')); 325 | uci.set('tailscale', 'settings', 'runwebclient', ((settings_from_rpc.runwebclient || false) ? '1' : '0')); 326 | uci.set('tailscale', 'settings', 'nosnat', ((settings_from_rpc.nosnat || false) ? '1' : '0')); 327 | uci.set('tailscale', 'settings', 'disable_magic_dns', ((settings_from_rpc.disable_magic_dns || false) ? '1' : '0')); 328 | 329 | uci.set('tailscale', 'settings', 'daemon_reduce_memory', '0'); 330 | uci.set('tailscale', 'settings', 'daemon_mtu', ''); 331 | return uci.save(); 332 | } 333 | }).then(function() { 334 | return [status, settings_from_rpc, subroutes]; 335 | }); 336 | }); 337 | }, 338 | 339 | render ([status = {}, settings = {}, subroutes_obj]) { 340 | const subroutes = (subroutes_obj && subroutes_obj.routes) ? subroutes_obj.routes : []; 341 | 342 | let s; 343 | map = new form.Map('tailscale', _('Tailscale'), _('Tailscale is a mesh VPN solution that makes it easy to connect your devices securely. This configuration page allows you to manage Tailscale settings on your OpenWrt device.')); 344 | 345 | s = map.section(form.NamedSection, '_status'); 346 | s.anonymous = true; 347 | s.render = function (section_id) { 348 | L.Poll.add( 349 | function () { 350 | return getRunningStatus().then(function (res) { 351 | const view = document.getElementById("service_status_display"); 352 | if (view) { 353 | const content = renderStatus(res); 354 | view.replaceChildren(content); 355 | } 356 | 357 | // login button only available when logged out 358 | const login_btn=document.getElementsByClassName('cbi-button cbi-button-apply')[0]; 359 | if(login_btn) { login_btn.disabled=(res.status != 'logout'); } 360 | }); 361 | }, 10); 362 | 363 | return E('div', { 'id': 'service_status_display', 'class': 'cbi-value' }, 364 | _('Collecting data ...') 365 | ); 366 | } 367 | 368 | // Bind settings to the 'settings' section of uci 369 | s = map.section(form.NamedSection, 'settings', 'settings', _('Settings')); 370 | s.dynamic = true; 371 | 372 | // Create the "General Settings" tab and apply tailscaleSettingsConf 373 | s.tab('general', _('General Settings')); 374 | 375 | defTabOpts(s, 'general', tailscaleSettingsConf, { optional: false }); 376 | const o = s.taboption('general', form.DynamicList, 'advertise_routes', _('Advertise Routes'),_('Advertise subnet routes behind this device. Select from the detected subnets below or enter custom routes (comma-separated).')); 377 | if (subroutes.length > 0) { 378 | subroutes.forEach(function(subnet) { 379 | o.value(subnet, subnet); 380 | }); 381 | } 382 | o.rmempty = true; 383 | 384 | const fwBtn = s.taboption('general', form.Button, '_setup_firewall', _('Auto Configure Firewall')); 385 | fwBtn.description = _('Experimental: applies minimal firewall and interface setup for Tailscale. It will create/patch network.tailscale (proto none, device tailscale0), add a firewall zone "tailscale" with ACCEPT/ACCEPT/ACCEPT, masq, mtu_fix, and ensure forwarding tailscale<->lan. It reloads network/firewall only if changes are made.'); 386 | fwBtn.inputstyle = 'action'; 387 | fwBtn.onclick = function() { 388 | const btn = this; 389 | btn.disabled = true; 390 | return callSetupFirewall().then(function(res) { 391 | const msg = res?.message || _('Firewall configuration applied.'); 392 | ui.addNotification(null, E('p', {}, msg), 'info'); 393 | }).catch(function(err) { 394 | ui.addNotification(null, E('p', {}, _('Failed to configure firewall: %s').format(err?.message || err || 'Unknown error')), 'error'); 395 | }).finally(function() { 396 | btn.disabled = false; 397 | }); 398 | }; 399 | 400 | // Create the account settings 401 | s.tab('account', _('Account Settings')); 402 | defTabOpts(s, 'account', accountConf, { optional: false }); 403 | 404 | const loginBtn = s.taboption('account', form.Button, '_login', _('Login'), 405 | _('Click to get a login URL for this device.') 406 | +'
'+_('If the timeout is displayed, you can refresh the page and click Login again.')); 407 | loginBtn.inputstyle = 'apply'; 408 | 409 | const customLoginUrl = s.taboption('account', form.Value, 'custom_login_url', 410 | _('Custom Login Server'), 411 | _('Optional: Specify a custom control server URL (e.g., a Headscale instance, https://example.com).') 412 | +'
'+_('Leave blank for default Tailscale control plane.') 413 | ); 414 | customLoginUrl.placeholder = ''; 415 | customLoginUrl.rmempty = true; 416 | 417 | const customLoginAuthKey = s.taboption('account', form.Value, 'custom_login_AuthKey', 418 | _('Custom Login Server Auth Key'), 419 | _('Optional: Specify an authentication key for the custom control server. Leave blank if not required.') 420 | +'
'+_('If you are using custom login server but not providing an Auth Key, will redirect to the login page without pre-filling the key.') 421 | ); 422 | customLoginAuthKey.placeholder = ''; 423 | customLoginAuthKey.rmempty = true; 424 | 425 | const logoutBtn = s.taboption('account', form.Button, '_logout', _('Logout'), 426 | _('Click to Log out account on this device.') 427 | +'
'+_('Disconnect from Tailscale and expire current node key.')); 428 | logoutBtn.inputstyle = 'apply'; 429 | logoutBtn.id = 'tailscale_logout_btn'; 430 | 431 | loginBtn.onclick = function() { 432 | const customServerInput = document.getElementById('widget.cbid.tailscale.settings.custom_login_url'); 433 | const customServer = customServerInput ? customServerInput.value : ''; 434 | const customserverAuthInput = document.getElementById('widget.cbid.tailscale.settings.custom_login_AuthKey'); 435 | const customServerAuth = customserverAuthInput ? customserverAuthInput.value : ''; 436 | const loginWindow = window.open('', '_blank'); 437 | if (!loginWindow) { 438 | ui.addTimeLimitedNotification(null, [ E('p', _('Could not open a new tab. Please check if your browser or an extension blocked the pop-up.')) ], 10000, 'error'); 439 | return; 440 | } 441 | // Display a prompt message in the new window 442 | const doc = loginWindow.document; 443 | doc.body.innerHTML = 444 | '

' + _('Tailscale Login') + '

' + 445 | '

' + _('Requesting Tailscale login URL... Please wait.') + '

' + 446 | '

' + _('This can take up to 30 seconds.') + '

'; 447 | 448 | ui.showModal(_('Requesting Login URL...'), E('em', {}, _('Please wait.'))); 449 | const payload = { 450 | loginserver: customServer || '', 451 | loginserver_authkey: customServerAuth || '' 452 | }; 453 | // Show a "loading" modal and execute the asynchronous RPC call 454 | ui.showModal(_('Requesting Login URL...'), E('em', {}, _('Please wait.'))); 455 | return callDoLogin(payload).then(function(res) { 456 | ui.hideModal(); 457 | if (res && res.url) { 458 | // After successfully obtaining the URL, redirect the previously opened tab 459 | loginWindow.location.href = res.url; 460 | } else { 461 | // If it fails, inform the user and they can close the new tab 462 | doc.body.innerHTML = 463 | '

' + _('Error') + '

' + 464 | '

' + _('Failed to get login URL. You may close this tab.') + '

'; 465 | ui.addTimeLimitedNotification(null, [ E('p', _('Failed to get login URL: Invalid response from server.')) ], 7000, 'error'); 466 | } 467 | }).catch(function(err) { 468 | ui.hideModal(); 469 | ui.addTimeLimitedNotification(null, [ E('p', _('Failed to get login URL: %s').format(err.message || _('Unknown error'))) ], 7000, 'error'); 470 | }); 471 | }; 472 | 473 | logoutBtn.onclick = function() { 474 | const confirmationContent = E([ 475 | E('p', {}, _('Are you sure you want to log out?') 476 | +'
'+_('This will disconnect this device from your Tailnet and require you to re-authenticate.')), 477 | 478 | E('div', { 'style': 'text-align: right; margin-top: 1em;' }, [ 479 | E('button', { 480 | 'class': 'cbi-button', 481 | 'click': ui.hideModal 482 | }, _('Cancel')), 483 | ' ', 484 | E('button', { 485 | 'class': 'cbi-button cbi-button-negative', 486 | 'click': function() { 487 | ui.hideModal(); 488 | ui.showModal(_('Logging out...'), E('em', {}, _('Please wait.'))); 489 | 490 | return callDoLogout().then(function(res) { 491 | ui.hideModal(); 492 | ui.addTimeLimitedNotification(null, [ E('p', _('Successfully logged out.')) ], 5000, 'info'); 493 | }).catch(function(err) { 494 | ui.hideModal(); 495 | ui.addTimeLimitedNotification(null, [ E('p', _('Logout failed: %s').format(err.message || _('Unknown error'))) ], 7000, 'error'); 496 | }); 497 | } 498 | }, _('Logout')) 499 | ]) 500 | ]); 501 | ui.showModal(_('Confirm Logout'), confirmationContent); 502 | }; 503 | 504 | // Create the "Daemon Settings" tab and apply daemonConf 505 | //s.tab('daemon', _('Daemon Settings')); 506 | //defTabOpts(s, 'daemon', daemonConf, { optional: false }); 507 | 508 | return map.render(); 509 | }, 510 | 511 | // The handleSaveApply function is executed after clicking "Save & Apply" 512 | handleSaveApply(ev) { 513 | return map.save().then(function () { 514 | const data = map.data.get('tailscale', 'settings'); 515 | 516 | // fix empty value issue 517 | if(!data.advertise_exit_node) data.advertise_exit_node = ''; 518 | if(!data.advertise_routes) data.advertise_routes = ''; 519 | if(!data.custom_login_url) data.custom_login_url = ''; 520 | if(!data.custom_login_AuthKey) data.custom_login_AuthKey = ''; 521 | 522 | ui.showModal(_('Applying changes...'), E('em', {}, _('Please wait.'))); 523 | 524 | return callSetSettings(data).then(function (response) { 525 | if (response.success) { 526 | ui.hideModal(); 527 | setTimeout(function() { 528 | ui.addTimeLimitedNotification(null, [ E('p', _('Tailscale settings applied successfully.')) ], 5000, 'info'); 529 | }, 1000); 530 | try { 531 | L.ui.changes.revert(); 532 | } catch (error) { 533 | ui.addTimeLimitedNotification(null, [ E('p', _('Error saving settings: %s').format(error || _('Unknown error'))) ], 7000, 'error'); 534 | } 535 | } else { 536 | ui.hideModal(); 537 | ui.addTimeLimitedNotification(null, [ E('p', _('Error applying settings: %s').format(response.error || _('Unknown error'))) ], 7000, 'error'); 538 | } 539 | }); 540 | }).catch(function(err) { 541 | ui.hideModal(); 542 | //console.error('Save failed:', err); 543 | ui.addTimeLimitedNotification(null, [ E('p', _('Failed to save settings: %s').format(err.message)) ], 7000, 'error'); 544 | }); 545 | }, 546 | 547 | handleSave: null, 548 | handleReset: null 549 | }); 550 | --------------------------------------------------------------------------------