├── .github └── workflows │ ├── build_release.yml │ └── update_hosts.yml ├── .gitignore ├── README.md ├── README_template.md ├── build.ps1 ├── hosts ├── requirements.txt ├── setDNS.ico ├── setDNS.py ├── setHosts.ico ├── setHosts.py ├── setHosts_Classic.ico └── setHosts_Classic.py /.github/workflows/build_release.yml: -------------------------------------------------------------------------------- 1 | name: Build and Release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | release: 7 | types: [created] 8 | 9 | jobs: 10 | build-and-release: 11 | strategy: 12 | matrix: 13 | include: 14 | - os: ubuntu-latest 15 | platform: Linux 16 | - os: windows-latest 17 | platform: Windows 18 | - os: macos-latest 19 | platform: macOS 20 | runs-on: ${{ matrix.os }} 21 | permissions: 22 | contents: write 23 | 24 | steps: 25 | - uses: actions/checkout@v4 26 | with: 27 | fetch-depth: 0 28 | 29 | - name: Set up Python 30 | uses: actions/setup-python@v5 31 | with: 32 | python-version: '3.x' 33 | cache: 'pip' 34 | 35 | - name: Install dependencies 36 | run: | 37 | python -m pip install --upgrade pip 38 | pip install -r requirements.txt 39 | pip install -U pyinstaller 40 | 41 | - name: Build executables (Linux) 42 | if: matrix.os == 'ubuntu-latest' 43 | run: | 44 | for script in setDNS.py setHosts.py setHosts_Classic.py; do 45 | pyinstaller --clean --onefile "$script" 46 | mv "dist/${script%.py}" "dist/${script%.py}-Linux-x64" 47 | done 48 | zip -j "cnNetTool-Linux-x64.zip" dist/*-Linux-x64 49 | 50 | - name: Build executables (Windows) 51 | if: matrix.os == 'windows-latest' 52 | run: | 53 | $scripts = @("setDNS.py", "setHosts.py", "setHosts_Classic.py") 54 | if (Test-Path -Path "dist") { 55 | Remove-Item -Recurse -Force "dist" 56 | } 57 | foreach ($script in $scripts) { 58 | $icoName = $script -replace '\.py$', '.ico' 59 | 60 | if (Test-Path -Path $icoName) { 61 | pyinstaller --clean --onefile $script --uac-admin --icon $icoName 62 | } else { 63 | pyinstaller --clean --onefile $script --uac-admin 64 | } 65 | # pyinstaller --onefile $script --uac-admin 66 | 67 | $exeName = $script -replace '\.py$', '' 68 | Move-Item "dist\$exeName.exe" "dist\$exeName-Windows-x64.exe" 69 | } 70 | Compress-Archive -Path dist\*-Windows-x64.exe -DestinationPath "cnNetTool-Windows-x64.zip" 71 | shell: pwsh 72 | 73 | - name: Build executables (macOS) 74 | if: matrix.os == 'macos-latest' 75 | run: | 76 | for script in setDNS.py setHosts.py setHosts_Classic.py; do 77 | pyinstaller --clean --onefile "$script" 78 | mv "dist/${script%.py}" "dist/${script%.py}-macOS-x64" 79 | done 80 | zip -j "cnNetTool-macOS-x64.zip" dist/*-macOS-x64 81 | 82 | - name: Upload artifacts 83 | uses: actions/upload-artifact@v4 84 | with: 85 | name: ${{ matrix.platform }}-executables 86 | path: cnNetTool-${{ matrix.platform }}-x64.zip 87 | 88 | create-release: 89 | needs: build-and-release 90 | runs-on: ubuntu-latest 91 | permissions: 92 | contents: write 93 | 94 | steps: 95 | - uses: actions/checkout@v4 96 | with: 97 | fetch-depth: 0 98 | 99 | - name: Download all artifacts 100 | uses: actions/download-artifact@v4 101 | 102 | - name: Get tag description 103 | id: tag_description 104 | run: | 105 | TAG_DESCRIPTION=$(git tag -l --format='%(contents)' ${{ github.ref_name }}) 106 | echo "tag_description=${TAG_DESCRIPTION}" >> $GITHUB_OUTPUT 107 | shell: bash 108 | 109 | - name: Get latest commit message 110 | id: commit_message 111 | run: | 112 | COMMIT_MESSAGE=$(git log -1 --pretty=%B) 113 | echo "commit_message=${COMMIT_MESSAGE}" >> $GITHUB_OUTPUT 114 | shell: bash 115 | 116 | - name: Check existing release 117 | id: check_release 118 | run: | 119 | if gh release view ${{ github.ref_name }} &> /dev/null; then 120 | echo "release_exists=true" >> $GITHUB_OUTPUT 121 | else 122 | echo "release_exists=false" >> $GITHUB_OUTPUT 123 | fi 124 | env: 125 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 126 | 127 | - name: Delete existing release 128 | if: steps.check_release.outputs.release_exists == 'true' 129 | run: | 130 | gh release delete ${{ github.ref_name }} --yes 131 | env: 132 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 133 | 134 | - name: Create Release 135 | env: 136 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 137 | run: | 138 | gh release create ${{ github.ref_name }} \ 139 | --title "${{ github.ref_name }} ${{ steps.tag_description.outputs.tag_description }}" \ 140 | --notes "Release for ${{ github.ref_name }} 141 | 142 | Changes in this release: 143 | ${{ steps.commit_message.outputs.commit_message }}" \ 144 | --draft=false \ 145 | **/cnNetTool-Linux-x64.zip \ 146 | **/cnNetTool-Windows-x64.zip \ 147 | **/cnNetTool-macOS-x64.zip 148 | shell: bash 149 | -------------------------------------------------------------------------------- /.github/workflows/update_hosts.yml: -------------------------------------------------------------------------------- 1 | name: Update hosts 2 | 3 | on: 4 | push: 5 | paths: 6 | - 'setHosts.py' 7 | - 'setHosts_Classic.py' 8 | - 'requirements.txt' 9 | branches: 10 | - 'master' 11 | schedule: 12 | - cron: '0 */1 * * *' 13 | workflow_dispatch: 14 | 15 | permissions: 16 | contents: write 17 | 18 | jobs: 19 | format-fix: 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@v4 25 | 26 | - name: Setup Python 27 | uses: actions/setup-python@v5 28 | with: 29 | python-version: '3.x' 30 | 31 | - name: Install formatting tools 32 | run: | 33 | pip install black flake8 isort 34 | 35 | - name: Run and fix formatting 36 | run: | 37 | black . 38 | isort . 39 | flake8 --ignore=E501,W503,E203 . 40 | 41 | # 提交修复的代码 42 | - name: Commit and push fixes 43 | if: success() || failure() 44 | run: | 45 | git config --global user.name "GitHub Actions" 46 | git config --global user.email "actions@github.com" 47 | git add . 48 | git commit -m "Fix formatting issues" || echo "No changes to commit" 49 | git push -f 50 | 51 | update-hosts: 52 | runs-on: ubuntu-latest 53 | concurrency: 54 | group: ${{ github.workflow }}-${{ github.ref }} 55 | cancel-in-progress: true 56 | 57 | steps: 58 | - name: Wait for 3 minutes 59 | if: | 60 | (github.event_name == 'push' && ( 61 | contains(github.event.head_commit.added, 'setDNS.py') || 62 | contains(github.event.head_commit.modified, 'setDNS.py') || 63 | contains(github.event.head_commit.added, 'setHosts.py') || 64 | contains(github.event.head_commit.modified, 'setHosts.py') || 65 | contains(github.event.head_commit.added, 'setHosts_Classic.py') || 66 | contains(github.event.head_commit.modified, 'setHosts_Classic.py') || 67 | contains(github.event.head_commit.added, 'requirements.txt') || 68 | contains(github.event.head_commit.modified, 'requirements.txt') || 69 | startsWith(github.ref, 'refs/tags/v') 70 | )) 71 | run: sleep 180 72 | 73 | - name: Checkout repository 74 | uses: actions/checkout@v4 75 | with: 76 | fetch-depth: 1 77 | 78 | - name: Set up Python 79 | uses: actions/setup-python@v5 80 | with: 81 | python-version: '3.x' 82 | cache: 'pip' 83 | 84 | - name: Install dependencies 85 | run: | 86 | python -m pip install pip 87 | pip install -r requirements.txt 88 | 89 | - name: Run hosts update script1 90 | run: | 91 | python3 setHosts_Classic.py --checkonly 92 | 93 | # 提交并推送更新(仅在文件变更时) 94 | - name: Commit and push changes 95 | uses: EndBug/add-and-commit@v9 96 | with: 97 | message: "Update hosts list" 98 | add: | 99 | hosts 100 | README.md 101 | push: true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.pyc 3 | .pybuild 4 | *.spec 5 | __pycache__ 6 | build 7 | dist 8 | *test*.* 9 | *_temp*.* 10 | *_tmp*.* 11 | .idea/ 12 | *cache/ 13 | .vscode/ 14 | .vs/ 15 | 16 | # https://github.com/phusion/debian-packaging-for-the-modern-developer/blob/master/.gitignore 17 | debhelper-build-stamp 18 | .DS_Store 19 | .debhelper 20 | *.deb 21 | *.dsc 22 | *.build 23 | *.buildinfo 24 | *.changes 25 | *.tar.gz 26 | *.log 27 | *.substvars 28 | *.csv 29 | .vagrant 30 | 31 | # 下方文件包含进仓库 32 | !README_template.md 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cnNetTool 2 | 3 | [![Release Version](https://img.shields.io/github/v/release/sinspired/cnNetTool?display_name=tag&logo=github&label=Release)](https://github.com/sinspired/cnNetTool/releases/latest) 4 | [![GitHub repo size](https://img.shields.io/github/repo-size/sinspired/cnNetTool?logo=github) 5 | ](https://github.com/sinspired/cnNetTool) 6 | [![GitHub last commit](https://img.shields.io/github/last-commit/sinspired/cnNetTool?logo=github&label=最后提交:)](ttps://github.com/sinspired/cnNetTool) 7 | 8 | 全面解锁Github,解决加载慢、无法访问等问题!解锁Google翻译,支持chrome网页翻译及插件,解锁划词翻译,以及依赖Google翻译API的各种平台插件。解锁tinyMediaManager影视刮削。 9 | 10 | 自动设置最佳DNS服务器。 11 | 12 | > 适合部分地区饱受dns污染困扰,访问 GitHub 卡顿、抽风、图裂,无法使用Chrome浏览器 自带翻译功能,无法刮削影视封面等问题。分别使用 `setDNS` 自动查找最快服务器并设置,使用 `setHosts` 自动查找DNS映射主机并设置。支持Windows、Linux、MacOS。Enjoy!❤ 13 | 14 | > [!NOTE] 15 | > 首次运行大约需要2分钟以获取DNS主机并建立缓存,请耐心等待。后续运行速度大概二三十秒。 16 | 17 | ## 一、使用方法 18 | 19 | ### 1.1 自动操作 20 | 21 | 直接下载下方文件,解压后双击运行,enjoy❤! 22 | 23 | [![Release Detail](https://img.shields.io/github/v/release/sinspired/cnNetTool?sort=date&display_name=release&logo=github&label=Release)](https://github.com/sinspired/cnNetTool/releases/latest) 24 | 25 | 程序使用DNS服务器实时解析和DNS A、AAAA记录获取IPv4及IPv6地址,通过本地网络环境检测延迟并进行SSL证书验证。 26 | 27 | 由于需要进行 `hosts` 修改备份操作,exe文件已标记需要管理员权限,如果被系统误报病毒,请允许后再次操作。 28 | 29 | > 强烈建议采用本方法,如果喜欢折腾,可以继续往下看。 30 | 31 | ### 1.2 手动操作 32 | 33 | #### 1.2.1 复制下面的内容 34 | 35 | ```bash 36 | 37 | # cnNetTool Start in 2025-06-06 14:39:23 +08:00 38 | 140.82.113.26 alive.github.com 39 | 140.82.113.25 live.github.com 40 | 140.82.114.6 api.github.com 41 | 140.82.112.9 codeload.github.com 42 | 140.82.114.22 central.github.com 43 | 140.82.112.4 gist.github.com 44 | 140.82.113.4 github.com 45 | 140.82.112.18 github.community 46 | 151.101.65.194 github.global.ssl.fastly.net 47 | 3.5.28.168 github-com.s3.amazonaws.com 48 | 52.216.93.43 github-production-release-asset-2e65be.s3.amazonaws.com 49 | 3.5.30.167 github-production-user-asset-6210df.s3.amazonaws.com 50 | 3.5.29.225 github-production-repository-file-5c1aeb.s3.amazonaws.com 51 | 13.107.42.16 pipelines.actions.githubusercontent.com 52 | 185.199.110.154 github.githubassets.com 53 | 16.15.184.174 github-cloud.s3.amazonaws.com 54 | 192.0.66.2 github.blog 55 | 185.199.110.153 githubstatus.com 56 | 185.199.110.153 assets-cdn.github.com 57 | 185.199.110.153 github.io 58 | 140.82.114.21 collector.github.com 59 | 140.82.114.21 education.github.com 60 | 185.199.110.133 avatars.githubusercontent.com 61 | 185.199.110.133 avatars0.githubusercontent.com 62 | 185.199.110.133 avatars1.githubusercontent.com 63 | 185.199.110.133 avatars2.githubusercontent.com 64 | 185.199.110.133 avatars3.githubusercontent.com 65 | 185.199.110.133 avatars4.githubusercontent.com 66 | 185.199.110.133 avatars5.githubusercontent.com 67 | 185.199.110.133 camo.githubusercontent.com 68 | 185.199.110.133 cloud.githubusercontent.com 69 | 185.199.110.133 desktop.githubusercontent.com 70 | 185.199.110.133 favicons.githubusercontent.com 71 | 185.199.110.133 github.map.fastly.net 72 | 185.199.110.133 media.githubusercontent.com 73 | 185.199.110.133 objects.githubusercontent.com 74 | 185.199.110.133 private-user-images.githubusercontent.com 75 | 185.199.110.133 raw.githubusercontent.com 76 | 185.199.110.133 user-images.githubusercontent.com 77 | 3.170.103.59 tmdb.org 78 | 3.170.103.59 api.tmdb.org 79 | 3.170.103.59 files.tmdb.org 80 | 3.170.103.59 themoviedb.org 81 | 3.170.103.59 api.themoviedb.org 82 | 3.170.103.59 www.themoviedb.org 83 | 3.170.103.59 auth.themoviedb.org 84 | 185.93.1.244 image.tmdb.org 85 | 185.93.1.244 images.tmdb.org 86 | 3.168.35.144 imdb.com 87 | 3.168.35.144 www.imdb.com 88 | 3.168.35.144 secure.imdb.com 89 | 3.168.35.144 s.media-imdb.com 90 | 3.168.35.144 us.dd.imdb.com 91 | 3.168.35.144 www.imdb.to 92 | 3.168.35.144 imdb-webservice.amazon.com 93 | 3.168.35.144 origin-www.imdb.com 94 | 104.102.249.65 m.media-amazon.com 95 | 104.102.249.65 Images-na.ssl-images-amazon.com 96 | 104.102.249.65 images-fe.ssl-images-amazon.com 97 | 104.102.249.65 images-eu.ssl-images-amazon.com 98 | 104.102.249.65 ia.media-imdb.com 99 | 104.102.249.65 f.media-amazon.com 100 | 104.102.249.65 imdb-video.media-imdb.com 101 | 104.102.249.65 dqpnq362acqdi.cloudfront.net 102 | 142.251.167.113 translate.google.com 103 | 142.251.167.113 translate.googleapis.com 104 | 142.251.167.113 translate-pa.googleapis.com 105 | 142.251.167.113 jnn-pa.googleapis.com 106 | 3.162.174.78 plugins.jetbrains.com 107 | 3.162.174.78 download.jetbrains.com 108 | 3.162.174.78 cache-redirector.jetbrains.com 109 | 110 | # Update time: 2025-06-06 14:39:23 +08:00 111 | # GitHub仓库: https://github.com/sinspired/cnNetTool 112 | # cnNetTool End 113 | 114 | ``` 115 | 116 | 以上内容会自动定时更新, 数据更新时间:2025-06-06 14:39:23 +08:00 117 | 118 | > [!NOTE] 119 | > 由于数据获取于非本地网络环境,请自行测试可用性,否则请采用方法 1,使用本地网络环境自动设置。 120 | 121 | #### 1.2.2 修改 hosts 文件 122 | 123 | hosts 文件在每个系统的位置不一,详情如下: 124 | - Windows 系统:`C:\Windows\System32\drivers\etc\hosts` 125 | - Linux 系统:`/etc/hosts` 126 | - Mac(苹果电脑)系统:`/etc/hosts` 127 | - Android(安卓)系统:`/system/etc/hosts` 128 | - iPhone(iOS)系统:`/etc/hosts` 129 | 130 | 修改方法,把第一步的内容复制到文本末尾: 131 | 132 | 1. Windows 使用记事本。 133 | 2. Linux、Mac 使用 Root 权限:`sudo vi /etc/hosts`。 134 | 3. iPhone、iPad 须越狱、Android 必须要 root。 135 | 136 | > [!NOTE] 137 | > Windows系统可能需要先把 `hosts` 文件复制到其他目录,修改后再复制回去,否则可能没有修改权限。 138 | 139 | ## 二、安装 140 | 141 | 首先安装 python,然后在终端中运行以下命令: 142 | 143 | ```bash 144 | git clone https://github.com/sinspired/cnNetTool.git 145 | cd cnNetTool 146 | pip install -r requirements.txt 147 | ``` 148 | 这将安装所有依赖项 149 | 150 | ## 参数说明 151 | 152 | **cnNetTool** 可以接受以下参数: 153 | 154 | ### DNS 服务器工具 `SetDNS.py` 155 | 156 | * --debug 启用调试日志 157 | * --show-availbale-list, --list, -l 显示可用dns列表,通过 --num 控制显示数量 158 | * --best-dns-num BEST_DNS_NUM, --num, -n 显示最佳DNS服务器的数量 159 | * --algorithm --mode {region,overall} 默认 `region` 平衡IPv4和ipv6 DNS,选择 `overall` 则会在所有IP中选择最快IP 160 | * --show-resolutions, --resolutions, -r 显示域名解析结果 161 | * --only-global, --global 仅使用国际DNS服务器 162 | 163 | ### Hosts文件工具 `SetHosts.py` 164 | 165 | * -log 设置日志输出等级,'DEBUG', 'INFO', 'WARNING', 'ERROR' 166 | * -num --num-fastest 限定Hosts主机 ip 数量 167 | * -max --max-latency 设置允许的最大延迟(毫秒) 168 | * -v --verbose 打印运行信息 169 | 170 | 命令行键入 `-h` `help` 获取帮助 171 | 172 | `py SetDNS.py -h` 173 | 174 | `py SetHosts.py -h` 175 | 176 | ## 三、运行 177 | 178 | 请使用管理员权限,在项目目录运行,分别设置解析最快的DNS服务器,更新hosts文件。 **接受传递参数,大部分时候直接运行即可**。 179 | 180 | ```bash 181 | py SetDNS.py 182 | py SetHosts.py 183 | ``` 184 | 可执行文件也可带参数运行 185 | ```pwsh 186 | ./SetDNS.exe --best-dns-num 10 --mode 'overall' --show-resolutions 187 | ./SetHosts.exe --num-fastest 3 --max-latency 500 188 | ``` 189 | 190 | -------------------------------------------------------------------------------- /README_template.md: -------------------------------------------------------------------------------- 1 | # cnNetTool 2 | 3 | [![Release Version](https://img.shields.io/github/v/release/sinspired/cnNetTool?display_name=tag&logo=github&label=Release)](https://github.com/sinspired/cnNetTool/releases/latest) 4 | [![GitHub repo size](https://img.shields.io/github/repo-size/sinspired/cnNetTool?logo=github) 5 | ](https://github.com/sinspired/cnNetTool) 6 | [![GitHub last commit](https://img.shields.io/github/last-commit/sinspired/cnNetTool?logo=github&label=最后提交:)](ttps://github.com/sinspired/cnNetTool) 7 | 8 | 全面解锁Github,解决加载慢、无法访问等问题!解锁Google翻译,支持chrome网页翻译及插件,解锁划词翻译,以及依赖Google翻译API的各种平台插件。解锁tinyMediaManager影视刮削。 9 | 10 | 自动设置最佳DNS服务器。 11 | 12 | > 适合部分地区饱受dns污染困扰,访问 GitHub 卡顿、抽风、图裂,无法使用Chrome浏览器 自带翻译功能,无法刮削影视封面等问题。分别使用 `setDNS` 自动查找最快服务器并设置,使用 `setHosts` 自动查找DNS映射主机并设置。支持Windows、Linux、MacOS。Enjoy!❤ 13 | 14 | > [!NOTE] 15 | > 首次运行大约需要2分钟以获取DNS主机并建立缓存,请耐心等待。后续运行速度大概二三十秒。 16 | 17 | ## 一、使用方法 18 | 19 | ### 1.1 自动操作 20 | 21 | 直接下载下方文件,解压后双击运行,enjoy❤! 22 | 23 | [![Release Detail](https://img.shields.io/github/v/release/sinspired/cnNetTool?sort=date&display_name=release&logo=github&label=Release)](https://github.com/sinspired/cnNetTool/releases/latest) 24 | 25 | 程序使用DNS服务器实时解析和DNS A、AAAA记录获取IPv4及IPv6地址,通过本地网络环境检测延迟并进行SSL证书验证。 26 | 27 | 由于需要进行 `hosts` 修改备份操作,exe文件已标记需要管理员权限,如果被系统误报病毒,请允许后再次操作。 28 | 29 | > 强烈建议采用本方法,如果喜欢折腾,可以继续往下看。 30 | 31 | ### 1.2 手动操作 32 | 33 | #### 1.2.1 复制下面的内容 34 | 35 | ```bash 36 | {hosts_str} 37 | ``` 38 | 39 | 以上内容会自动定时更新, 数据更新时间:{update_time} 40 | 41 | > [!NOTE] 42 | > 由于数据获取于非本地网络环境,请自行测试可用性,否则请采用方法 1,使用本地网络环境自动设置。 43 | 44 | #### 1.2.2 修改 hosts 文件 45 | 46 | hosts 文件在每个系统的位置不一,详情如下: 47 | - Windows 系统:`C:\Windows\System32\drivers\etc\hosts` 48 | - Linux 系统:`/etc/hosts` 49 | - Mac(苹果电脑)系统:`/etc/hosts` 50 | - Android(安卓)系统:`/system/etc/hosts` 51 | - iPhone(iOS)系统:`/etc/hosts` 52 | 53 | 修改方法,把第一步的内容复制到文本末尾: 54 | 55 | 1. Windows 使用记事本。 56 | 2. Linux、Mac 使用 Root 权限:`sudo vi /etc/hosts`。 57 | 3. iPhone、iPad 须越狱、Android 必须要 root。 58 | 59 | > [!NOTE] 60 | > Windows系统可能需要先把 `hosts` 文件复制到其他目录,修改后再复制回去,否则可能没有修改权限。 61 | 62 | ## 二、安装 63 | 64 | 首先安装 python,然后在终端中运行以下命令: 65 | 66 | ```bash 67 | git clone https://github.com/sinspired/cnNetTool.git 68 | cd cnNetTool 69 | pip install -r requirements.txt 70 | ``` 71 | 这将安装所有依赖项 72 | 73 | ## 参数说明 74 | 75 | **cnNetTool** 可以接受以下参数: 76 | 77 | ### DNS 服务器工具 `SetDNS.py` 78 | 79 | * --debug 启用调试日志 80 | * --show-availbale-list, --list, -l 显示可用dns列表,通过 --num 控制显示数量 81 | * --best-dns-num BEST_DNS_NUM, --num, -n 显示最佳DNS服务器的数量 82 | * --algorithm --mode {region,overall} 默认 `region` 平衡IPv4和ipv6 DNS,选择 `overall` 则会在所有IP中选择最快IP 83 | * --show-resolutions, --resolutions, -r 显示域名解析结果 84 | * --only-global, --global 仅使用国际DNS服务器 85 | 86 | ### Hosts文件工具 `SetHosts.py` 87 | 88 | * -log 设置日志输出等级,'DEBUG', 'INFO', 'WARNING', 'ERROR' 89 | * -num --num-fastest 限定Hosts主机 ip 数量 90 | * -max --max-latency 设置允许的最大延迟(毫秒) 91 | * -v --verbose 打印运行信息 92 | 93 | 命令行键入 `-h` `help` 获取帮助 94 | 95 | `py SetDNS.py -h` 96 | 97 | `py SetHosts.py -h` 98 | 99 | ## 三、运行 100 | 101 | 请使用管理员权限,在项目目录运行,分别设置解析最快的DNS服务器,更新hosts文件。 **接受传递参数,大部分时候直接运行即可**。 102 | 103 | ```bash 104 | py SetDNS.py 105 | py SetHosts.py 106 | ``` 107 | 可执行文件也可带参数运行 108 | ```pwsh 109 | ./SetDNS.exe --best-dns-num 10 --mode 'overall' --show-resolutions 110 | ./SetHosts.exe --num-fastest 3 --max-latency 500 111 | ``` 112 | 113 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | # 首先必须激活虚拟环境,在虚拟环境下运行 2 | .venv\Scripts\Activate.ps1 3 | $scripts = @("setDNS.py", "setHosts.py", "setHosts_Classic.py") 4 | if (Test-Path -Path "dist") { 5 | Remove-Item -Recurse -Force "dist" 6 | } 7 | foreach ($script in $scripts) { 8 | $icoName = $script -replace '\.py$', '.ico' 9 | 10 | if (Test-Path -Path $icoName) { 11 | pyinstaller --clean --onefile $script --uac-admin --icon $icoName --hidden-import dns 12 | } 13 | else { 14 | pyinstaller --clean --onefile $script --uac-admin --hidden-import dns 15 | } 16 | # pyinstaller --onefile $script --uac-admin 17 | 18 | $exeName = $script -replace '\.py$', '' 19 | Move-Item "dist\$exeName.exe" "dist\$exeName-Windows-x64.exe" 20 | } -------------------------------------------------------------------------------- /hosts: -------------------------------------------------------------------------------- 1 | 2 | # cnNetTool Start in 2025-06-06 14:39:23 +08:00 3 | 140.82.113.26 alive.github.com 4 | 140.82.113.25 live.github.com 5 | 140.82.114.6 api.github.com 6 | 140.82.112.9 codeload.github.com 7 | 140.82.114.22 central.github.com 8 | 140.82.112.4 gist.github.com 9 | 140.82.113.4 github.com 10 | 140.82.112.18 github.community 11 | 151.101.65.194 github.global.ssl.fastly.net 12 | 3.5.28.168 github-com.s3.amazonaws.com 13 | 52.216.93.43 github-production-release-asset-2e65be.s3.amazonaws.com 14 | 3.5.30.167 github-production-user-asset-6210df.s3.amazonaws.com 15 | 3.5.29.225 github-production-repository-file-5c1aeb.s3.amazonaws.com 16 | 13.107.42.16 pipelines.actions.githubusercontent.com 17 | 185.199.110.154 github.githubassets.com 18 | 16.15.184.174 github-cloud.s3.amazonaws.com 19 | 192.0.66.2 github.blog 20 | 185.199.110.153 githubstatus.com 21 | 185.199.110.153 assets-cdn.github.com 22 | 185.199.110.153 github.io 23 | 140.82.114.21 collector.github.com 24 | 140.82.114.21 education.github.com 25 | 185.199.110.133 avatars.githubusercontent.com 26 | 185.199.110.133 avatars0.githubusercontent.com 27 | 185.199.110.133 avatars1.githubusercontent.com 28 | 185.199.110.133 avatars2.githubusercontent.com 29 | 185.199.110.133 avatars3.githubusercontent.com 30 | 185.199.110.133 avatars4.githubusercontent.com 31 | 185.199.110.133 avatars5.githubusercontent.com 32 | 185.199.110.133 camo.githubusercontent.com 33 | 185.199.110.133 cloud.githubusercontent.com 34 | 185.199.110.133 desktop.githubusercontent.com 35 | 185.199.110.133 favicons.githubusercontent.com 36 | 185.199.110.133 github.map.fastly.net 37 | 185.199.110.133 media.githubusercontent.com 38 | 185.199.110.133 objects.githubusercontent.com 39 | 185.199.110.133 private-user-images.githubusercontent.com 40 | 185.199.110.133 raw.githubusercontent.com 41 | 185.199.110.133 user-images.githubusercontent.com 42 | 3.170.103.59 tmdb.org 43 | 3.170.103.59 api.tmdb.org 44 | 3.170.103.59 files.tmdb.org 45 | 3.170.103.59 themoviedb.org 46 | 3.170.103.59 api.themoviedb.org 47 | 3.170.103.59 www.themoviedb.org 48 | 3.170.103.59 auth.themoviedb.org 49 | 185.93.1.244 image.tmdb.org 50 | 185.93.1.244 images.tmdb.org 51 | 3.168.35.144 imdb.com 52 | 3.168.35.144 www.imdb.com 53 | 3.168.35.144 secure.imdb.com 54 | 3.168.35.144 s.media-imdb.com 55 | 3.168.35.144 us.dd.imdb.com 56 | 3.168.35.144 www.imdb.to 57 | 3.168.35.144 imdb-webservice.amazon.com 58 | 3.168.35.144 origin-www.imdb.com 59 | 104.102.249.65 m.media-amazon.com 60 | 104.102.249.65 Images-na.ssl-images-amazon.com 61 | 104.102.249.65 images-fe.ssl-images-amazon.com 62 | 104.102.249.65 images-eu.ssl-images-amazon.com 63 | 104.102.249.65 ia.media-imdb.com 64 | 104.102.249.65 f.media-amazon.com 65 | 104.102.249.65 imdb-video.media-imdb.com 66 | 104.102.249.65 dqpnq362acqdi.cloudfront.net 67 | 142.251.167.113 translate.google.com 68 | 142.251.167.113 translate.googleapis.com 69 | 142.251.167.113 translate-pa.googleapis.com 70 | 142.251.167.113 jnn-pa.googleapis.com 71 | 3.162.174.78 plugins.jetbrains.com 72 | 3.162.174.78 download.jetbrains.com 73 | 3.162.174.78 cache-redirector.jetbrains.com 74 | 75 | # Update time: 2025-06-06 14:39:23 +08:00 76 | # GitHub仓库: https://github.com/sinspired/cnNetTool 77 | # cnNetTool End 78 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.11.10 2 | dnspython==2.7.0 3 | httpx==0.28.1 4 | prettytable==3.16.0 5 | rich==14.0.0 6 | wcwidth==0.2.13 7 | -------------------------------------------------------------------------------- /setDNS.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinspired/cnNetTool/a41aa9ddb97ce99c91cb1dabe79a25e2bd09ee88/setDNS.ico -------------------------------------------------------------------------------- /setDNS.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import argparse 5 | import ctypes 6 | import logging 7 | import os 8 | import platform 9 | import statistics 10 | import subprocess 11 | import sys 12 | import threading 13 | import time 14 | from concurrent.futures import ThreadPoolExecutor, as_completed 15 | 16 | import dns.resolver 17 | from prettytable import PrettyTable 18 | 19 | # 设置日志记录 20 | logger = logging.getLogger(__name__) 21 | 22 | # 全局常量 23 | DNS_TIMEOUT = 1 # DNS 查询超时时间(秒) 24 | TEST_ITERATIONS = 2 # 每个DNS服务器测试的次数 25 | DISPLAY_LIMIT = 20 # 每个域名显示的 DNS 服务器数量 26 | MAX_THREADS = 20 # 最大线程数 27 | BEST_DNS_NUM = 5 # 输出最佳 DNS 服务器数量 28 | 29 | # 待测试的域名列表 30 | DOMAINS_TO_TEST = [ 31 | "translate.google.com", 32 | "translate.googleapis.com", 33 | "translate-pa.googleapis.com", 34 | "github.com", 35 | "github.io", 36 | "tmdb.org", 37 | "api.github.com", 38 | "raw.githubusercontent.com", 39 | ] 40 | 41 | # DNS服务器列表 42 | DNS_SERVERS = { 43 | "全球": { 44 | "OpenDNS": { 45 | "ipv4": ["208.67.222.222"], 46 | "ipv6": ["2620:0:ccc::2"], 47 | }, 48 | "Google Public DNS": { 49 | "ipv4": ["8.8.8.8"], 50 | "ipv6": ["2001:4860:4860::8888", "2001:4860:4860::8844"], 51 | }, 52 | "Quad9": { 53 | "ipv4": ["9.9.9.9", "149.112.112.112"], 54 | "ipv6": ["2620:fe::fe", "2620:fe::9"], 55 | }, 56 | "Verisign": { 57 | "ipv4": ["64.6.64.6", "64.6.65.6"], 58 | "ipv6": ["2620:74:1b::1:1", "2620:74:1c::2:2"], 59 | }, 60 | "NTT Communications DNS": {"ipv4": ["129.250.35.250"], "ipv6": []}, 61 | "KT DNS": {"ipv4": ["168.126.63.1"], "ipv6": []}, 62 | "CPC HK": {"ipv4": ["210.184.24.65", "152.101.4.130"], "ipv6": []}, 63 | "Soft Bank": {"ipv4": ["101.110.50.106"], "ipv6": []}, 64 | "SingNet": {"ipv4": ["118.201.189.90"], "ipv6": []}, 65 | "SK Broadband": {"ipv4": ["1.228.180.5"], "ipv6": []}, 66 | "Korea Telecom": {"ipv4": ["183.99.33.6"], "ipv6": []}, 67 | "Amazon.HK": {"ipv4": ["18.163.103.200"]}, 68 | "IPTELECOM.HK": {"ipv4": ["43.251.159.130"]}, 69 | "Broadband Network.HK": {"ipv4": ["14.198.168.140"]}, 70 | "HKT.HK": {"ipv4": ["203.198.161.89"]}, 71 | "Cloudie.HK": {"ipv4": ["103.51.144.212"]}, 72 | "Dimension.HK": {"ipv4": ["66.203.146.122"]}, 73 | "ONL.HK": {"ipv4": ["103.142.147.14"]}, 74 | "SkyExchange.HK": {"ipv4": ["156.241.7.91"]}, 75 | }, 76 | "中国大陆": { 77 | "114DNS": { 78 | "ipv4": ["114.114.114.114", "114.114.115.115"], 79 | "ipv6": [], 80 | }, 81 | "上海牙木科技|联通机房": { 82 | "ipv4": ["1.1.8.8"], 83 | "ipv6": [], 84 | }, 85 | "上海通讯": { 86 | "ipv4": ["202.46.33.250", "202.46.33.250"], 87 | "ipv6": [], 88 | }, 89 | "百度DNS": { 90 | "ipv4": ["180.76.76.76"], 91 | "ipv6": [], 92 | }, 93 | "深圳桑瑞诗科技": { 94 | "ipv4": ["202.46.34.74"], 95 | "ipv6": [], 96 | }, 97 | # "阿里云DNS": { 98 | # "ipv4": ["223.5.5.5", "223.6.6.6"], 99 | # "ipv6": ["2400:3200::1", "2400:3200:baba::1"], 100 | # }, 101 | # "DNSPod (腾讯)": {"ipv4": ["119.29.29.29"], "ipv6": ["2402:4e00::"]}, 102 | }, 103 | } 104 | 105 | 106 | def test_dns_server( 107 | server: str, domain: str, record_type: str 108 | ) -> tuple[bool, float, list[str]]: 109 | """ 110 | 测试指定的DNS服务器 111 | 112 | :param server: DNS服务器IP地址 113 | :param domain: 要解析的域名 114 | :param record_type: DNS记录类型 (A 或 AAAA) 115 | :return: 元组 (是否成功解析, 响应时间, IP地址列表) 116 | """ 117 | resolver = dns.resolver.Resolver(configure=False) 118 | resolver.nameservers = [server] 119 | resolver.lifetime = DNS_TIMEOUT 120 | start_time = time.time() 121 | try: 122 | answers = resolver.resolve(domain, record_type) 123 | end_time = time.time() 124 | response_time = (end_time - start_time) * 1000 # 转换为毫秒 125 | ips = [str(rdata) for rdata in answers] 126 | logger.debug(f"成功解析 {domain} 使用 {server} ({record_type}): {ips}") 127 | return True, response_time, ips 128 | except Exception as e: 129 | end_time = time.time() 130 | response_time = (end_time - start_time) * 1000 # 转换为毫秒 131 | logger.debug(f"无法解析 {domain} 使用 {server} ({record_type}): {str(e)}") 132 | return False, response_time, ["解析失败"] 133 | 134 | 135 | def evaluate_dns_server(server: str, ip_version: str) -> tuple[float, float, dict]: 136 | """ 137 | 评估DNS服务器的性能 138 | 139 | :param server: DNS服务器IP地址 140 | :param ip_version: IP版本 ("ipv4" 或 "ipv6") 141 | :return: 元组 (成功率, 平均响应时间, 域名解析结果) 142 | """ 143 | results = [] 144 | resolutions = {} 145 | for domain in DOMAINS_TO_TEST: 146 | success, response_time, ips = test_dns_server( 147 | server, domain, "A" if ip_version == "ipv4" else "AAAA" 148 | ) 149 | results.append((success, response_time)) 150 | resolutions[domain] = ips 151 | 152 | success_rate = sum(1 for r in results if r[0]) / len(results) 153 | avg_response_time = statistics.mean(r[1] for r in results) 154 | return success_rate, avg_response_time, resolutions 155 | 156 | 157 | def find_available_dns(args) -> tuple[dict, dict]: 158 | """ 159 | 查找最佳的DNS服务器并获取域名解析结果 160 | 161 | :return: 包含IPv4和IPv6最佳DNS服务器列表的字典,以及域名解析结果的字典 162 | """ 163 | dns_performance = {} 164 | domain_resolutions = {domain: {} for domain in DOMAINS_TO_TEST} 165 | 166 | with ThreadPoolExecutor(max_workers=MAX_THREADS) as executor: 167 | future_to_server = {} 168 | for region, providers in DNS_SERVERS.items(): 169 | if args.only_global and region != "全球": 170 | continue 171 | 172 | for provider, servers in providers.items(): 173 | for ip_version in ["ipv4", "ipv6"]: 174 | for server in servers.get(ip_version, []): 175 | future = executor.submit( 176 | evaluate_dns_server, server, ip_version 177 | ) 178 | future_to_server[future] = ( 179 | server, 180 | ip_version, 181 | region, 182 | provider, 183 | ) 184 | 185 | for future in as_completed(future_to_server): 186 | server, ip_version, region, provider = future_to_server[future] 187 | try: 188 | success_rate, avg_response_time, resolutions = future.result() 189 | dns_performance[server] = { 190 | "success_rate": success_rate, 191 | "avg_response_time": avg_response_time, 192 | "ip_version": ip_version, 193 | "region": region, 194 | "provider": provider, 195 | } 196 | # 保存域名解析结果 197 | for domain, ips in resolutions.items(): 198 | domain_resolutions[domain][server] = ips 199 | 200 | logger.debug( 201 | f"{ip_version.upper()} DNS {server} ({ 202 | region} - {provider}) 成功率 {success_rate:.2%}, 平均延迟 {avg_response_time:.2f}ms" 203 | ) 204 | except Exception as exc: 205 | logger.error(f"{server} 测试出错: {str(exc)}") 206 | 207 | # 对每个IP版本排序 208 | top_ipv4 = sorted( 209 | (s for s in dns_performance.items() if s[1]["ip_version"] == "ipv4"), 210 | key=lambda x: (-x[1]["success_rate"], x[1]["avg_response_time"]), 211 | ) 212 | top_ipv6 = sorted( 213 | (s for s in dns_performance.items() if s[1]["ip_version"] == "ipv6"), 214 | key=lambda x: (-x[1]["success_rate"], x[1]["avg_response_time"]), 215 | ) 216 | 217 | return {"ipv4": top_ipv4, "ipv6": top_ipv6}, domain_resolutions 218 | 219 | 220 | def print_domain_resolutions( 221 | domain_resolutions: dict[str, dict[str, list[str]]], dns_performance: dict 222 | ): 223 | """ 224 | 打印域名解析结果表格 225 | 226 | :param domain_resolutions: 域名解析结果 227 | :param dns_performance: DNS性能数据 228 | """ 229 | for domain, resolutions in domain_resolutions.items(): 230 | table = PrettyTable() 231 | table.title = f"域名 {domain} 的解析结果" 232 | table.field_names = ["DNS服务器", "IP版本", "区域", "提供商", "解析结果"] 233 | table.align["DNS服务器"] = "l" 234 | table.align["解析结果"] = "l" 235 | 236 | ipv4_count = 0 237 | ipv6_count = 0 238 | 239 | for server, ips in resolutions.items(): 240 | info = dns_performance[server] 241 | if info["ip_version"] == "ipv4" and ipv4_count < DISPLAY_LIMIT: 242 | table.add_row( 243 | [ 244 | server, 245 | info["ip_version"].upper(), 246 | info["region"], 247 | info["provider"], 248 | "\n".join(ips[:3]) + ("\n..." if len(ips) > 3 else ""), 249 | ] 250 | ) 251 | ipv4_count += 1 252 | elif info["ip_version"] == "ipv6" and ipv6_count < DISPLAY_LIMIT: 253 | table.add_row( 254 | [ 255 | server, 256 | info["ip_version"].upper(), 257 | info["region"], 258 | info["provider"], 259 | "\n".join(ips[:3]) + ("\n..." if len(ips) > 3 else ""), 260 | ] 261 | ) 262 | ipv6_count += 1 263 | 264 | if ipv4_count >= DISPLAY_LIMIT and ipv6_count >= DISPLAY_LIMIT: 265 | break 266 | 267 | print(table) 268 | print() # 为了美观,在表格之间添加一个空行 269 | 270 | 271 | def set_dns_servers(ipv4_dns_list: list[str], ipv6_dns_list: list[str]): 272 | """ 273 | 设置系统DNS服务器 274 | 275 | :param ipv4_dns_list: IPv4 DNS服务器列表 276 | :param ipv6_dns_list: IPv6 DNS服务器列表 277 | """ 278 | ipv4_dns_list = [str(dns) for dns in ipv4_dns_list if dns] 279 | ipv6_dns_list = [str(dns) for dns in ipv6_dns_list if dns] 280 | 281 | system = platform.system() 282 | logger.info(f"正在设置DNS服务器for {system}") 283 | if system == "Windows": 284 | try: 285 | interfaces = subprocess.check_output( 286 | ["netsh", "interface", "show", "interface"] 287 | ).decode("gbk") 288 | except UnicodeDecodeError: 289 | interfaces = subprocess.check_output( 290 | ["netsh", "interface", "show", "interface"] 291 | ).decode("utf-8", errors="ignore") 292 | 293 | for line in interfaces.split("\n"): 294 | if "Connected" in line or "已连接" in line: 295 | interface = line.split()[-1] 296 | # 更严格地检查并忽略WSL相关的虚拟网卡 297 | if "WSL" in interface or "Hyper-V" in interface or "VM" in interface: 298 | logger.info(f"跳过虚拟网卡: {interface}") 299 | continue 300 | if ipv4_dns_list: 301 | logger.debug( 302 | f"设置IPv4 DNS for {interface}:{ 303 | ', '.join(ipv4_dns_list)}" 304 | ) 305 | try: 306 | subprocess.run( 307 | [ 308 | "netsh", 309 | "interface", 310 | "ipv4", 311 | "set", 312 | "dns", 313 | interface, 314 | "static", 315 | ipv4_dns_list[0], 316 | ], 317 | check=True, 318 | ) 319 | for dns_item in ipv4_dns_list[1:]: 320 | subprocess.run( 321 | [ 322 | "netsh", 323 | "interface", 324 | "ipv4", 325 | "add", 326 | "dns", 327 | interface, 328 | dns_item, 329 | "index=2", 330 | ], 331 | check=True, 332 | ) 333 | except subprocess.CalledProcessError as e: 334 | logger.debug(f"设置IPv4 DNS for {interface}失败: {e}") 335 | if ipv6_dns_list: 336 | logger.debug( 337 | f"设置IPv6 DNS for {interface}: { 338 | ', '.join(ipv6_dns_list)}" 339 | ) 340 | try: 341 | subprocess.run( 342 | [ 343 | "netsh", 344 | "interface", 345 | "ipv6", 346 | "set", 347 | "dns", 348 | interface, 349 | "static", 350 | ipv6_dns_list[0], 351 | ], 352 | check=True, 353 | ) 354 | for dns_item in ipv6_dns_list[1:]: 355 | subprocess.run( 356 | [ 357 | "netsh", 358 | "interface", 359 | "ipv6", 360 | "add", 361 | "dns", 362 | interface, 363 | dns_item, 364 | "index=2", 365 | ], 366 | check=True, 367 | ) 368 | except subprocess.CalledProcessError as e: 369 | logger.debug(f"设置IPv6 DNS for {interface}失败: {e}") 370 | 371 | elif system == "Linux": 372 | with open("/etc/resolv.conf", "w") as f: 373 | for dns_item in ipv4_dns_list: 374 | logger.debug(f"添加IPv4 DNS到 /etc/resolv.conf: {dns_item}") 375 | f.write(f"nameserver {dns_item}\n") 376 | for dns_item in ipv6_dns_list: 377 | logger.debug(f"添加IPv6 DNS到 /etc/resolv.conf: {dns_item}") 378 | f.write(f"nameserver {dns_item}\n") 379 | elif system == "Darwin": # macOS 380 | all_dns = ipv4_dns_list + ipv6_dns_list 381 | dns_string = " ".join(all_dns) 382 | logger.debug(f"设置DNS for Wi-Fi: {dns_string}") 383 | subprocess.run(["networksetup", "-setdnsservers", "Wi-Fi"] + all_dns) 384 | else: 385 | logger.error(f"不支持的操作系统: {system}") 386 | 387 | 388 | def get_best_dns_by_region(dns_list: list, region: str) -> tuple[str, dict] | None: 389 | """ 390 | 根据区域获取最佳DNS服务器 391 | 392 | :param dns_list: DNS服务器列表 393 | :param region: 区域 394 | :return: 最佳DNS服务器信息或None 395 | """ 396 | return next((s for s in dns_list if s[1]["region"] == region), None) 397 | 398 | 399 | def get_best_dns_overall(dns_list: list) -> tuple[str, dict]: 400 | """ 401 | 获取整体最佳DNS服务器 402 | 403 | :param dns_list: DNS服务器列表 404 | :return: 可用DNS服务器信息 405 | """ 406 | return max( 407 | dns_list, 408 | key=lambda x: (x[1]["success_rate"], -x[1]["avg_response_time"]), 409 | ) 410 | 411 | 412 | def get_recommended_dns(available_dns: dict, algorithm: str) -> dict[str, list]: 413 | """ 414 | 获取推荐的DNS服务器 415 | 416 | :param available_dns: 最佳DNS服务器列表 417 | :param algorithm: 推荐算法 ("region" 或 "overall") 418 | :return: 推荐的DNS服务器列表 419 | """ 420 | recommended = {"ipv4": [], "ipv6": []} 421 | for ip_version in ["ipv4", "ipv6"]: 422 | 423 | if args.only_global or algorithm == "overall": 424 | best = get_best_dns_overall(available_dns[ip_version]) 425 | # second_best = get_best_dns_overall( 426 | # [dns for dns in available_dns[ip_version] if dns != best] 427 | # ) 428 | # recommended[ip_version] = [best[0], second_best[0]] 429 | recommended[ip_version] = [best[0]] 430 | else: 431 | cn = get_best_dns_by_region(available_dns[ip_version], "中国大陆") 432 | global_ = get_best_dns_by_region(available_dns[ip_version], "全球") 433 | recommended[ip_version] = [ 434 | cn[0] if cn else None, 435 | global_[0] if global_ else None, 436 | ] 437 | return recommended 438 | 439 | 440 | def print_recommended_dns_table(dns_list: list, ip_version: str, available_dns: dict): 441 | """ 442 | 打印推荐的DNS服务器表格 443 | 444 | :param dns_list: 推荐的DNS服务器列表 445 | :param ip_version: IP版本 ("ipv4" 或 "ipv6") 446 | :param available_dns: 可用DNS服务器信息 447 | """ 448 | table = PrettyTable() 449 | table.title = f"推荐的最佳{ip_version.upper()} DNS服务器" 450 | table.field_names = ["DNS", "提供商", "区域", "成功率", "平均延迟(ms)"] 451 | for dns_item in dns_list: 452 | if dns_item: 453 | # 在best_dns列表中查找正确的服务器信息 454 | server_info = next( 455 | ( 456 | info 457 | for server, info in available_dns[ip_version] 458 | if server == dns_item 459 | ), 460 | None, 461 | ) 462 | if server_info: 463 | table.add_row( 464 | [ 465 | dns_item, 466 | server_info["provider"], 467 | server_info["region"], 468 | f"{server_info['success_rate']:.2%}", 469 | f"{server_info['avg_response_time']:.2f}", 470 | ] 471 | ) 472 | print(table) 473 | print() # 表格之间添加空行以美化 474 | 475 | 476 | def print_available_dns(available_dns, best_dns_num): 477 | print() 478 | print("可用DNS服务器:") 479 | 480 | for ip_version in ["ipv4", "ipv6"]: 481 | if available_dns[ip_version]: 482 | # 使用PrettyTable展示前 n 个DNS服务器信息 483 | table = PrettyTable() 484 | best_dns_num = min(len(available_dns[ip_version]), best_dns_num) 485 | table.title = f"前 {best_dns_num} 个可用 {ip_version.upper()} DNS服务器" 486 | table.field_names = [ 487 | "排名", 488 | "服务器", 489 | "区域", 490 | "提供商", 491 | "成功率", 492 | "平均延迟(ms)", 493 | ] 494 | 495 | for i, (server, info) in enumerate( 496 | available_dns[ip_version][:best_dns_num], 1 497 | ): 498 | table.add_row( 499 | [ 500 | i, 501 | server, 502 | info["region"], 503 | info["provider"], 504 | f"{info['success_rate']:.2%}", 505 | f"{info['avg_response_time']:.2f}", 506 | ] 507 | ) 508 | print(table) 509 | print() 510 | 511 | 512 | def get_input_with_timeout(prompt, timeout=10): 513 | # print(prompt, end="", flush=True) 514 | user_input = [] 515 | 516 | def input_thread(): 517 | user_input.append(input(prompt)) 518 | 519 | thread = threading.Thread(target=input_thread) 520 | thread.daemon = True 521 | thread.start() 522 | 523 | thread.join(timeout) 524 | if thread.is_alive(): 525 | print("\n已超时,自动执行...") 526 | return "y", thread 527 | print() # 换行 528 | return user_input[0].strip() if user_input else "y", thread 529 | 530 | 531 | def main(): 532 | """ 533 | 主函数 534 | """ 535 | 536 | # 创建自定义的日志格式化器 537 | log_formatter = logging.Formatter( 538 | "%(asctime)s - %(levelname)s - %(message)s", datefmt="%I:%M%p" 539 | ) 540 | 541 | # 创建控制台日志处理器并设置格式化器 542 | console_handler = logging.StreamHandler() 543 | console_handler.setFormatter(log_formatter) 544 | 545 | # 设置日志级别 546 | logger = logging.getLogger() 547 | if args.debug: 548 | logger.setLevel(logging.DEBUG) 549 | else: 550 | logger.setLevel(logging.INFO) 551 | 552 | # 添加控制台处理器到日志 553 | if not logger.handlers: 554 | logger.addHandler(console_handler) 555 | 556 | logger.info("开始测试DNS服务器...") 557 | available_dns, domain_resolutions = find_available_dns(args) 558 | 559 | if available_dns["ipv4"] or available_dns["ipv6"]: 560 | if args.show_resolutions: 561 | logger.info("显示域名解析结果...") 562 | dns_performance = { 563 | server: info 564 | for dns_list in available_dns.values() 565 | for server, info in dns_list 566 | } 567 | print_domain_resolutions(domain_resolutions, dns_performance) 568 | 569 | # 防止 best_dns_num 数值超过数组长度 570 | num_servers = max(len(available_dns["ipv4"]), len(available_dns["ipv6"])) 571 | if args.best_dns_num > num_servers: 572 | args.best_dns_num = num_servers 573 | 574 | if args.show_availbale_list: 575 | # 输出最好的前 best_dns_num 个dns服务器 576 | print_available_dns(available_dns, args.best_dns_num) 577 | 578 | print() 579 | logger.debug("推荐的最佳DNS服务器:") 580 | recommended_dns = get_recommended_dns(available_dns, args.algorithm) 581 | for ip_version in ["ipv4", "ipv6"]: 582 | if recommended_dns[ip_version]: 583 | print_recommended_dns_table( 584 | recommended_dns[ip_version], ip_version, available_dns 585 | ) 586 | print() 587 | confirm, thread = get_input_with_timeout( 588 | "是否要设置系统DNS为推荐的最佳服务器?(y/n,10秒后自动执行): ", 10 589 | ) 590 | if confirm.lower() == "y": 591 | set_dns_servers(recommended_dns["ipv4"], recommended_dns["ipv6"]) 592 | logger.info("DNS服务器已更新") 593 | if thread.is_alive(): # 确认输入线程是否仍在运行 594 | thread.join() # 等待线程完成 595 | input("任务执行完毕,按任意键退出!") 596 | else: 597 | logger.info("操作已取消") 598 | else: 599 | logger.warning("未找到合适的DNS服务器") 600 | input("\n任务执行失败,按任意键退出!") 601 | 602 | 603 | def is_admin() -> bool: 604 | """ 605 | 检查当前用户是否具有管理员权限 606 | 607 | :return: 是否具有管理员权限 608 | """ 609 | try: 610 | return os.getuid() == 0 611 | except AttributeError: 612 | return ctypes.windll.shell32.IsUserAnAdmin() != 0 613 | 614 | 615 | def run_as_admin(): 616 | """ 617 | 以管理员权限重新运行脚本 618 | """ 619 | if is_admin(): 620 | return 621 | 622 | if sys.platform.startswith("win"): 623 | script = os.path.abspath(sys.argv[0]) 624 | params = " ".join([script] + sys.argv[1:]) 625 | ctypes.windll.shell32.ShellExecuteW( 626 | None, "runas", sys.executable, params, None, 1 627 | ) 628 | else: 629 | os.execvp("sudo", ["sudo", "python3"] + sys.argv) 630 | sys.exit(0) 631 | 632 | 633 | if __name__ == "__main__": 634 | parser = argparse.ArgumentParser( 635 | description=( 636 | "------------------------------------------------------------\n" 637 | "DNS解析器和设置工具,请使用管理员权限运行,自动设置最佳 DNS 服务器。\n" 638 | "------------------------------------------------------------\n" 639 | ), 640 | epilog=( 641 | "------------------------------------------------------------\n" 642 | "项目: https://github.com/sinspired/cnNetTool\n" 643 | "作者: Sinspired\n" 644 | "邮箱: ggmomo@gmail.com\n" 645 | "发布: 2024-11-11\n" 646 | ), 647 | formatter_class=argparse.RawTextHelpFormatter, # 允许换行格式 648 | ) 649 | parser.add_argument("--debug", action="store_true", help="启用调试日志") 650 | parser.add_argument( 651 | "--show-availbale-list", 652 | "--list", 653 | "-l", 654 | action="store_true", 655 | help="显示可用dns列表,通过 --num 控制显示数量", 656 | ) 657 | parser.add_argument( 658 | "--best-dns-num", 659 | "--num", 660 | "-n", 661 | default=BEST_DNS_NUM, 662 | type=int, 663 | action="store", 664 | help="显示最佳DNS服务器的数量", 665 | ) 666 | parser.add_argument( 667 | "--algorithm", 668 | "--mode", 669 | choices=["allregions", "overall"], 670 | default="allregions", 671 | help="推荐最佳DNS的算法 (按区域或整体)", 672 | ) 673 | parser.add_argument( 674 | "--only-global", # argparse 会自动将第一个参数名转换为属性名。 675 | "--global", 676 | dest="only_global", # 这里指定属性名 677 | action="store_true", 678 | help="仅使用国际DNS服务器", 679 | ) 680 | parser.add_argument( 681 | "--show-resolutions", 682 | "--resolutions", 683 | "-r", 684 | action="store_true", 685 | help="显示域名解析结果", 686 | ) 687 | args = parser.parse_args() 688 | 689 | if not is_admin(): 690 | logger.info("需要管理员权限来设置DNS服务器。正在尝试提升权限...") 691 | run_as_admin() 692 | 693 | main() 694 | -------------------------------------------------------------------------------- /setHosts.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinspired/cnNetTool/a41aa9ddb97ce99c91cb1dabe79a25e2bd09ee88/setHosts.ico -------------------------------------------------------------------------------- /setHosts.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import asyncio 3 | import ctypes 4 | import json 5 | import logging 6 | import os 7 | import platform 8 | import re 9 | import shutil 10 | import socket 11 | import ssl 12 | import sys 13 | from datetime import datetime, timedelta, timezone 14 | from enum import Enum 15 | from functools import wraps 16 | from math import floor 17 | from pathlib import Path 18 | from typing import Dict, List, Optional, Set, Tuple 19 | 20 | import aiohttp 21 | import dns.resolver 22 | from rich import print as rprint 23 | from rich.progress import BarColumn, Progress, TaskID, TimeRemainingColumn 24 | 25 | # -------------------- 常量设置 -------------------- # 26 | RESOLVER_TIMEOUT = 1 # DNS 解析超时时间 秒 27 | HOSTS_NUM = 1 # 每个域名限定Hosts主机 ipv4 数量 28 | MAX_LATENCY = 300 # 允许的最大延迟 29 | PING_TIMEOUT = 1 # ping 超时时间 30 | NUM_PINGS = 4 # ping次数 31 | 32 | # 初始化日志模块 33 | logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") 34 | 35 | 36 | # -------------------- 解析参数 -------------------- # 37 | def parse_args(): 38 | parser = argparse.ArgumentParser( 39 | description=( 40 | "------------------------------------------------------------\n" 41 | "Hosts文件更新工具,此工具可自动解析域名并优化系统的hosts文件\n" 42 | "------------------------------------------------------------\n" 43 | ), 44 | epilog=( 45 | "------------------------------------------------------------\n" 46 | "项目: https://github.com/sinspired/cnNetTool\n" 47 | "作者: Sinspired\n" 48 | "邮箱: ggmomo@gmail.com\n" 49 | "发布: 2024-11-11\n" 50 | ), 51 | formatter_class=argparse.RawTextHelpFormatter, # 允许换行格式 52 | ) 53 | 54 | parser.add_argument( 55 | "-log", 56 | default="info", 57 | choices=["debug", "info", "warning", "error"], 58 | help="设置日志输出等级", 59 | ) 60 | parser.add_argument( 61 | "-num", 62 | "--hosts-num", 63 | default=HOSTS_NUM, 64 | type=int, 65 | help="限定Hosts主机 ip 数量", 66 | ) 67 | parser.add_argument( 68 | "-max", 69 | "--max-latency", 70 | default=MAX_LATENCY, 71 | type=int, 72 | help="设置允许的最大延迟(毫秒)", 73 | ) 74 | parser.add_argument( 75 | "-v", 76 | "--verbose", 77 | action="store_true", 78 | help="打印运行信息", 79 | ) 80 | parser.add_argument( 81 | "-n", 82 | "--NotUseDnsServers", 83 | action="store_true", 84 | help="不使用DNS服务器解析(避免GitHub等被dns污染的网站获取错误地址)", 85 | ) 86 | 87 | return parser.parse_args() 88 | 89 | 90 | args = parse_args() 91 | logging.getLogger().setLevel(args.log.upper()) 92 | 93 | 94 | # -------------------- 辅助功能模块 -------------------- # 95 | class Utils: 96 | @staticmethod 97 | def is_ipv6(ip: str) -> bool: 98 | return ":" in ip 99 | 100 | @staticmethod 101 | def get_hosts_file_path() -> str: 102 | os_type = platform.system().lower() 103 | if os_type == "windows": 104 | return r"C:\Windows\System32\drivers\etc\hosts" 105 | elif os_type in ["linux", "darwin"]: 106 | return "/etc/hosts" 107 | else: 108 | raise ValueError("不支持的操作系统") 109 | 110 | @staticmethod 111 | def backup_hosts_file(hosts_file_path: str): 112 | if os.path.exists(hosts_file_path): 113 | backup_path = f"{hosts_file_path}.bak" 114 | shutil.copy(hosts_file_path, backup_path) 115 | rprint( 116 | f"\n[blue]已备份 [underline]{hosts_file_path}[/underline] 到 [underline]{backup_path}[/underline][/blue]" 117 | ) 118 | 119 | @staticmethod 120 | def write_readme_file( 121 | hosts_content: List[str], temp_file_path: str, update_time: str 122 | ): 123 | """ 124 | 根据模板文件生成 README.md 文件,并将 hosts 文件内容写入其中。 125 | 126 | 参数: 127 | hosts_content (List[str]): hosts 文件的内容,以列表形式传入 128 | temp_file_path (str): 输出的 README.md 文件路径 129 | update_time (str): hosts 文件的更新时间,格式为 "YYYY-MM-DD HH:MM:SS +0800" 130 | """ 131 | try: 132 | # 获取template文件的绝对路径 133 | current_dir = os.path.dirname(os.path.abspath(__file__)) 134 | template_path = os.path.join(current_dir, temp_file_path) 135 | 136 | if not os.path.exists(template_path): 137 | raise FileNotFoundError(f"模板文件未找到: {template_path}") 138 | 139 | # 读取模板文件 140 | with open(template_path, "r", encoding="utf-8") as temp_fb: 141 | template_content = temp_fb.read() 142 | 143 | # 将hosts内容转换为字符串 144 | hosts_str = "\n".join(hosts_content) 145 | 146 | # 使用替换方法而不是format 147 | readme_content = template_content.replace("{hosts_str}", hosts_str) 148 | readme_content = readme_content.replace("{update_time}", update_time) 149 | 150 | # 写入新文件 151 | with open("README.md", "w", encoding="utf-8") as output_fb: 152 | output_fb.write(readme_content) 153 | 154 | rprint( 155 | "[blue]已更新 README.md 文件,位于: [underline]README.md[/underline][/blue]\n" 156 | ) 157 | 158 | except FileNotFoundError as e: 159 | print(f"错误: {str(e)}") 160 | except Exception as e: 161 | print(f"生成 README.md 文件时发生错误: {str(e)}") 162 | 163 | def get_formatted_line(char="-", color="green", width_percentage=0.97): 164 | """ 165 | 生成格式化的分隔线 166 | 167 | 参数: 168 | char: 要重复的字符 169 | color: rich支持的颜色名称 170 | width_percentage: 终端宽度的百分比(0.0-1.0) 171 | """ 172 | # 获取终端宽度 173 | terminal_width = shutil.get_terminal_size().columns 174 | # 计算目标宽度(终端宽度的指定百分比) 175 | target_width = floor(terminal_width * width_percentage) 176 | 177 | # 生成重复字符 178 | line = char * target_width 179 | 180 | # 返回带颜色标记的行 181 | return f"[{color}]{line}[/{color}]" 182 | 183 | def get_formatted_output(text, fill_char=".", align_position=0.97): 184 | """ 185 | 格式化输出文本,确保不超出终端宽度 186 | 187 | 参数: 188 | text: 要格式化的文本 189 | fill_char: 填充字符 190 | align_position: 终端宽度的百分比(0.0-1.0) 191 | """ 192 | # 获取终端宽度并计算目标宽度 193 | terminal_width = shutil.get_terminal_size().columns 194 | target_width = floor(terminal_width * align_position) 195 | 196 | # 移除rich标记计算实际文本长度 197 | plain_text = ( 198 | text.replace("[blue on green]", "").replace("[/blue on green]", "") 199 | # .replace("[完成]", "") 200 | ) 201 | 202 | if "[完成]" in text: 203 | main_text = plain_text.strip() 204 | completion_mark = "[完成]" 205 | # 关键修改:直接从目标宽度减去主文本长度,不再额外预留[完成]的空间 206 | fill_count = target_width - len(main_text) - len(completion_mark) - 6 207 | fill_count = max(0, fill_count) 208 | 209 | filled_text = f"{main_text}{fill_char * fill_count}{completion_mark}" 210 | return f"[blue on green]{filled_text}[/blue on green]" 211 | else: 212 | # 普通文本的处理保持不变 213 | fill_count = target_width - len(plain_text.strip()) - 6 214 | fill_count = max(0, fill_count) 215 | filled_text = f"{plain_text.strip()}{' ' * fill_count}" 216 | return f"[blue on green]{filled_text}[/blue on green]" 217 | 218 | 219 | # -------------------- 域名与分组管理 -------------------- # 220 | class GroupType(Enum): 221 | SHARED = "shared hosts" # 多个域名共用一组DNS主机 IP 222 | SEPARATE = "separate hosts" # 每个域名独立拥有DNS主机 IP 223 | 224 | 225 | class DomainGroup: 226 | def __init__( 227 | self, 228 | name: str, 229 | domains: List[str], 230 | ips: Optional[Set[str]] = None, 231 | group_type: GroupType = GroupType.SHARED, 232 | ): 233 | self.name = name 234 | self.domains = domains if isinstance(domains, list) else [domains] 235 | self.ips = ips or set() 236 | self.group_type = group_type 237 | 238 | 239 | # -------------------- 域名解析模块 -------------------- # 240 | class DomainResolver: 241 | # 设置缓存过期时间为1周 242 | DNS_CACHE_EXPIRY_TIME = timedelta(weeks=1) 243 | 244 | def __init__(self, dns_servers: List[str], max_latency: int, dns_cache_file: str): 245 | self.dns_servers = dns_servers 246 | self.max_latency = max_latency 247 | self.dns_cache_file = Path(dns_cache_file) 248 | self.dns_records = self._init_dns_cache() 249 | 250 | def _init_dns_cache(self) -> dict: 251 | """初始化 DNS 缓存,如果缓存文件存在且未过期则加载,否则返回空字典""" 252 | if self._is_dns_cache_valid(): 253 | return self.load_hosts_cache() 254 | # 如果 DNS 缓存过期,删除旧缓存文件 255 | if self.dns_cache_file.exists(): 256 | self.dns_cache_file.unlink() 257 | return {} 258 | 259 | def _is_dns_cache_valid(self) -> bool: 260 | """检查 DNS 缓存是否有效""" 261 | if not self.dns_cache_file.exists(): 262 | return False 263 | 264 | file_age = datetime.now() - datetime.fromtimestamp( 265 | os.path.getmtime(self.dns_cache_file) 266 | ) 267 | return file_age <= self.DNS_CACHE_EXPIRY_TIME 268 | 269 | def load_hosts_cache(self) -> Dict[str, Dict]: 270 | try: 271 | with open(self.dns_cache_file, "r", encoding="utf-8") as f: 272 | return json.load(f) 273 | except Exception as e: 274 | logging.error(f"加载 DNS 缓存文件失败: {e}") 275 | return {} 276 | 277 | def save_hosts_cache(self): 278 | try: 279 | with open(self.dns_cache_file, "w", encoding="utf-8") as f: 280 | json.dump(self.dns_records, f, indent=4, ensure_ascii=False) 281 | logging.debug(f"成功保存 DNS 缓存到文件 {self.dns_cache_file}") 282 | except Exception as e: 283 | logging.error(f"保存 DNS 缓存到文件时发生错误: {e}") 284 | 285 | async def resolve_domain(self, domain: str) -> Set[str]: 286 | ips = set() 287 | 288 | # 1. 首先通过常规DNS服务器解析 289 | if not args.NotUseDnsServers: 290 | dns_ips = await self._resolve_via_dns(domain) 291 | ips.update(dns_ips) 292 | 293 | # # 2. 然后通过DNS_records解析 294 | # # 由于init时已经处理了过期文件,这里只需要检查域名是否在缓存中 295 | # if domain in self.dns_records: 296 | # domain_hosts = self.dns_records.get(domain, {}) 297 | # ipv4_ips = domain_hosts.get("ipv4", []) 298 | # ipv6_ips = domain_hosts.get("ipv6", []) 299 | 300 | # ips.update(ipv4_ips + ipv6_ips) 301 | # else: 302 | # ipaddress_ips = await self._resolve_via_ipaddress(domain) 303 | # ips.update(ipaddress_ips) 304 | 305 | # if ips: 306 | # logging.debug(f"成功解析 {domain}, 发现 {len(ips)} 个 DNS 主机") 307 | # else: 308 | # logging.debug(f"警告: 无法解析 {domain}") 309 | 310 | return ips 311 | 312 | async def _resolve_via_dns(self, domain: str) -> Set[str]: 313 | ips = set() 314 | for dns_server in self.dns_servers: 315 | try: 316 | resolver = dns.resolver.Resolver() 317 | resolver.nameservers = [dns_server] 318 | resolver.lifetime = RESOLVER_TIMEOUT 319 | 320 | for qtype in ["A", "AAAA"]: 321 | try: 322 | answers = await asyncio.to_thread( 323 | resolver.resolve, domain, qtype 324 | ) 325 | ips.update(answer.address for answer in answers) 326 | except dns.resolver.NoAnswer: 327 | pass 328 | 329 | if ips: 330 | logging.debug(f"成功使用 {dns_server} 解析 {domain}") 331 | logging.debug(f"DNS_resolver:\n {ips}") 332 | return ips 333 | except Exception as e: 334 | logging.debug(f"使用 {dns_server} 解析 {domain} 失败: {e}") 335 | 336 | return ips 337 | 338 | def retry_async(tries=3, delay=1): 339 | def decorator(func): 340 | @wraps(func) 341 | async def wrapper(*args, **kwargs): 342 | for attempt in range(tries): 343 | try: 344 | return await func(*args, **kwargs) 345 | except Exception as e: 346 | if attempt < tries - 1: 347 | logging.debug( 348 | f"通过DNS_records解析 {args[1]},第 {attempt + 2} 次尝试:" 349 | ) 350 | if attempt == tries - 1: 351 | logging.debug( 352 | f"通过DNS_records解析 {args[1]},{tries} 次尝试后终止!" 353 | ) 354 | raise e 355 | await asyncio.sleep(delay) 356 | return None 357 | 358 | return wrapper 359 | 360 | return decorator 361 | 362 | @retry_async(tries=3) 363 | async def _resolve_via_ipaddress(self, domain: str) -> Set[str]: 364 | ips = set() 365 | url = f"https://sites.ipaddress.com/{domain}" 366 | headers = { 367 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " 368 | "AppleWebKit/537.36 (KHTML, like Gecko) " 369 | "Chrome/106.0.0.0 Safari/537.36" 370 | } 371 | 372 | try: 373 | async with aiohttp.ClientSession() as session: 374 | async with session.get(url, headers=headers, timeout=5) as response: 375 | if response.status != 200: 376 | logging.info( 377 | f"DNS_records(ipaddress.com) 查询请求失败: {response.status}" 378 | ) 379 | return ips 380 | 381 | content = await response.text() 382 | # 匹配IPv4地址 383 | ipv4_pattern = r"\b(?:[0-9]{1,3}\.){3}[0-9]{1,3}\b" 384 | ipv4_ips = set(re.findall(ipv4_pattern, content)) 385 | 386 | # 匹配IPv6地址 387 | ipv6_pattern = r"(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}" 388 | ipv6_ips = set(re.findall(ipv6_pattern, content)) 389 | 390 | ips.update(ipv4_ips) 391 | ips.update(ipv6_ips) 392 | 393 | if ips: 394 | # 更新hosts缓存 395 | current_time = datetime.now().isoformat() 396 | self.dns_records[domain] = { 397 | "last_update": current_time, 398 | "ipv4": list(ipv4_ips), 399 | "ipv6": list(ipv6_ips), 400 | "source": "DNS_records", 401 | } 402 | # 保存到文件 403 | self.save_hosts_cache() 404 | logging.debug( 405 | f"通过 ipaddress.com 成功解析 {domain} 并更新 DNS_records 缓存" 406 | ) 407 | logging.debug(f"DNS_records:\n {ips}") 408 | else: 409 | logging.debug( 410 | f"ipaddress.com 未解析到 {domain} 的 DNS_records 地址" 411 | ) 412 | 413 | except Exception as e: 414 | logging.error(f"通过DNS_records解析 {domain} 失败: {e}") 415 | raise 416 | 417 | return ips 418 | 419 | 420 | # -------------------- 延迟测速模块 -------------------- # 421 | 422 | 423 | class LatencyTester: 424 | def __init__(self, hosts_num: int): 425 | self.hosts_num = hosts_num 426 | self.progress = None 427 | self.current_task = None 428 | 429 | def set_progress(self, progress, task): 430 | """设置进度显示器和当前任务""" 431 | self.progress = progress 432 | self.current_task = task 433 | 434 | async def get_latency(self, ip: str, port: int = 443) -> float: 435 | try: 436 | # 使用 getaddrinfo 来获取正确的地址格式 437 | addrinfo = await asyncio.get_event_loop().getaddrinfo( 438 | ip, port, family=socket.AF_UNSPEC, type=socket.SOCK_STREAM 439 | ) 440 | 441 | for family, type, proto, canonname, sockaddr in addrinfo: 442 | try: 443 | start = asyncio.get_event_loop().time() 444 | _, writer = await asyncio.wait_for( 445 | asyncio.open_connection(sockaddr[0], sockaddr[1]), 446 | timeout=PING_TIMEOUT, 447 | ) 448 | end = asyncio.get_event_loop().time() 449 | writer.close() 450 | await writer.wait_closed() 451 | return (end - start) * 1000 452 | except asyncio.TimeoutError: 453 | continue 454 | except Exception as e: 455 | logging.debug(f"连接测试失败 {ip} (sockaddr: {sockaddr}): {e}") 456 | continue 457 | 458 | return float("inf") 459 | except Exception as e: 460 | logging.error(f"获取地址信息失败 {ip}: {e}") 461 | return float("inf") 462 | 463 | async def get_host_average_latency( 464 | self, ip: str, port: int = 443 465 | ) -> Tuple[str, float]: 466 | try: 467 | response_times = await asyncio.gather( 468 | *[self.get_latency(ip, port) for _ in range(NUM_PINGS)] 469 | ) 470 | response_times = [t for t in response_times if t != float("inf")] 471 | if response_times: 472 | average_response_time = sum(response_times) / len(response_times) 473 | else: 474 | average_response_time = float("inf") 475 | 476 | if average_response_time == 0: 477 | logging.error(f"{ip} 平均延迟为 0 ms,视为无效") 478 | return ip, float("inf") 479 | 480 | if self.progress and self.current_task: 481 | self.progress.update(self.current_task, advance=1) 482 | 483 | logging.debug(f"{ip} 平均延迟: {average_response_time:.2f} ms") 484 | return ip, average_response_time 485 | except Exception as e: 486 | logging.debug(f"ping {ip} 时出错: {e}") 487 | return ip, float("inf") 488 | 489 | async def is_cert_valid(self, domain: str, ip: str, port: int = 443) -> bool: 490 | 491 | # 设置SSL上下文,用于证书验证 492 | context = ssl.create_default_context() 493 | context.verify_mode = ssl.CERT_REQUIRED # 验证服务器证书 494 | context.check_hostname = True # 确保证书主机名匹配 495 | 496 | try: 497 | # 1. 尝试与IP地址建立SSL连接 498 | with socket.create_connection((ip, port), timeout=5) as sock: 499 | with context.wrap_socket(sock, server_hostname=domain) as ssock: 500 | cert = ssock.getpeercert() 501 | # 检查证书的有效期 502 | not_after = datetime.strptime( 503 | cert["notAfter"], "%b %d %H:%M:%S %Y %Z" 504 | ) 505 | if not_after < datetime.now(): 506 | logging.debug(f"{domain} ({ip}): 证书已过期") 507 | return False 508 | 509 | # 验证证书域名(由context自动完成),同时获取连接状态 510 | logging.debug( 511 | f"{domain} ({ip}): SSL证书有效,截止日期为 {not_after}" 512 | ) 513 | return True 514 | 515 | except ssl.SSLError as e: 516 | logging.debug(f"{domain} ({ip}): SSL错误 - {e}") 517 | return False 518 | except socket.timeout as e: 519 | logging.debug(f"{domain} ({ip}): 连接超时 - {e}") 520 | return False 521 | except ConnectionError as e: 522 | logging.debug(f"{domain} ({ip}): 连接被强迫关闭,ip有效 - {e}") 523 | return True 524 | except Exception as e: 525 | logging.error(f"{domain} ({ip}): 其他错误 - {e}") 526 | return False 527 | 528 | async def get_lowest_latency_hosts( 529 | self, 530 | group_name: str, 531 | domains: List[str], 532 | file_ips: Set[str], 533 | latency_limit: int, 534 | latency_task_id: TaskID, 535 | ) -> List[Tuple[str, float]]: 536 | all_ips = file_ips 537 | total_ips = len(all_ips) 538 | 539 | # 更新进度条描述和总数 540 | if self.progress and latency_task_id: 541 | self.progress.update( 542 | latency_task_id, 543 | total=total_ips, 544 | visible=False, 545 | ) 546 | if args.verbose: 547 | rprint( 548 | f"[bright_black]- [{group_name}] {domains[0] if len(domains) == 1 else f'{len(domains)} 域名'} 解析到 [bold bright_green]{len(all_ips):2}[/bold bright_green] 个唯一IP地址 [{group_name}][/bright_black]" 549 | ) 550 | 551 | # Ping所有IP 552 | ping_tasks = [self.get_host_average_latency(ip) for ip in all_ips] 553 | 554 | results = [] 555 | # 使用 asyncio.as_completed 确保每个任务完成时立即处理 556 | for coro in asyncio.as_completed(ping_tasks): 557 | result = await coro 558 | results.append(result) 559 | 560 | # 每完成一个任务立即更新进度 561 | if self.progress and latency_task_id: 562 | self.progress.update( 563 | latency_task_id, 564 | advance=1, 565 | visible=True, 566 | total=total_ips, 567 | ) 568 | 569 | if self.progress and latency_task_id: 570 | # 确保进度完结 571 | self.progress.update( 572 | latency_task_id, 573 | completed=total_ips, 574 | visible=True, 575 | ) 576 | 577 | results = [result for result in results if result[1] != float("inf")] 578 | valid_results = [] 579 | 580 | if results: 581 | valid_results = [result for result in results if result[1] < latency_limit] 582 | if not valid_results: 583 | logging.debug( 584 | f"{group_name} {domains[0] if len(domains) == 1 else f'{len(domains)} 域名'} 未发现延迟小于 {latency_limit}ms 的IP。" 585 | ) 586 | 587 | valid_results = [min(results, key=lambda x: x[1])] 588 | latency_limit = valid_results[0][1] 589 | logging.debug( 590 | f"{group_name} {domains[0] if len(domains) == 1 else f'{len(domains)} 域名'} 的主机IP最低延迟{latency_limit}ms" 591 | ) 592 | 593 | else: 594 | rprint( 595 | f"[red]{group_name} {domains[0] if len(domains) == 1 else f'{len(domains)} 域名'} 延迟检测没有获得有效IP[/red]" 596 | ) 597 | return [] 598 | 599 | # 排序结果 600 | valid_results = sorted(valid_results, key=lambda x: x[1]) 601 | 602 | ipv4_results = [r for r in valid_results if not Utils.is_ipv6(r[0])] 603 | ipv6_results = [r for r in valid_results if Utils.is_ipv6(r[0])] 604 | 605 | best_hosts = [] 606 | selected_count = 0 607 | 608 | # 检测 IPv4 证书有效性 609 | for ip, latency in ipv4_results: 610 | if await self.is_cert_valid( 611 | domains[0], ip 612 | ): # shareGroup会传入多个域名,只需检测第一个就行 613 | best_hosts.append((ip, latency)) 614 | selected_count += 1 615 | if ipv6_results or selected_count >= self.hosts_num: 616 | break 617 | 618 | # 检测 IPv6 证书有效性 619 | if ipv6_results: 620 | for ip, latency in ipv6_results: 621 | if await self.is_cert_valid(domains[0], ip): 622 | best_hosts.append((ip, latency)) 623 | break 624 | 625 | if args.verbose: 626 | rprint( 627 | f"[bold yellow]最快DNS主机 {'(IPv4/IPv6)' if ipv6_results else '(IPv4 Only)'} 延迟 < {latency_limit:.0f}ms | [{group_name}] " 628 | f"{domains[0] if len(domains) == 1 else f'{len(domains)} 域名合用 IP'}:[/bold yellow]" 629 | ) 630 | 631 | for ip, time in best_hosts: 632 | rprint( 633 | f" [green]{ip}[/green] [bright_black]{time:.2f} ms[/bright_black]" 634 | ) 635 | 636 | return best_hosts 637 | 638 | 639 | # -------------------- Hosts文件管理 -------------------- # 640 | class HostsManager: 641 | def __init__(self): 642 | # 自动根据操作系统获取hosts文件路径 643 | self.hosts_file_path = self._get_hosts_file_path() 644 | 645 | @staticmethod 646 | def _get_hosts_file_path() -> str: 647 | """根据操作系统自动获取 hosts 文件路径。""" 648 | return Utils.get_hosts_file_path() 649 | 650 | def write_to_hosts_file(self, new_entries: List[str]): 651 | Utils.backup_hosts_file(self.hosts_file_path) 652 | 653 | with open(self.hosts_file_path, "r") as f: 654 | existing_content = f.read().splitlines() 655 | 656 | new_domains = { 657 | entry.split()[1] for entry in new_entries if len(entry.split()) >= 2 658 | } 659 | 660 | new_content = [] 661 | skip = False 662 | skip_tags = ("# cnNetTool", "# Update", "# Star", "# GitHub") 663 | 664 | for line in existing_content: 665 | line = line.strip() 666 | 667 | # 跳过标记块 668 | if any(line.startswith(tag) for tag in skip_tags): 669 | skip = True 670 | 671 | if line == "": 672 | skip = True 673 | 674 | if skip: 675 | if line == "" or line.startswith("#"): 676 | continue 677 | skip = False 678 | 679 | # 非标记块内容保留 680 | if ( 681 | not skip 682 | and (line.startswith("#") or not line) 683 | and not any(tag in line for tag in skip_tags) 684 | ): 685 | new_content.append(line) 686 | continue 687 | 688 | # 检查域名是否为新条目 689 | parts = line.split() 690 | if len(parts) >= 2 and parts[1] not in new_domains: 691 | new_content.append(line) 692 | else: 693 | logging.debug(f"删除旧条目: {line}") 694 | 695 | update_time = ( 696 | datetime.now(timezone.utc) 697 | .astimezone(timezone(timedelta(hours=8))) 698 | .strftime("%Y-%m-%d %H:%M:%S %z") 699 | .replace("+0800", "+08:00") 700 | ) 701 | 702 | rprint("\n[bold yellow]正在更新 hosts 文件...[/bold yellow]") 703 | 704 | save_hosts_content = [] # 提取新内容文本 705 | 706 | # 1. 添加标题 707 | new_content.append(f"\n# cnNetTool Start in {update_time}") 708 | save_hosts_content.append(f"\n# cnNetTool Start in {update_time}") 709 | 710 | # 2. 添加主机条目 711 | for entry in new_entries: 712 | # 分割 IP 和域名 713 | ip, domain = entry.strip().split(maxsplit=1) 714 | 715 | # 计算需要的制表符数量 716 | # IP 地址最长可能是 39 个字符 (IPv6) 717 | # 我们使用制表符(8个空格)来对齐,确保视觉上的整齐 718 | ip_length = len(ip) 719 | if ip_length <= 8: 720 | tabs = "\t\t\t" # 两个制表符 721 | if ip_length <= 10: 722 | tabs = "\t\t" # 两个制表符 723 | elif ip_length <= 16: 724 | tabs = "\t" # 一个制表符 725 | else: 726 | tabs = "\t" # 对于很长的IP,只使用一个空格 727 | 728 | # 返回格式化后的条目 729 | formatedEntry = f"{ip}{tabs}{domain}" 730 | 731 | new_content.append(formatedEntry) 732 | save_hosts_content.append(formatedEntry) 733 | rprint(f"+ {formatedEntry}") 734 | 735 | # 3. 添加项目描述 736 | new_content.extend( 737 | [ 738 | f"\n# Update time: {update_time}", 739 | "# GitHub仓库: https://github.com/sinspired/cnNetTool", 740 | "# cnNetTool End\n", 741 | ] 742 | ) 743 | save_hosts_content.extend( 744 | [ 745 | f"\n# Update time: {update_time}", 746 | "# GitHub仓库: https://github.com/sinspired/cnNetTool", 747 | "# cnNetTool End\n", 748 | ] 749 | ) 750 | 751 | # 4. 写入hosts文件 752 | with open(self.hosts_file_path, "w") as f: 753 | f.write("\n".join(new_content)) 754 | 755 | # 保存 hosts 文本 756 | with open("hosts", "w") as f: 757 | f.write("\n".join(save_hosts_content)) 758 | rprint( 759 | f"\n[blue]已生成 hosts 文件,位于: [underline]hosts[/underline][/blue] (共 {len(new_entries)} 个条目)" 760 | ) 761 | 762 | if not getattr(sys, "frozen", False): 763 | # 如果未打包为可执行程序 764 | Utils.write_readme_file( 765 | save_hosts_content, "README_template.md", f"{update_time}" 766 | ) 767 | 768 | 769 | # -------------------- 主控制模块 -------------------- # 770 | class HostsUpdater: 771 | def __init__( 772 | self, 773 | domain_groups: List[DomainGroup], 774 | resolver: DomainResolver, 775 | tester: LatencyTester, 776 | hosts_manager: HostsManager, 777 | ): 778 | self.domain_groups = domain_groups 779 | self.resolver = resolver 780 | self.tester = tester 781 | self.hosts_manager = hosts_manager 782 | # 添加并发限制 783 | self.semaphore = asyncio.Semaphore(200) # 限制并发请求数 784 | 785 | # 添加进度显示实例 786 | self.progress = Progress( 787 | "[progress.description]{task.description}", 788 | BarColumn(), 789 | "[progress.percentage]{task.percentage:>3.0f}%", 790 | TimeRemainingColumn(), 791 | ) 792 | 793 | async def _resolve_domains_batch( 794 | self, domains: List[str], resolve_task_id: TaskID 795 | ) -> Dict[str, Set[str]]: 796 | """批量解析域名,带进度更新""" 797 | results = {} 798 | total_domains = len(domains) 799 | 800 | # 更新进度条描述和总数 801 | if self.progress and resolve_task_id: 802 | self.progress.update( 803 | resolve_task_id, 804 | total=total_domains, 805 | ) 806 | 807 | async with self.semaphore: 808 | for i, domain in enumerate(domains, 1): 809 | try: 810 | ips = await self.resolver.resolve_domain(domain) 811 | results[domain] = ips 812 | except Exception as e: 813 | logging.error(f"解析域名 {domain} 失败: {e}") 814 | results[domain] = set() 815 | 816 | # 更新进度 817 | self.progress.update( 818 | resolve_task_id, 819 | advance=1, 820 | visible=True, 821 | ) 822 | 823 | if self.progress and resolve_task_id: 824 | # 确保进度完结 825 | self.progress.update( 826 | resolve_task_id, 827 | completed=total_domains, 828 | visible=True, 829 | ) 830 | return results 831 | 832 | async def _process_domain_group(self, group: DomainGroup, index: int) -> List[str]: 833 | """处理单个域名组""" 834 | entries = [] 835 | all_ips = group.ips.copy() 836 | 837 | # 创建 seperateGroup 的主进度任务 838 | seperateGroup_task_id = self.progress.add_task( 839 | f"处理组 {group.name}", 840 | total=len(group.domains), 841 | visible=False, 842 | ) 843 | 844 | # 创建 shareGroup 的主进度任务 845 | shareGroup_task_id = self.progress.add_task( 846 | f"处理组 {group.name}", 847 | total=100, 848 | visible=False, 849 | ) 850 | 851 | # 为 _resolve_domains_batch 设置 [域名解析] 子任务进度显示 852 | resolve_task_id = self.progress.add_task( 853 | f"- [域名解析] {group.name}", 854 | total=0, # 初始设为0,后续会更新 855 | visible=False, # 初始隐藏,等需要时显示 856 | ) 857 | 858 | # 为 LatencyTester 设置子任务进度显示 859 | latency_task_id = self.progress.add_task( 860 | f"- [测试延迟] {group.name}", 861 | total=0, # 初始设为0,后续会更新 862 | visible=False, # 初始隐藏,等需要时显示 863 | ) 864 | 865 | self.tester.set_progress(self.progress, latency_task_id) 866 | 867 | if group.group_type == GroupType.SEPARATE: 868 | for domain in group.domains: 869 | resolved_ips = await self._resolve_domains_batch( 870 | [domain], resolve_task_id 871 | ) 872 | domain_ips = resolved_ips.get(domain, set()) 873 | 874 | # 隐藏域名解析进度条 875 | self.progress.update(resolve_task_id, visible=False) 876 | 877 | if not domain_ips: 878 | logging.warning(f"{domain} 未解析到任何可用IP。跳过该域名。") 879 | continue 880 | 881 | fastest_ips = await self.tester.get_lowest_latency_hosts( 882 | group.name, 883 | [domain], 884 | domain_ips, 885 | self.resolver.max_latency, 886 | latency_task_id, 887 | ) 888 | if fastest_ips: 889 | entries.extend(f"{ip}\t{domain}" for ip, latency in fastest_ips) 890 | else: 891 | logging.warning(f"{domain} 未发现满足延迟检测要求的IP。") 892 | # 隐藏延迟测试进度条 893 | self.progress.update(latency_task_id, visible=False) 894 | # 主进度更新 895 | self.progress.update( 896 | seperateGroup_task_id, 897 | advance=1, 898 | visible=True, 899 | ) 900 | 901 | self.progress.update( 902 | seperateGroup_task_id, 903 | visible=False, 904 | ) 905 | 906 | # 标记该组处理完成 907 | self.progress.update( 908 | seperateGroup_task_id, 909 | description=f"处理组 {group.name}", 910 | completed=len(group.domains), 911 | visible=True, 912 | ) 913 | 914 | else: 915 | # 共用主机的域名组 916 | resolved_ips_dict = await self._resolve_domains_batch( 917 | group.domains, resolve_task_id 918 | ) 919 | # 隐藏域名解析进度条 920 | self.progress.update(resolve_task_id, visible=False) 921 | self.progress.update( 922 | shareGroup_task_id, 923 | visible=True, 924 | advance=40, 925 | ) 926 | 927 | for ips in resolved_ips_dict.values(): 928 | all_ips.update(ips) 929 | 930 | if not all_ips: 931 | logging.warning(f"组 {group.name} 未解析到任何可用IP。跳过该组。") 932 | return entries 933 | 934 | logging.debug(f"组 {group.name} 解析到 {len(all_ips)} 个 DNS 主机记录") 935 | 936 | fastest_ips = await self.tester.get_lowest_latency_hosts( 937 | group.name, 938 | group.domains, 939 | all_ips, 940 | self.resolver.max_latency, 941 | latency_task_id, 942 | ) 943 | self.progress.update( 944 | shareGroup_task_id, 945 | visible=True, 946 | advance=40, 947 | ) 948 | 949 | # 隐藏延迟测试进度条 950 | self.progress.update(latency_task_id, visible=False) 951 | 952 | if fastest_ips: 953 | for domain in group.domains: 954 | entries.extend(f"{ip}\t{domain}" for ip, latency in fastest_ips) 955 | # logging.info(f"已处理域名: {domain}") 956 | else: 957 | logging.warning(f"组 {group.name} 未发现满足延迟检测要求的IP。") 958 | 959 | self.progress.update( 960 | shareGroup_task_id, 961 | visible=True, 962 | advance=20, 963 | ) 964 | 965 | return entries 966 | 967 | async def update_hosts(self): 968 | """主更新函数,支持并发进度显示""" 969 | 970 | with self.progress: 971 | # 并发处理所有组 972 | tasks = [ 973 | self._process_domain_group(group, i) 974 | for i, group in enumerate(self.domain_groups, 1) 975 | ] 976 | 977 | all_entries_lists = await asyncio.gather(*tasks) 978 | all_entries = [entry for entries in all_entries_lists for entry in entries] 979 | 980 | if all_entries: 981 | self.hosts_manager.write_to_hosts_file(all_entries) 982 | rprint(Utils.get_formatted_output("Hosts文件更新[完成]")) 983 | else: 984 | logging.warning("没有有效条目可写入") 985 | rprint("[bold red]警告: 没有有效条目可写入。hosts文件未更新。[/bold red]") 986 | 987 | 988 | # -------------------- 权限提升模块-------------------- # 989 | class PrivilegeManager: 990 | @staticmethod 991 | def is_admin() -> bool: 992 | try: 993 | return os.getuid() == 0 994 | except AttributeError: 995 | return ctypes.windll.shell32.IsUserAnAdmin() != 0 996 | 997 | @staticmethod 998 | def run_as_admin(): 999 | if PrivilegeManager.is_admin(): 1000 | return 1001 | 1002 | if sys.platform.startswith("win"): 1003 | script = os.path.abspath(sys.argv[0]) 1004 | params = " ".join([script] + sys.argv[1:]) 1005 | ctypes.windll.shell32.ShellExecuteW( 1006 | None, "runas", sys.executable, params, None, 1 1007 | ) 1008 | else: 1009 | os.execvp("sudo", ["sudo", "python3"] + sys.argv) 1010 | sys.exit(0) 1011 | 1012 | 1013 | # -------------------- 数据配置模块-------------------- # 1014 | 1015 | 1016 | class Config: 1017 | DOMAIN_GROUPS = [ 1018 | DomainGroup( 1019 | name="GitHub Services", 1020 | group_type=GroupType.SEPARATE, 1021 | domains=[ 1022 | "alive.github.com", 1023 | "api.github.com", 1024 | "central.github.com", 1025 | "codeload.github.com", 1026 | "collector.github.com", 1027 | "gist.github.com", 1028 | "github.com", 1029 | "github.community", 1030 | "github.global.ssl.fastly.net", 1031 | "github-com.s3.amazonaws.com", 1032 | "github-production-release-asset-2e65be.s3.amazonaws.com", 1033 | "live.github.com", 1034 | "pipelines.actions.githubusercontent.com", 1035 | "github.githubassets.com", 1036 | ], 1037 | ips={}, 1038 | ), 1039 | DomainGroup( 1040 | name="GitHub Asset", 1041 | group_type=GroupType.SHARED, 1042 | domains=[ 1043 | "github.io", 1044 | "githubstatus.com", 1045 | "assets-cdn.github.com", 1046 | ], 1047 | ips={}, 1048 | ), 1049 | DomainGroup( 1050 | name="GitHub Static", 1051 | group_type=GroupType.SHARED, 1052 | domains=[ 1053 | "avatars.githubusercontent.com", 1054 | "avatars0.githubusercontent.com", 1055 | "avatars1.githubusercontent.com", 1056 | "avatars2.githubusercontent.com", 1057 | "avatars3.githubusercontent.com", 1058 | "avatars4.githubusercontent.com", 1059 | "avatars5.githubusercontent.com", 1060 | "camo.githubusercontent.com", 1061 | "cloud.githubusercontent.com", 1062 | "desktop.githubusercontent.com", 1063 | "favicons.githubusercontent.com", 1064 | "github.map.fastly.net", 1065 | "media.githubusercontent.com", 1066 | "objects.githubusercontent.com", 1067 | "private-user-images.githubusercontent.com", 1068 | "raw.githubusercontent.com", 1069 | "user-images.githubusercontent.com", 1070 | ], 1071 | ips={}, 1072 | ), 1073 | DomainGroup( 1074 | name="TMDB API", 1075 | domains=[ 1076 | "tmdb.org", 1077 | "api.tmdb.org", 1078 | "files.tmdb.org", 1079 | ], 1080 | ips={}, 1081 | ), 1082 | DomainGroup( 1083 | name="THE MOVIEDB", 1084 | domains=[ 1085 | "themoviedb.org", 1086 | "api.themoviedb.org", 1087 | "www.themoviedb.org", 1088 | "auth.themoviedb.org", 1089 | ], 1090 | ips={}, 1091 | ), 1092 | DomainGroup( 1093 | name="TMDB 封面", 1094 | domains=["image.tmdb.org", "images.tmdb.org"], 1095 | ips={}, 1096 | ), 1097 | DomainGroup( 1098 | name="IMDB 网页", 1099 | group_type=GroupType.SEPARATE, 1100 | domains=[ 1101 | "imdb.com", 1102 | "www.imdb.com", 1103 | "secure.imdb.com", 1104 | "s.media-imdb.com", 1105 | "us.dd.imdb.com", 1106 | "www.imdb.to", 1107 | "imdb-webservice.amazon.com", 1108 | "origin-www.imdb.com", 1109 | ], 1110 | ips={}, 1111 | ), 1112 | DomainGroup( 1113 | name="IMDB CDN", 1114 | group_type=GroupType.SEPARATE, 1115 | domains=[ 1116 | "m.media-amazon.com", 1117 | "Images-na.ssl-images-amazon.com", 1118 | "images-fe.ssl-images-amazon.com", 1119 | "images-eu.ssl-images-amazon.com", 1120 | "ia.media-imdb.com", 1121 | "f.media-amazon.com", 1122 | "imdb-video.media-imdb.com", 1123 | "dqpnq362acqdi.cloudfront.net", 1124 | ], 1125 | ips={}, 1126 | ), 1127 | DomainGroup( 1128 | name="Google 翻译", 1129 | domains=[ 1130 | "translate.google.com", 1131 | "translate.googleapis.com", 1132 | "translate-pa.googleapis.com", 1133 | ], 1134 | ips={ 1135 | "108.177.127.214", 1136 | "108.177.97.141", 1137 | "142.250.101.157", 1138 | "142.250.110.102", 1139 | "142.250.141.100", 1140 | "142.250.145.113", 1141 | "142.250.145.139", 1142 | "142.250.157.133", 1143 | "142.250.157.149", 1144 | "142.250.176.6", 1145 | "142.250.181.232", 1146 | "142.250.183.106", 1147 | "142.250.187.139", 1148 | "142.250.189.6", 1149 | "142.250.196.174", 1150 | "142.250.199.161", 1151 | "142.250.199.75", 1152 | "142.250.204.37", 1153 | "142.250.204.38", 1154 | "142.250.204.49", 1155 | "142.250.27.113", 1156 | "142.250.4.136", 1157 | "142.250.66.10", 1158 | "142.250.76.35", 1159 | "142.251.1.102", 1160 | "142.251.1.136", 1161 | "142.251.163.91", 1162 | "142.251.165.101", 1163 | "142.251.165.104", 1164 | "142.251.165.106", 1165 | "142.251.165.107", 1166 | "142.251.165.110", 1167 | "142.251.165.112", 1168 | "142.251.165.122", 1169 | "142.251.165.133", 1170 | "142.251.165.139", 1171 | "142.251.165.146", 1172 | "142.251.165.152", 1173 | "142.251.165.155", 1174 | "142.251.165.164", 1175 | "142.251.165.165", 1176 | "142.251.165.193", 1177 | "142.251.165.195", 1178 | "142.251.165.197", 1179 | "142.251.165.201", 1180 | "142.251.165.82", 1181 | "142.251.165.94", 1182 | "142.251.178.105", 1183 | "142.251.178.110", 1184 | "142.251.178.114", 1185 | "142.251.178.117", 1186 | "142.251.178.122", 1187 | "142.251.178.137", 1188 | "142.251.178.146", 1189 | "142.251.178.164", 1190 | "142.251.178.166", 1191 | "142.251.178.181", 1192 | "142.251.178.190", 1193 | "142.251.178.195", 1194 | "142.251.178.197", 1195 | "142.251.178.199", 1196 | "142.251.178.200", 1197 | "142.251.178.214", 1198 | "142.251.178.83", 1199 | "142.251.178.84", 1200 | "142.251.178.88", 1201 | "142.251.178.92", 1202 | "142.251.178.99", 1203 | "142.251.2.139", 1204 | "142.251.221.121", 1205 | "142.251.221.129", 1206 | "142.251.221.138", 1207 | "142.251.221.98", 1208 | "142.251.40.104", 1209 | "142.251.41.14", 1210 | "142.251.41.36", 1211 | "142.251.42.197", 1212 | "142.251.8.155", 1213 | "142.251.8.189", 1214 | "172.217.16.210", 1215 | "172.217.164.103", 1216 | "172.217.168.203", 1217 | "172.217.168.215", 1218 | "172.217.168.227", 1219 | "172.217.169.138", 1220 | "172.217.17.104", 1221 | "172.217.171.228", 1222 | "172.217.175.23", 1223 | "172.217.19.72", 1224 | "172.217.192.149", 1225 | "172.217.192.92", 1226 | "172.217.197.156", 1227 | "172.217.197.91", 1228 | "172.217.204.104", 1229 | "172.217.204.156", 1230 | "172.217.214.112", 1231 | "172.217.218.133", 1232 | "172.217.222.92", 1233 | "172.217.31.136", 1234 | "172.217.31.142", 1235 | "172.217.31.163", 1236 | "172.217.31.168", 1237 | "172.217.31.174", 1238 | "172.253.117.118", 1239 | "172.253.122.154", 1240 | "172.253.62.88", 1241 | "173.194.199.94", 1242 | "173.194.216.102", 1243 | "173.194.220.101", 1244 | "173.194.220.138", 1245 | "173.194.221.101", 1246 | "173.194.222.106", 1247 | "173.194.222.138", 1248 | "173.194.66.137", 1249 | "173.194.67.101", 1250 | "173.194.68.97", 1251 | "173.194.73.106", 1252 | "173.194.73.189", 1253 | "173.194.76.107", 1254 | "173.194.77.81", 1255 | "173.194.79.200", 1256 | "209.85.201.155", 1257 | "209.85.201.198", 1258 | "209.85.201.201", 1259 | "209.85.203.198", 1260 | "209.85.232.101", 1261 | "209.85.232.110", 1262 | "209.85.232.133", 1263 | "209.85.232.195", 1264 | "209.85.233.100", 1265 | "209.85.233.102", 1266 | "209.85.233.105", 1267 | "209.85.233.136", 1268 | "209.85.233.191", 1269 | "209.85.233.93", 1270 | "216.239.32.40", 1271 | "216.58.200.10", 1272 | "216.58.213.8", 1273 | "34.105.140.105", 1274 | "34.128.8.104", 1275 | "34.128.8.40", 1276 | "34.128.8.55", 1277 | "34.128.8.64", 1278 | "34.128.8.70", 1279 | "34.128.8.71", 1280 | "34.128.8.85", 1281 | "34.128.8.97", 1282 | "35.196.72.166", 1283 | "35.228.152.85", 1284 | "35.228.168.221", 1285 | "35.228.195.190", 1286 | "35.228.40.236", 1287 | "64.233.162.102", 1288 | "64.233.163.97", 1289 | "64.233.165.132", 1290 | "64.233.165.97", 1291 | "64.233.169.100", 1292 | "64.233.188.155", 1293 | "64.233.189.133", 1294 | "64.233.189.148", 1295 | "66.102.1.167", 1296 | "66.102.1.88", 1297 | "74.125.133.155", 1298 | "74.125.135.17", 1299 | "74.125.139.97", 1300 | "74.125.142.116", 1301 | "74.125.193.152", 1302 | "74.125.196.195", 1303 | "74.125.201.91", 1304 | "74.125.204.101", 1305 | "74.125.204.113", 1306 | "74.125.204.114", 1307 | "74.125.204.132", 1308 | "74.125.204.141", 1309 | "74.125.204.147", 1310 | "74.125.206.117", 1311 | "74.125.206.137", 1312 | "74.125.206.144", 1313 | "74.125.206.146", 1314 | "74.125.206.154", 1315 | "74.125.21.191", 1316 | "74.125.71.145", 1317 | "74.125.71.152", 1318 | "74.125.71.199", 1319 | "2404:6800:4008:c13::5a", 1320 | "2404:6800:4008:c15::94", 1321 | "2607:f8b0:4004:c07::66", 1322 | "2607:f8b0:4004:c07::71", 1323 | "2607:f8b0:4004:c07::8a", 1324 | "2607:f8b0:4004:c07::8b", 1325 | "2a00:1450:4001:829::201a", 1326 | "2001:67c:2960:6464::d8ef:2076", 1327 | "2001:67c:2960:6464::d8ef:2039", 1328 | "2001:67c:2960:6464::d8ef:2038", 1329 | "2001:67c:2960:6464::d8ef:2028", 1330 | "2001:67c:2960:6464::d8ef:2006", 1331 | "185.199.109.133", 1332 | "185.199.110.133", 1333 | "185.199.111.133", 1334 | "172.217.70.133", 1335 | "142.251.42.55", 1336 | "142.251.43.87", 1337 | "142.251.43.75", 1338 | "142.251.43.139", 1339 | }, 1340 | ), 1341 | DomainGroup( 1342 | name="JetBrain 插件", 1343 | domains=[ 1344 | "plugins.jetbrains.com", 1345 | "download.jetbrains.com", 1346 | "cache-redirector.jetbrains.com", 1347 | ], 1348 | ips={}, 1349 | ), 1350 | ] 1351 | 1352 | # DNS 服务器 1353 | DNS_SERVERS = [ 1354 | "2402:4e00::", # DNSPod (IPv6) 1355 | "223.5.5.5", # Alibaba DNS (IPv4) 1356 | "119.29.29.29", # DNSPod (IPv4) 1357 | "2400:3200::1", # Alibaba DNS (IPv6) 1358 | "8.8.8.8", # Google Public DNS (IPv4) 1359 | "2001:4860:4860::8888", # Google Public DNS (IPv6) 1360 | "114.114.114.114", # 114 DNS 1361 | "208.67.222.222", # Open DNS (IPv4) 1362 | "2620:0:ccc::2", # Open DNS (IPv6) 1363 | ] 1364 | 1365 | @staticmethod 1366 | def get_dns_cache_file() -> Path: 1367 | """获取 DNS 缓存文件路径,并确保目录存在。""" 1368 | if getattr(sys, "frozen", False): 1369 | # 打包后的执行文件路径 1370 | # current_dir = Path(sys.executable).resolve().parent 1371 | # dns_cache_dir = current_dir / "dns_cache" 1372 | 1373 | # 获取用户目录下的 .setHosts,以防止没有写入权限 1374 | dns_cache_dir = ( 1375 | Path(os.getenv("USERPROFILE", os.getenv("HOME"))) 1376 | / ".setHosts" 1377 | / "dns_cache" 1378 | ) 1379 | else: 1380 | # 脚本运行时路径 1381 | current_dir = Path(__file__).resolve().parent 1382 | dns_cache_dir = current_dir / "dns_cache" 1383 | 1384 | dns_cache_dir.mkdir(parents=True, exist_ok=True) # 确保目录存在 1385 | 1386 | # (提示:dns_records.json 文件将存储 A、AAAA 等 DNS 资源记录缓存。) 1387 | return dns_cache_dir / "dns_records.json" # 返回缓存文件路径 1388 | 1389 | 1390 | # -------------------- 主函数入口 -------------------- # 1391 | async def main(): 1392 | rprint(Utils.get_formatted_line()) # 默认绿色横线 1393 | rprint(Utils.get_formatted_output("启动 setHosts 自动更新···")) 1394 | rprint(Utils.get_formatted_line()) # 默认绿色横线 1395 | print() 1396 | 1397 | start_time = datetime.now() # 记录程序开始运行时间 1398 | 1399 | # 从配置类中加载DOMAIN_GROUPS、DNS_SERVERS和dns_cache_dir 1400 | DOMAIN_GROUPS = Config.DOMAIN_GROUPS 1401 | dns_servers = Config.DNS_SERVERS 1402 | dns_cache_file = Config.get_dns_cache_file() 1403 | 1404 | # 1.域名解析 1405 | resolver = DomainResolver( 1406 | dns_servers=dns_servers, 1407 | max_latency=args.max_latency, 1408 | dns_cache_file=dns_cache_file, 1409 | ) 1410 | 1411 | # 2.延迟检测 1412 | tester = LatencyTester(hosts_num=args.hosts_num) 1413 | 1414 | # 3.Hosts文件操作 1415 | hosts_manager = HostsManager() 1416 | 1417 | # 4.初始化 Hosts更新器 参数 1418 | updater = HostsUpdater( 1419 | domain_groups=DOMAIN_GROUPS, 1420 | resolver=resolver, 1421 | tester=tester, 1422 | hosts_manager=hosts_manager, 1423 | ) 1424 | 1425 | if not PrivilegeManager.is_admin(): 1426 | rprint( 1427 | "[bold red]需要管理员权限来修改hosts文件。正在尝试提升权限...[/bold red]" 1428 | ) 1429 | PrivilegeManager.run_as_admin() 1430 | 1431 | # 启动 Hosts更新器 1432 | await updater.update_hosts() 1433 | 1434 | # 计算程序运行时间 1435 | end_time = datetime.now() 1436 | total_time = end_time - start_time 1437 | rprint( 1438 | f"[bold]代码运行时间:[/bold] [cyan]{total_time.total_seconds():.2f} 秒[/cyan]" 1439 | ) 1440 | 1441 | if getattr(sys, "frozen", False): 1442 | # 如果打包为可执行程序时 1443 | input("\n任务执行完毕,按任意键退出!") 1444 | 1445 | 1446 | if __name__ == "__main__": 1447 | asyncio.run(main()) 1448 | -------------------------------------------------------------------------------- /setHosts_Classic.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinspired/cnNetTool/a41aa9ddb97ce99c91cb1dabe79a25e2bd09ee88/setHosts_Classic.ico -------------------------------------------------------------------------------- /setHosts_Classic.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import asyncio 3 | import concurrent 4 | import ctypes 5 | import json 6 | import logging 7 | import logging.config 8 | import os 9 | import platform 10 | import re 11 | import shutil 12 | import socket 13 | import ssl 14 | import sys 15 | from datetime import datetime, timedelta, timezone 16 | from enum import Enum 17 | from functools import wraps 18 | from math import floor 19 | from pathlib import Path 20 | from typing import Dict, List, Optional, Set, Tuple 21 | 22 | import dns.resolver 23 | import httpx 24 | import wcwidth 25 | from rich import print as rprint 26 | 27 | # from rich.progress import Progress, SpinnerColumn, TextColumn 28 | 29 | # -------------------- 常量设置 -------------------- # 30 | RESOLVER_TIMEOUT = 0.1 # DNS 解析超时时间 秒 31 | HOSTS_NUM = 1 # 每个域名限定Hosts主机 ipv4 数量 32 | MAX_LATENCY = 500 # 允许的最大延迟 33 | PING_TIMEOUT = 1 # ping 超时时间 34 | NUM_PINGS = 4 # ping次数 35 | 36 | # 初始化日志模块 37 | logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") 38 | 39 | 40 | # -------------------- 解析参数 -------------------- # 41 | def parse_args(): 42 | parser = argparse.ArgumentParser( 43 | description=( 44 | "------------------------------------------------------------\n" 45 | "Hosts文件更新工具,此工具可自动解析域名并优化系统的hosts文件\n" 46 | "------------------------------------------------------------\n" 47 | ), 48 | epilog=( 49 | "------------------------------------------------------------\n" 50 | "项目: https://github.com/sinspired/cnNetTool\n" 51 | "作者: Sinspired\n" 52 | "邮箱: ggmomo@gmail.com\n" 53 | "发布: 2024-12-06\n" 54 | ), 55 | formatter_class=argparse.RawTextHelpFormatter, # 允许换行格式 56 | ) 57 | 58 | parser.add_argument( 59 | "-log", 60 | default="info", 61 | choices=["debug", "info", "warning", "error"], 62 | help="设置日志输出等级", 63 | ) 64 | parser.add_argument( 65 | "-num", 66 | "--hosts-num", 67 | default=HOSTS_NUM, 68 | type=int, 69 | help="限定Hosts主机 ip 数量", 70 | ) 71 | parser.add_argument( 72 | "-max", 73 | "--max-latency", 74 | default=MAX_LATENCY, 75 | type=int, 76 | help="设置允许的最大延迟(毫秒)", 77 | ) 78 | parser.add_argument( 79 | "-v", 80 | "--verbose", 81 | action="store_true", 82 | help="打印运行信息", 83 | ) 84 | parser.add_argument( 85 | "-size", 86 | "--batch-size", 87 | default=5, 88 | type=int, 89 | help="SSL证书验证批次", 90 | ) 91 | parser.add_argument( 92 | "-policy", 93 | "--dns-resolve-policy", 94 | default="all", 95 | type=str, 96 | help="DNS解析器区域选择,[all、global、china]", 97 | ) 98 | parser.add_argument( 99 | "-n", 100 | "--checkonly", 101 | action="store_true", 102 | help="仅检测,不自动设置", 103 | ) 104 | return parser.parse_args() 105 | 106 | 107 | args = parse_args() 108 | logging.getLogger().setLevel(args.log.upper()) 109 | 110 | 111 | # -------------------- 辅助功能模块 -------------------- # 112 | class Utils: 113 | @staticmethod 114 | def is_ipv6(ip: str) -> bool: 115 | return ":" in ip 116 | 117 | @staticmethod 118 | def get_hosts_file_path() -> str: 119 | os_type = platform.system().lower() 120 | if os_type == "windows": 121 | return r"C:\Windows\System32\drivers\etc\hosts" 122 | elif os_type in ["linux", "darwin"]: 123 | return "/etc/hosts" 124 | else: 125 | raise ValueError("不支持的操作系统") 126 | 127 | @staticmethod 128 | def backup_hosts_file(hosts_file_path: str): 129 | if os.path.exists(hosts_file_path): 130 | backup_path = f"{hosts_file_path}.bak" 131 | shutil.copy(hosts_file_path, backup_path) 132 | rprint( 133 | f"\n[blue]已备份 [underline]{hosts_file_path}[/underline] 到 [underline]{backup_path}[/underline][/blue]" 134 | ) 135 | 136 | @staticmethod 137 | def write_readme_file( 138 | hosts_content: List[str], temp_file_path: str, update_time: str 139 | ): 140 | """ 141 | 根据模板文件生成 README.md 文件,并将 hosts 文件内容写入其中。 142 | 143 | 参数: 144 | hosts_content (List[str]): hosts 文件的内容,以列表形式传入 145 | temp_file_path (str): 输出的 README.md 文件路径 146 | update_time (str): hosts 文件的更新时间,格式为 "YYYY-MM-DD HH:MM:SS +0800" 147 | """ 148 | try: 149 | # 获取template文件的绝对路径 150 | current_dir = os.path.dirname(os.path.abspath(__file__)) 151 | template_path = os.path.join(current_dir, temp_file_path) 152 | 153 | if not os.path.exists(template_path): 154 | raise FileNotFoundError(f"模板文件未找到: {template_path}") 155 | 156 | # 读取模板文件 157 | with open(template_path, "r", encoding="utf-8") as temp_fb: 158 | template_content = temp_fb.read() 159 | 160 | # 将hosts内容转换为字符串 161 | hosts_str = "\n".join(hosts_content) 162 | 163 | # 使用替换方法而不是format 164 | readme_content = template_content.replace("{hosts_str}", hosts_str) 165 | readme_content = readme_content.replace("{update_time}", update_time) 166 | 167 | # 写入新文件 168 | with open("README.md", "w", encoding="utf-8") as output_fb: 169 | output_fb.write(readme_content) 170 | 171 | rprint( 172 | "[blue]已更新 README.md 文件,位于: [underline]README.md[/underline][/blue]\n" 173 | ) 174 | 175 | except FileNotFoundError as e: 176 | print(f"错误: {str(e)}") 177 | except Exception as e: 178 | print(f"生成 README.md 文件时发生错误: {str(e)}") 179 | 180 | def get_formatted_line(char="-", color="green", width_percentage=0.97): 181 | """ 182 | 生成格式化的分隔线 183 | 184 | 参数: 185 | char: 要重复的字符 186 | color: rich支持的颜色名称 187 | width_percentage: 终端宽度的百分比(0.0-1.0) 188 | """ 189 | # 获取终端宽度 190 | terminal_width = shutil.get_terminal_size().columns 191 | # 计算目标宽度(终端宽度的指定百分比) 192 | target_width = floor(terminal_width * width_percentage) 193 | 194 | # 生成重复字符 195 | line = char * target_width 196 | 197 | # 返回带颜色标记的行 198 | return f"[{color}]{line}[/{color}]" 199 | 200 | def get_formatted_output(text, fill_char=".", align_position=0.97): 201 | """ 202 | 格式化输出文本,确保不超出终端宽度 203 | 204 | 参数: 205 | text: 要格式化的文本 206 | fill_char: 填充字符 207 | align_position: 终端宽度的百分比(0.0-1.0) 208 | """ 209 | # 获取终端宽度并计算目标宽度 210 | terminal_width = shutil.get_terminal_size().columns 211 | target_width = floor(terminal_width * align_position) 212 | 213 | # 移除rich标记计算实际文本长度 214 | plain_text = ( 215 | text.replace("[blue on green]", "").replace("[/blue on green]", "") 216 | # .replace("[完成]", "") 217 | ) 218 | 219 | if "[完成]" in text: 220 | main_text = plain_text.strip() 221 | completion_mark = "[完成]" 222 | # 关键修改:直接从目标宽度减去主文本长度,不再额外预留[完成]的空间 223 | fill_count = target_width - len(main_text) - len(completion_mark) - 6 224 | fill_count = max(0, fill_count) 225 | 226 | filled_text = f"{main_text}{fill_char * fill_count}{completion_mark}" 227 | return f"[blue on green]{filled_text}[/blue on green]" 228 | else: 229 | # 普通文本的处理保持不变 230 | fill_count = target_width - len(plain_text.strip()) - 6 231 | fill_count = max(0, fill_count) 232 | filled_text = f"{plain_text.strip()}{' ' * fill_count}" 233 | return f"[blue on green]{filled_text}[/blue on green]" 234 | 235 | def get_align_str( 236 | i, 237 | group_name, 238 | reference_str="启动 setHosts 自动更新··· ", 239 | ): 240 | """ 241 | 创建一个经过填充的进度字符串,使其显示宽度与参考字符串相同 242 | 243 | Args: 244 | i: 当前处理的组索引 245 | group_name: 组名称 246 | reference_str: 参考字符串,用于对齐长度 247 | 248 | Returns: 249 | 调整后的格式化字符串 250 | """ 251 | # 计算参考字符串的显示宽度 252 | ref_width = wcwidth.wcswidth(reference_str) 253 | 254 | # 构建基础字符串(不包含尾部填充) 255 | base_str = f"正在处理第 {i} 组域名: {group_name.upper()}" 256 | 257 | # 计算基础字符串的显示宽度 258 | base_width = wcwidth.wcswidth(base_str) 259 | 260 | # 计算需要添加的空格数量 261 | # 需要考虑Rich标签不计入显示宽度 262 | padding_needed = ref_width - base_width 263 | 264 | # 确保填充不会为负数 265 | padding_needed = max(0, padding_needed) 266 | 267 | # 构建最终的格式化字符串 268 | formatted_str = f"\n[bold white on bright_black]正在处理第 [green]{i}[/green] 组域名: {group_name.upper()}{' ' * padding_needed}[/bold white on bright_black]" 269 | 270 | return formatted_str 271 | 272 | 273 | # -------------------- 域名与分组管理 -------------------- # 274 | class GroupType(Enum): 275 | SHARED = "shared hosts" # 多个域名共用一组DNS主机 IP 276 | SEPARATE = "separate hosts" # 每个域名独立拥有DNS主机 IP 277 | 278 | 279 | class DomainGroup: 280 | def __init__( 281 | self, 282 | name: str, 283 | domains: List[str], 284 | ips: Optional[Set[str]] = None, 285 | group_type: GroupType = GroupType.SHARED, 286 | ): 287 | self.name = name 288 | self.domains = domains if isinstance(domains, list) else [domains] 289 | self.ips = ips or set() 290 | self.group_type = group_type 291 | 292 | 293 | # -------------------- 域名解析模块 -------------------- # 294 | class DomainResolver: 295 | # 设置缓存过期时间为1周 296 | DNS_CACHE_EXPIRY_TIME = timedelta(weeks=1) 297 | 298 | def __init__(self, dns_servers: List[str], max_latency: int, dns_cache_file: str): 299 | self.dns_servers = dns_servers 300 | self.max_latency = max_latency 301 | self.dns_cache_file = Path(dns_cache_file) 302 | self.dns_records = self._init_dns_cache() 303 | 304 | def _init_dns_cache(self) -> dict: 305 | """初始化 DNS 缓存,如果缓存文件存在且未过期则加载,否则返回空字典""" 306 | if self._is_dns_cache_valid(): 307 | return self.load_hosts_cache() 308 | # 如果 DNS 缓存过期,删除旧缓存文件 309 | if self.dns_cache_file.exists(): 310 | self.dns_cache_file.unlink() 311 | return {} 312 | 313 | def _is_dns_cache_valid(self) -> bool: 314 | """检查 DNS 缓存是否有效""" 315 | if not self.dns_cache_file.exists(): 316 | return False 317 | 318 | file_age = datetime.now() - datetime.fromtimestamp( 319 | os.path.getmtime(self.dns_cache_file) 320 | ) 321 | return file_age <= self.DNS_CACHE_EXPIRY_TIME 322 | 323 | def load_hosts_cache(self) -> Dict[str, Dict]: 324 | try: 325 | with open(self.dns_cache_file, "r", encoding="utf-8") as f: 326 | return json.load(f) 327 | except Exception as e: 328 | logging.error(f"加载 DNS 缓存文件失败: {e}") 329 | return {} 330 | 331 | def save_hosts_cache(self): 332 | try: 333 | with open(self.dns_cache_file, "w", encoding="utf-8") as f: 334 | json.dump(self.dns_records, f, indent=4, ensure_ascii=False) 335 | logging.debug(f"成功保存 DNS 缓存到文件 {self.dns_cache_file}") 336 | except Exception as e: 337 | logging.error(f"保存 DNS 缓存到文件时发生错误: {e}") 338 | 339 | async def resolve_domain(self, domain: str) -> Set[str]: 340 | start_time = datetime.now() 341 | ips = set() 342 | 343 | # 1. 首先通过常规DNS服务器解析 344 | dns_ips = await self._resolve_via_dns(domain, "all") 345 | ips.update(dns_ips) 346 | 347 | dns_resolve_end_time = datetime.now() 348 | 349 | dns_resolve_duration = dns_resolve_end_time - start_time 350 | logging.debug(f"DNS解析耗时: {dns_resolve_duration.total_seconds():.2f}秒") 351 | 352 | # # 2. 然后通过DNS_records解析 353 | # # 由于init时已经处理了过期文件,这里只需要检查域名是否在缓存中 354 | # if domain in self.dns_records: 355 | # domain_hosts = self.dns_records.get(domain, {}) 356 | # ipv4_ips = domain_hosts.get("ipv4", []) 357 | # ipv6_ips = domain_hosts.get("ipv6", []) 358 | 359 | # ips.update(ipv4_ips + ipv6_ips) 360 | # logging.debug( 361 | # f"成功通过缓存文件解析 {domain}, 发现 {len(ipv4_ips) + len(ipv6_ips)} 个 DNS 主机:\n{ipv4_ips}\n{ipv6_ips if ipv6_ips else ''}\n" 362 | # ) 363 | # else: 364 | # ipaddress_ips = await self._resolve_via_ipaddress(domain) 365 | # if ipaddress_ips: 366 | # ips.update(ipaddress_ips) 367 | 368 | # if ips: 369 | # logging.debug( 370 | # f"成功通过 DNS服务器 和 DNS记录 解析 {domain}, 发现 {len(ips)} 个 唯一 DNS 主机\n{ips}\n" 371 | # ) 372 | # else: 373 | # logging.debug(f"警告: 无法解析 {domain}") 374 | 375 | ipaddress_resolve_end_time = datetime.now() 376 | ipaddress_resolve_duration = ipaddress_resolve_end_time - dns_resolve_end_time 377 | total_resolve_duration = ipaddress_resolve_end_time - start_time 378 | 379 | logging.debug( 380 | f"IP地址解析耗时: {ipaddress_resolve_duration.total_seconds():.2f}秒" 381 | ) 382 | logging.debug(f"DNS解析总耗时: {total_resolve_duration.total_seconds():.2f}秒") 383 | 384 | return ips 385 | 386 | async def _resolve_via_dns(self, domain: str, dns_type: str = "all") -> Set[str]: 387 | """ 388 | 通过 DNS 解析域名 389 | 390 | :param domain: 待解析的域名 391 | :param dns_type: 解析使用的 DNS 类型。可选值: 392 | - "all": 同时使用国内和国际 DNS 393 | - "china": 仅使用国内 DNS 394 | - "international": 仅使用国际 DNS 395 | :return: 解析得到的 IP 集合 396 | """ 397 | 398 | async def resolve_with_dns_server(dns_server_info: dict) -> Set[str]: 399 | """单个DNS服务器的解析协程""" 400 | dns_server = dns_server_info["ip"] 401 | dns_provider = dns_server_info["provider"] 402 | ips = set() 403 | resolver = dns.resolver.Resolver(configure=False) 404 | resolver.nameservers = [dns_server] 405 | resolver.timeout = RESOLVER_TIMEOUT 406 | resolver.lifetime = RESOLVER_TIMEOUT 407 | 408 | try: 409 | # 使用 to_thread 在线程池中执行同步的 DNS 查询 410 | for qtype in ["A", "AAAA"]: 411 | try: 412 | answers = await asyncio.to_thread( 413 | resolver.resolve, domain, qtype 414 | ) 415 | ips.update(answer.address for answer in answers) 416 | except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): 417 | pass 418 | except Exception as e: 419 | logging.debug(f"DNS 查询异常 ({qtype}, {dns_server}): {e}") 420 | 421 | if ips: 422 | logging.debug( 423 | f"成功使用 {dns_provider} : {dns_server} 解析 {domain},共 {len(ips)} 个主机: {ips}" 424 | ) 425 | 426 | return ips 427 | 428 | except Exception as e: 429 | logging.debug(f"使用 {dns_server} 解析 {domain} 失败: {e}") 430 | return set() 431 | 432 | # 根据 dns_type 选择要使用的 DNS 服务器 433 | if dns_type.lower() == "all": 434 | dns_servers = ( 435 | self.dns_servers["china_mainland"] + self.dns_servers["international"] 436 | ) 437 | elif dns_type.lower() == "china": 438 | dns_servers = self.dns_servers["china_mainland"] 439 | elif dns_type.lower() == "global" or dns_type.lower() == "international": 440 | dns_servers = self.dns_servers["international"] 441 | else: 442 | dns_servers = ( 443 | self.dns_servers["china_mainland"] + self.dns_servers["international"] 444 | ) 445 | # raise ValueError(f"无效的 DNS 类型:{dns_type}") 446 | 447 | # 并发解析所有选定的 DNS 服务器,并保留非空结果 448 | tasks = [resolve_with_dns_server(dns_server) for dns_server in dns_servers] 449 | results = await asyncio.gather(*tasks) 450 | 451 | # 合并所有非空的解析结果 452 | ips = set(ip for result in results for ip in result if ip) 453 | if ips: 454 | logging.debug( 455 | f"成功使用多个 DNS 服务器解析 {domain},共 {len(ips)} 个主机:\n{ips}\n" 456 | ) 457 | # input("按任意键继续") 458 | return ips 459 | 460 | def retry_async(tries=3, delay=0): 461 | def decorator(func): 462 | @wraps(func) 463 | async def wrapper(*args, **kwargs): 464 | domain = args[1] 465 | for attempt in range(tries): 466 | try: 467 | return await func(*args, **kwargs) 468 | except Exception: 469 | if attempt < tries - 1: 470 | print(f"第 {attempt + 2} 次尝试:") 471 | # logging.debug(f"通过DNS_records解析 {args[1]},第 {attempt + 2} 次尝试:") 472 | if attempt == tries - 1: 473 | self = args[0] # 明确 self 的引用 474 | domain = args[1] 475 | current_time = datetime.now().isoformat() 476 | self.dns_records[domain] = { 477 | "last_update": current_time, 478 | "ipv4": [], 479 | "ipv6": [], 480 | "source": "DNS_records", 481 | } 482 | self.save_hosts_cache() 483 | logging.warning( 484 | f"ipaddress.com {tries} 次尝试后未解析到 {domain} 的 DNS_records 地址," 485 | f"已写入空地址到缓存以免无谓消耗网络资源" 486 | ) 487 | # print(f"通过 DNS_records 解析 { 488 | # domain},{tries} 次尝试后终止!") 489 | return None 490 | await asyncio.sleep(delay) 491 | return None 492 | 493 | return wrapper 494 | 495 | return decorator 496 | 497 | LOGGING_CONFIG = { 498 | "version": 1, 499 | "handlers": { 500 | "httpxHandlers": { 501 | "class": "logging.StreamHandler", 502 | "formatter": "http", 503 | "stream": "ext://sys.stderr", 504 | } 505 | }, 506 | "formatters": { 507 | "http": { 508 | "format": "%(levelname)s [%(asctime)s] %(name)s - %(message)s", 509 | "datefmt": "%Y-%m-%d %H:%M:%S", 510 | } 511 | }, 512 | "loggers": { 513 | "httpx": { 514 | "handlers": ["httpxHandlers"], 515 | "level": "WARNING", 516 | }, 517 | "httpcore": { 518 | "handlers": ["httpxHandlers"], 519 | "level": "WARNING", 520 | }, 521 | }, 522 | } 523 | 524 | logging.config.dictConfig(LOGGING_CONFIG) 525 | 526 | @retry_async(tries=3) 527 | async def _resolve_via_ipaddress(self, domain: str) -> Set[str]: 528 | ips = set() 529 | url = f"https://www.ipaddress.com/website/{domain}" 530 | headers = { 531 | "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) " 532 | "AppleWebKit/537.36 (KHTML, like Gecko) " 533 | "Chrome/106.0.0.0 Safari/537.36" 534 | } 535 | # headers = { 536 | # "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.5615.121 Safari/537.36", 537 | # "Referer": "https://www.ipaddress.com", 538 | # } 539 | 540 | try: 541 | async with httpx.AsyncClient( 542 | timeout=httpx.Timeout(1.0), 543 | follow_redirects=True, 544 | http2=True, 545 | ) as client: 546 | response = await client.get(url, headers=headers) 547 | 548 | # # 使用内置方法检查状态码 549 | response.raise_for_status() # 自动处理非200状态码 550 | 551 | content = response.text 552 | 553 | ipv4_pattern = r">((?:[0-9]{1,3}\.){3}[0-9]{1,3})\b<" 554 | # ipv6_pattern = r">((?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4})<" 555 | # 支持ipv6压缩 556 | ipv6_pattern = r">((?:[0-9a-fA-F]{1,4}:){2,7}[0-9a-fA-F]{1,4}|[0-9a-fA-F]{1,4}(?::[0-9a-fA-F]{1,4}){0,5}::[0-9a-fA-F]{1,6})<" 557 | 558 | ipv4_ips = set(re.findall(ipv4_pattern, content)) 559 | ipv6_ips = set(re.findall(ipv6_pattern, content)) 560 | 561 | ips.update(ipv4_ips) 562 | ips.update(ipv6_ips) 563 | 564 | if ips: 565 | current_time = datetime.now().isoformat() 566 | self.dns_records[domain] = { 567 | "last_update": current_time, 568 | "ipv4": list(ipv4_ips), 569 | "ipv6": list(ipv6_ips), 570 | "source": "DNS_records", 571 | } 572 | self.save_hosts_cache() 573 | logging.debug( 574 | f"通过 ipaddress.com 成功解析 {domain} 并更新 DNS_records 缓存" 575 | ) 576 | logging.debug(f"DNS_records:\n {ips}") 577 | else: 578 | self.dns_records[domain] = { 579 | "last_update": datetime.now().isoformat(), 580 | "ipv4": [], 581 | "ipv6": [], 582 | "source": "DNS_records", 583 | } 584 | self.save_hosts_cache() 585 | logging.warning( 586 | f"ipaddress.com 未解析到 {domain} 的 DNS_records 地址,已写入空地址到缓存以免无谓消耗网络资源" 587 | ) 588 | except Exception as e: 589 | logging.error(f"通过DNS_records解析 {domain} 失败! {e}") 590 | raise 591 | return ips 592 | 593 | 594 | # -------------------- 延迟测速模块 -------------------- # 595 | 596 | 597 | class LatencyTester: 598 | def __init__(self, hosts_num: int, max_workers: int = 200): 599 | self.hosts_num = hosts_num 600 | self.max_workers = max_workers 601 | 602 | async def get_lowest_latency_hosts( 603 | self, 604 | group_name: str, 605 | domains: List[str], 606 | file_ips: Set[str], 607 | latency_limit: int, 608 | ) -> List[Tuple[str, float]]: 609 | """ 610 | 使用线程池和异步操作优化IP延迟和SSL证书验证 611 | """ 612 | all_ips = list(file_ips) 613 | # start_time = datetime.now() 614 | rprint( 615 | f"[bright_black]- 获取到 [bold bright_green]{len(all_ips)}[/bold bright_green] 个唯一IP地址[/bright_black]" 616 | ) 617 | if all_ips: 618 | rprint("[bright_black]- 检测主机延迟...[/bright_black]") 619 | 620 | # 使用线程池来并发处理SSL证书验证 621 | with concurrent.futures.ThreadPoolExecutor( 622 | max_workers=self.max_workers 623 | ) as executor: 624 | # 第一步:并发获取IP延迟 625 | ping_tasks = [self.get_host_average_latency(ip) for ip in all_ips] 626 | latency_results = await asyncio.gather(*ping_tasks) 627 | 628 | # 筛选有效延迟的IP 629 | valid_latency_results = [ 630 | result for result in latency_results if result[1] != float("inf") 631 | ] 632 | if valid_latency_results: 633 | if len(valid_latency_results) < len(all_ips): 634 | rprint( 635 | f"[bright_black]- 检测到 [bold bright_green]{len(valid_latency_results)}[/bold bright_green] 个有效IP地址[/bright_black]" 636 | ) 637 | valid_latency_ips = [ 638 | result 639 | for result in valid_latency_results 640 | if result[1] < latency_limit 641 | ] 642 | if not valid_latency_ips: 643 | logging.warning(f"未发现延迟小于 {latency_limit}ms 的IP。") 644 | min_result = [min(valid_latency_results, key=lambda x: x[1])] 645 | latency_limit = min_result[0][1] * 2 646 | logging.debug(f"主机IP最低延迟 {latency_limit:.0f}ms") 647 | valid_latency_ips = [ 648 | result 649 | for result in valid_latency_results 650 | if result[1] <= latency_limit 651 | ] 652 | else: 653 | rprint("[red]延迟检测没有获得有效IP[/red]") 654 | return [] 655 | 656 | # 排序结果 657 | valid_latency_ips = sorted(valid_latency_ips, key=lambda x: x[1]) 658 | 659 | if len(valid_latency_ips) < len(valid_latency_results): 660 | rprint( 661 | f"[bright_black]- 检测到 [bold bright_green]{len(valid_latency_ips)}[/bold bright_green] 个延迟小于 {latency_limit}ms 的有效IP地址[/bright_black]" 662 | ) 663 | 664 | ipv4_results = [r for r in valid_latency_ips if not Utils.is_ipv6(r[0])] 665 | ipv6_results = [r for r in valid_latency_ips if Utils.is_ipv6(r[0])] 666 | 667 | # 第二步:使用线程池并发验证SSL证书 668 | # if "github" in group_name.lower(): 669 | if len(valid_latency_ips) > 1 and any( 670 | keyword in group_name.lower() for keyword in ["google"] 671 | ): 672 | rprint("[bright_black]- 验证SSL证书...[/bright_black]") 673 | ipv4_count = 0 674 | ipv6_count = 0 675 | batch_size = args.batch_size 676 | total_results = len(valid_latency_ips) 677 | valid_results = [] 678 | 679 | loop = asyncio.get_running_loop() 680 | 681 | for i in range(0, total_results, batch_size): 682 | min_len = min(total_results, batch_size) 683 | batch = valid_latency_ips[i : i + min_len] 684 | ssl_verification_tasks = [ 685 | loop.run_in_executor( 686 | executor, 687 | self._sync_is_cert_valid_dict, 688 | domains[0], 689 | ip, 690 | latency, 691 | ) 692 | for ip, latency in batch 693 | ] 694 | 695 | for future in asyncio.as_completed(ssl_verification_tasks): 696 | ip, latency, ssl_valid = await future 697 | if ssl_valid: 698 | valid_results.append((ip, latency)) 699 | if Utils.is_ipv6(ip): 700 | ipv6_count += 1 701 | else: 702 | ipv4_count += 1 703 | if ipv6_results: 704 | if ipv4_results: 705 | if ipv6_count >= 1 and ipv4_count >= 1: 706 | break 707 | else: 708 | if ipv6_count >= 1: 709 | break 710 | else: 711 | if ipv4_count >= self.hosts_num: 712 | break 713 | if ipv6_results: 714 | if ipv4_results: 715 | if ipv6_count >= 1 and ipv4_count >= 1: 716 | break 717 | else: 718 | if ipv6_count >= 1: 719 | break 720 | else: 721 | if ipv4_count >= self.hosts_num: 722 | break 723 | else: 724 | valid_results = valid_latency_ips 725 | 726 | # 按延迟排序并选择最佳主机 727 | valid_results = sorted(valid_results, key=lambda x: x[1]) 728 | 729 | if not valid_results: 730 | rprint(f"[red]未发现延迟小于 {latency_limit}ms 且证书有效的IP。[/red]") 731 | 732 | # 选择最佳主机(支持IPv4和IPv6) 733 | best_hosts = self._select_best_hosts(valid_results) 734 | 735 | # 打印结果(可以根据需要保留或修改原有的打印逻辑) 736 | self._print_results(best_hosts, latency_limit) 737 | 738 | return best_hosts 739 | 740 | async def get_host_average_latency( 741 | self, ip: str, port: int = 443 742 | ) -> Tuple[str, float]: 743 | try: 744 | response_times = await asyncio.gather( 745 | *[self.get_latency(ip, port) for _ in range(NUM_PINGS)] 746 | ) 747 | response_times = [t for t in response_times if t != float("inf")] 748 | if response_times: 749 | average_response_time = sum(response_times) / len(response_times) 750 | else: 751 | average_response_time = float("inf") 752 | 753 | if average_response_time == 0: 754 | logging.error(f"{ip} 平均延迟为 0 ms,视为无效") 755 | return ip, float("inf") 756 | 757 | logging.debug(f"{ip} 平均延迟: {average_response_time:.2f} ms") 758 | return ip, average_response_time 759 | except Exception as e: 760 | logging.debug(f"ping {ip} 时出错: {e}") 761 | return ip, float("inf") 762 | 763 | async def get_latency(self, ip: str, port: int = 443) -> float: 764 | try: 765 | # 使用 getaddrinfo 来获取正确的地址格式 766 | addrinfo = await asyncio.get_event_loop().getaddrinfo( 767 | ip, port, family=socket.AF_UNSPEC, type=socket.SOCK_STREAM 768 | ) 769 | 770 | for family, type, proto, canonname, sockaddr in addrinfo: 771 | try: 772 | start = asyncio.get_event_loop().time() 773 | _, writer = await asyncio.wait_for( 774 | asyncio.open_connection(sockaddr[0], sockaddr[1]), 775 | timeout=PING_TIMEOUT, 776 | ) 777 | end = asyncio.get_event_loop().time() 778 | writer.close() 779 | await writer.wait_closed() 780 | return (end - start) * 1000 781 | except asyncio.TimeoutError: 782 | continue 783 | except Exception as e: 784 | logging.debug(f"连接测试失败 {ip} (sockaddr: {sockaddr}): {e}") 785 | continue 786 | return float("inf") 787 | except Exception as e: 788 | logging.error(f"获取地址信息失败 {ip}: {e}") 789 | return float("inf") 790 | 791 | def _sync_is_cert_valid_dict( 792 | self, domain: str, ip: str, latency: float, port: int = 443 793 | ) -> Tuple[str, float, bool]: 794 | """ 795 | 同步版本的证书验证方法,用于在线程池中执行 796 | """ 797 | try: 798 | context = ssl.create_default_context() 799 | context.verify_mode = ssl.CERT_REQUIRED 800 | context.check_hostname = True 801 | 802 | with socket.create_connection((ip, port), timeout=2) as sock: 803 | with context.wrap_socket(sock, server_hostname=domain) as ssock: 804 | cert = ssock.getpeercert() 805 | not_after = datetime.strptime( 806 | cert["notAfter"], "%b %d %H:%M:%S %Y %Z" 807 | ) 808 | if not_after < datetime.now(): 809 | logging.debug(f"{domain} ({ip}) {latency:.0f}ms: 证书已过期") 810 | return (ip, latency, False) 811 | 812 | logging.debug( 813 | f"{domain} ({ip}) {latency:.0f}ms: SSL证书有效,截止日期为 {not_after}" 814 | ) 815 | return (ip, latency, True) 816 | 817 | except ConnectionError as e: 818 | logging.debug( 819 | f"{domain} ({ip}) {latency:.0f}ms: 连接被强迫关闭,ip有效 - {e}" 820 | ) 821 | return (ip, latency, True) 822 | except Exception as e: 823 | logging.debug(f"{domain} ({ip}) {latency:.0f}ms: 证书验证失败 - {e}") 824 | return (ip, latency, False) 825 | 826 | def _sync_is_cert_valid_dict_average( 827 | self, domains: List[str], ip: str, latency: float, port: int = 443 828 | ) -> Tuple[str, float, bool]: 829 | """ 830 | 同步版本的证书验证方法,用于在线程池中执行。 831 | 任意一个 domain 验证通过就视为通过。 832 | """ 833 | for domain in domains: 834 | try: 835 | context = ssl.create_default_context() 836 | context.verify_mode = ssl.CERT_REQUIRED 837 | context.check_hostname = True 838 | 839 | with socket.create_connection((ip, port), timeout=2) as sock: 840 | with context.wrap_socket(sock, server_hostname=domain) as ssock: 841 | cert = ssock.getpeercert() 842 | not_after = datetime.strptime( 843 | cert["notAfter"], "%b %d %H:%M:%S %Y %Z" 844 | ) 845 | if not_after < datetime.now(): 846 | logging.debug( 847 | f"{domain} ({ip}) {latency:.0f}ms: 证书已过期" 848 | ) 849 | continue # 检查下一个 domain 850 | 851 | logging.debug( 852 | f"{domain} ({ip}) {latency:.0f}ms: SSL证书有效,截止日期为 {not_after}" 853 | ) 854 | return (ip, latency, True) # 任意一个验证通过即返回成功 855 | 856 | except ConnectionError as e: 857 | logging.debug( 858 | f"{domain} ({ip}) {latency:.0f}ms: 连接被强迫关闭,ip有效 - {e}" 859 | ) 860 | return (ip, latency, True) 861 | except Exception as e: 862 | logging.debug(f"{domain} ({ip}) {latency:.0f}ms: 证书验证失败 - {e}") 863 | continue # 检查下一个 domain 864 | 865 | # 如果所有 domain 都验证失败 866 | return (ip, latency, False) 867 | 868 | def _select_best_hosts( 869 | self, valid_results: List[Tuple[str, float]] 870 | ) -> List[Tuple[str, float]]: 871 | """ 872 | 选择最佳主机,优先考虑IPv4和IPv6 873 | """ 874 | ipv4_results = [r for r in valid_results if not Utils.is_ipv6(r[0])] 875 | ipv6_results = [r for r in valid_results if Utils.is_ipv6(r[0])] 876 | 877 | best_hosts = [] 878 | selected_count = 0 879 | 880 | if ipv4_results: 881 | min_ipv4_results = min(ipv4_results, key=lambda x: x[1]) 882 | 883 | # 先选择IPv4 884 | if ipv4_results: 885 | logging.debug(f"有效IPv4:\n{ipv4_results}\n") 886 | for ip, latency in ipv4_results: 887 | best_hosts.append((ip, latency)) 888 | selected_count += 1 889 | if ( 890 | ipv6_results and selected_count >= 1 891 | ) or selected_count >= self.hosts_num: 892 | break 893 | # 再选择IPv6 894 | if ipv6_results: 895 | logging.debug(f"有效IPv6:\n{ipv6_results}\n") 896 | for ip, latency in ipv6_results: 897 | if ipv4_results and latency <= min_ipv4_results[1] * 2: 898 | best_hosts.append((ip, latency)) 899 | break 900 | else: 901 | best_hosts.append((ip, latency)) 902 | break 903 | 904 | return best_hosts 905 | 906 | def _print_results(self, best_hosts: List[Tuple[str, float]], latency_limit: int): 907 | """ 908 | 打印结果的方法 909 | """ 910 | rprint( 911 | f"[bold yellow]最快的 DNS主机 IP(优先选择 IPv6) 丨 延迟 < {latency_limit:.0f}ms :[/bold yellow]" 912 | ) 913 | for ip, time in best_hosts: 914 | rprint( 915 | f" [green]{ip}[/green] [bright_black]{time:.2f} ms[/bright_black]" 916 | ) 917 | 918 | # end_time = datetime.now() 919 | # total_time = end_time - start_time 920 | # rprint( 921 | # f"[bright_black]- 运行时间:[/bright_black] [cyan]{total_time.total_seconds():.2f} 秒[/cyan]") 922 | 923 | 924 | # -------------------- Hosts文件管理 -------------------- # 925 | 926 | 927 | class HostsManager: 928 | def __init__(self): 929 | # 自动根据操作系统获取hosts文件路径 930 | self.hosts_file_path = self._get_hosts_file_path() 931 | 932 | @staticmethod 933 | def _get_hosts_file_path() -> str: 934 | """根据操作系统自动获取 hosts 文件路径。""" 935 | return Utils.get_hosts_file_path() 936 | 937 | def write_to_hosts_file(self, new_entries: List[str]): 938 | if not args.checkonly: 939 | Utils.backup_hosts_file(self.hosts_file_path) 940 | 941 | with open(self.hosts_file_path, "r") as f: 942 | existing_content = f.read().splitlines() 943 | 944 | new_domains = { 945 | entry.split()[1] for entry in new_entries if len(entry.split()) >= 2 946 | } 947 | 948 | new_content = [] 949 | skip = False 950 | skip_tags = ("# cnNetTool", "# Update", "# Star", "# GitHub") 951 | 952 | for line in existing_content: 953 | line = line.strip() 954 | 955 | # 跳过标记块 956 | if any(line.startswith(tag) for tag in skip_tags): 957 | skip = True 958 | 959 | if line == "": 960 | skip = True 961 | 962 | if skip: 963 | if line == "" or line.startswith("#"): 964 | continue 965 | skip = False 966 | 967 | # 非标记块内容保留 968 | if ( 969 | not skip 970 | and (line.startswith("#") or not line) 971 | and not any(tag in line for tag in skip_tags) 972 | ): 973 | new_content.append(line) 974 | continue 975 | 976 | # 检查域名是否为新条目 977 | parts = line.split() 978 | if len(parts) >= 2 and parts[1] not in new_domains: 979 | new_content.append(line) 980 | else: 981 | logging.debug(f"删除旧条目: {line}") 982 | 983 | update_time = ( 984 | datetime.now(timezone.utc) 985 | .astimezone(timezone(timedelta(hours=8))) 986 | .strftime("%Y-%m-%d %H:%M:%S %z") 987 | .replace("+0800", "+08:00") 988 | ) 989 | 990 | rprint("\n[bold yellow]正在更新 hosts 文件...[/bold yellow]") 991 | 992 | save_hosts_content = [] # 提取新内容文本 993 | 994 | # 1. 添加标题 995 | new_content.append(f"\n# cnNetTool Start in {update_time}") 996 | save_hosts_content.append(f"\n# cnNetTool Start in {update_time}") 997 | 998 | # 2. 添加主机条目 999 | for entry in new_entries: 1000 | # 分割 IP 和域名 1001 | ip, domain = entry.strip().split(maxsplit=1) 1002 | 1003 | # 计算需要的制表符数量 1004 | # IP 地址最长可能是 39 个字符 (IPv6) 1005 | # 我们使用制表符(8个空格)来对齐,确保视觉上的整齐 1006 | ip_length = len(ip) 1007 | if ip_length <= 8: 1008 | tabs = "\t\t\t" # 两个制表符 1009 | if ip_length <= 10: 1010 | tabs = "\t\t" # 两个制表符 1011 | elif ip_length <= 16: 1012 | tabs = "\t" # 一个制表符 1013 | else: 1014 | tabs = "\t" # 对于很长的IP,只使用一个空格 1015 | 1016 | # 返回格式化后的条目 1017 | formatedEntry = f"{ip}{tabs}{domain}" 1018 | 1019 | new_content.append(formatedEntry) 1020 | save_hosts_content.append(formatedEntry) 1021 | rprint(f"+ {formatedEntry}") 1022 | 1023 | # 3. 添加项目描述 1024 | new_content.extend( 1025 | [ 1026 | f"\n# Update time: {update_time}", 1027 | "# GitHub仓库: https://github.com/sinspired/cnNetTool", 1028 | "# cnNetTool End\n", 1029 | ] 1030 | ) 1031 | save_hosts_content.extend( 1032 | [ 1033 | f"\n# Update time: {update_time}", 1034 | "# GitHub仓库: https://github.com/sinspired/cnNetTool", 1035 | "# cnNetTool End\n", 1036 | ] 1037 | ) 1038 | 1039 | # 4. 写入hosts文件 1040 | if not args.checkonly: 1041 | with open(self.hosts_file_path, "w") as f: 1042 | f.write("\n".join(new_content)) 1043 | 1044 | # 保存 hosts 文本 1045 | with open("hosts", "w") as f: 1046 | f.write("\n".join(save_hosts_content)) 1047 | rprint( 1048 | f"\n[blue]已生成 hosts 文件,位于: [underline]hosts[/underline][/blue] (共 {len(new_entries)} 个条目)" 1049 | ) 1050 | 1051 | if not getattr(sys, "frozen", False): 1052 | # 如果未打包为可执行程序 1053 | Utils.write_readme_file( 1054 | save_hosts_content, "README_template.md", f"{update_time}" 1055 | ) 1056 | 1057 | 1058 | # -------------------- 主控制模块 -------------------- # 1059 | class HostsUpdater: 1060 | def __init__( 1061 | self, 1062 | domain_groups: List[DomainGroup], 1063 | resolver: DomainResolver, 1064 | tester: LatencyTester, 1065 | hosts_manager: HostsManager, 1066 | ): 1067 | self.domain_groups = domain_groups 1068 | self.resolver = resolver 1069 | self.tester = tester 1070 | self.hosts_manager = hosts_manager 1071 | 1072 | async def update_hosts(self): 1073 | # 更新hosts文件的主逻辑 1074 | all_entries = [] 1075 | 1076 | for i, group in enumerate(self.domain_groups, 1): 1077 | progress_str = Utils.get_align_str(i, group.name) 1078 | rprint(progress_str) 1079 | # 先获取预设IP 1080 | default_ips = group.ips.copy() 1081 | 1082 | # 2. 根据不同组设置IP 1083 | if group.group_type == GroupType.SEPARATE: 1084 | for domain in group.domains: 1085 | rprint(f"\n为域名 {domain} 设置 DNS 映射主机") 1086 | # 重置初始ip,否则会混淆 1087 | all_ips = set() 1088 | if default_ips: 1089 | rprint( 1090 | f"[bright_black]- 读取到 [bold bright_green]{len(default_ips)}[/bold bright_green] 个预设IP地址[/bright_black]" 1091 | ) 1092 | all_ips.update(default_ips) 1093 | 1094 | resolved_ips = await self.resolver.resolve_domain(domain) 1095 | all_ips.update(resolved_ips) 1096 | 1097 | if not all_ips: 1098 | logging.warning(f"{domain} 未找到任何可用IP。跳过该域名。") 1099 | continue 1100 | 1101 | fastest_ips = set() 1102 | fastest_ips = await self.tester.get_lowest_latency_hosts( 1103 | group.name, 1104 | [domain], 1105 | all_ips, 1106 | self.resolver.max_latency, 1107 | ) 1108 | if not fastest_ips: 1109 | logging.warning(f"{domain} 未找到延迟满足要求的IP。") 1110 | continue 1111 | 1112 | new_entries = [f"{ip}\t{domain}" for ip, latency in fastest_ips] 1113 | all_entries.extend(new_entries) 1114 | else: 1115 | all_ips = set() 1116 | if default_ips: 1117 | rprint( 1118 | f"[bright_black]- 读取到 [bold bright_green]{len(default_ips)}[/bold bright_green] 个预设IP地址[/bright_black]" 1119 | ) 1120 | all_ips.update(default_ips) 1121 | 1122 | # 收集组内所有域名的DNS解析结果 1123 | domain_resolve_tasks = [ 1124 | self.resolver.resolve_domain(domain) for domain in group.domains 1125 | ] 1126 | resolved_ips = await asyncio.gather( 1127 | *domain_resolve_tasks, return_exceptions=True 1128 | ) 1129 | 1130 | all_ips.update(ip for ip_list in resolved_ips for ip in ip_list if ip) 1131 | 1132 | if not all_ips: 1133 | logging.warning(f"组 {group.name} 未找到任何可用IP。跳过该组。") 1134 | continue 1135 | 1136 | # rprint(f" 找到 {len(all_ips)} 个 DNS 主机记录") 1137 | 1138 | fastest_ips = await self.tester.get_lowest_latency_hosts( 1139 | group.name, 1140 | # [group.domains[0]], # 只需传入一个域名,因为只是用来测试IP 1141 | group.domains, # 传入所有域名以获得更准确的延迟测试结果 1142 | all_ips, 1143 | self.resolver.max_latency, 1144 | ) 1145 | 1146 | if not fastest_ips: 1147 | logging.warning(f"组 {group.name} 未找到延迟满足要求的IP。") 1148 | continue 1149 | 1150 | rprint( 1151 | f"\n[bold]为组 {group.name} 内所有域名应用延迟最低的 DNS 映射主机IP:[/bold]" 1152 | ) 1153 | for domain in group.domains: 1154 | new_entries = [f"{ip}\t{domain}" for ip, latency in fastest_ips] 1155 | rprint(f"[bright_black] - {domain}[/bright_black]") 1156 | all_entries.extend(new_entries) 1157 | 1158 | if all_entries: 1159 | self.hosts_manager.write_to_hosts_file(all_entries) 1160 | rprint( 1161 | "\n[blue on green]Hosts 文件更新......................................... [完成][/blue on green]" 1162 | ) 1163 | else: 1164 | rprint("\n[bold red]警告: 没有有效条目可写入。hosts文件未更新。[/bold red]") 1165 | 1166 | 1167 | # -------------------- 权限提升模块-------------------- # 1168 | class PrivilegeManager: 1169 | @staticmethod 1170 | def is_admin() -> bool: 1171 | try: 1172 | return os.getuid() == 0 1173 | except AttributeError: 1174 | return ctypes.windll.shell32.IsUserAnAdmin() != 0 1175 | 1176 | @staticmethod 1177 | def run_as_admin(): 1178 | if PrivilegeManager.is_admin(): 1179 | return 1180 | 1181 | if sys.platform.startswith("win"): 1182 | script = os.path.abspath(sys.argv[0]) 1183 | params = " ".join([script] + sys.argv[1:]) 1184 | ctypes.windll.shell32.ShellExecuteW( 1185 | None, "runas", sys.executable, params, None, 1 1186 | ) 1187 | else: 1188 | os.execvp("sudo", ["sudo", "python3"] + sys.argv) 1189 | sys.exit(0) 1190 | 1191 | 1192 | # -------------------- 数据配置模块-------------------- # 1193 | 1194 | 1195 | class Config: 1196 | DOMAIN_GROUPS = [ 1197 | DomainGroup( 1198 | name="GitHub Services", 1199 | group_type=GroupType.SEPARATE, 1200 | domains=[ 1201 | "alive.github.com", 1202 | "live.github.com", 1203 | "api.github.com", 1204 | "codeload.github.com", 1205 | "central.github.com", 1206 | "gist.github.com", 1207 | "github.com", 1208 | "github.community", 1209 | "github.global.ssl.fastly.net", 1210 | "github-com.s3.amazonaws.com", 1211 | "github-production-release-asset-2e65be.s3.amazonaws.com", 1212 | "github-production-user-asset-6210df.s3.amazonaws.com", 1213 | "github-production-repository-file-5c1aeb.s3.amazonaws.com", 1214 | "pipelines.actions.githubusercontent.com", 1215 | "github.githubassets.com", 1216 | "github-cloud.s3.amazonaws.com", 1217 | "github.blog", 1218 | ], 1219 | ips={}, 1220 | ), 1221 | DomainGroup( 1222 | name="GitHub Asset", 1223 | group_type=GroupType.SHARED, 1224 | domains=[ 1225 | "githubstatus.com", 1226 | "assets-cdn.github.com", 1227 | "github.io", 1228 | ], 1229 | ips={}, 1230 | ), 1231 | DomainGroup( 1232 | name="GitHub Central&Education ", 1233 | group_type=GroupType.SHARED, 1234 | domains=[ 1235 | "collector.github.com", 1236 | "education.github.com", 1237 | ], 1238 | ips={}, 1239 | ), 1240 | DomainGroup( 1241 | name="GitHub Static", 1242 | group_type=GroupType.SHARED, 1243 | domains=[ 1244 | "avatars.githubusercontent.com", 1245 | "avatars0.githubusercontent.com", 1246 | "avatars1.githubusercontent.com", 1247 | "avatars2.githubusercontent.com", 1248 | "avatars3.githubusercontent.com", 1249 | "avatars4.githubusercontent.com", 1250 | "avatars5.githubusercontent.com", 1251 | "camo.githubusercontent.com", 1252 | "cloud.githubusercontent.com", 1253 | "desktop.githubusercontent.com", 1254 | "favicons.githubusercontent.com", 1255 | "github.map.fastly.net", 1256 | "media.githubusercontent.com", 1257 | "objects.githubusercontent.com", 1258 | "private-user-images.githubusercontent.com", 1259 | "raw.githubusercontent.com", 1260 | "user-images.githubusercontent.com", 1261 | ], 1262 | ips={}, 1263 | ), 1264 | DomainGroup( 1265 | name="TMDB themoviedb", 1266 | group_type=GroupType.SHARED, 1267 | domains=[ 1268 | "tmdb.org", 1269 | "api.tmdb.org", 1270 | "files.tmdb.org", 1271 | "themoviedb.org", 1272 | "api.themoviedb.org", 1273 | "www.themoviedb.org", 1274 | "auth.themoviedb.org", 1275 | ], 1276 | ips={ 1277 | "18.239.36.98", 1278 | "108.160.169.178", 1279 | "18.165.122.73", 1280 | "13.249.146.88", 1281 | "13.224.167.74", 1282 | "13.249.146.96", 1283 | "99.86.4.122", 1284 | "108.160.170.44", 1285 | "108.160.169.54", 1286 | "98.159.108.58", 1287 | "13.226.225.4", 1288 | "31.13.80.37", 1289 | "202.160.128.238", 1290 | "13.224.167.16", 1291 | "199.96.63.53", 1292 | "104.244.43.6", 1293 | "18.239.36.122", 1294 | "66.220.149.32", 1295 | "108.157.14.15", 1296 | "202.160.128.14", 1297 | "52.85.242.44", 1298 | "199.59.149.207", 1299 | "54.230.129.92", 1300 | "54.230.129.11", 1301 | "103.240.180.117", 1302 | "66.220.148.145", 1303 | "54.192.175.79", 1304 | "143.204.68.100", 1305 | "31.13.84.2", 1306 | "18.239.36.64", 1307 | "52.85.242.124", 1308 | "54.230.129.83", 1309 | "18.165.122.27", 1310 | "13.33.88.3", 1311 | "202.160.129.36", 1312 | "108.157.14.112", 1313 | "99.86.4.16", 1314 | "199.59.149.237", 1315 | "199.59.148.202", 1316 | "54.230.129.74", 1317 | "202.160.128.40", 1318 | "199.16.156.39", 1319 | "13.224.167.108", 1320 | "192.133.77.133", 1321 | "168.143.171.154", 1322 | "54.192.175.112", 1323 | "128.242.245.43", 1324 | "54.192.175.108", 1325 | "54.192.175.87", 1326 | "199.59.148.229", 1327 | "143.204.68.22", 1328 | "13.33.88.122", 1329 | "52.85.242.73", 1330 | "18.165.122.87", 1331 | "168.143.162.58", 1332 | "103.228.130.61", 1333 | "128.242.240.180", 1334 | "99.86.4.8", 1335 | "104.244.46.52", 1336 | "199.96.58.85", 1337 | "13.226.225.73", 1338 | "128.121.146.109", 1339 | "69.30.25.21", 1340 | "13.249.146.22", 1341 | "13.249.146.87", 1342 | "157.240.12.5", 1343 | "3.162.38.113", 1344 | "143.204.68.72", 1345 | "104.244.43.52", 1346 | "13.224.167.10", 1347 | "3.162.38.31", 1348 | "3.162.38.11", 1349 | "3.162.38.66", 1350 | "202.160.128.195", 1351 | "162.125.6.1", 1352 | "104.244.43.128", 1353 | "18.165.122.23", 1354 | "99.86.4.35", 1355 | "108.160.165.212", 1356 | "108.157.14.27", 1357 | "13.226.225.44", 1358 | "157.240.9.36", 1359 | "13.33.88.37", 1360 | "18.239.36.92", 1361 | "199.59.148.247", 1362 | "13.33.88.97", 1363 | "31.13.84.34", 1364 | "124.11.210.175", 1365 | "13.226.225.52", 1366 | "31.13.86.21", 1367 | "108.157.14.86", 1368 | "143.204.68.36", 1369 | }, 1370 | ), 1371 | DomainGroup( 1372 | name="TMDB 封面", 1373 | group_type=GroupType.SHARED, 1374 | domains=["image.tmdb.org", "images.tmdb.org"], 1375 | ips={ 1376 | "89.187.162.242", 1377 | "169.150.249.167", 1378 | "143.244.50.209", 1379 | "143.244.50.210", 1380 | "143.244.50.88", 1381 | "143.244.50.82", 1382 | "169.150.249.165", 1383 | "143.244.49.178", 1384 | "143.244.49.179", 1385 | "143.244.50.89", 1386 | "143.244.50.212", 1387 | "169.150.207.215", 1388 | "169.150.249.163", 1389 | "143.244.50.85", 1390 | "143.244.50.91", 1391 | "143.244.50.213", 1392 | "169.150.249.164", 1393 | "169.150.249.162", 1394 | "169.150.249.166", 1395 | "143.244.49.183", 1396 | "143.244.49.177", 1397 | "143.244.50.83", 1398 | "138.199.9.104", 1399 | "169.150.249.169", 1400 | "143.244.50.214", 1401 | "79.127.213.217", 1402 | "143.244.50.87", 1403 | "143.244.50.84", 1404 | "169.150.249.168", 1405 | "143.244.49.180", 1406 | "143.244.50.86", 1407 | "143.244.50.90", 1408 | "143.244.50.211", 1409 | }, 1410 | ), 1411 | DomainGroup( 1412 | name="IMDB 网页", 1413 | group_type=GroupType.SHARED, 1414 | domains=[ 1415 | "imdb.com", 1416 | "www.imdb.com", 1417 | "secure.imdb.com", 1418 | "s.media-imdb.com", 1419 | "us.dd.imdb.com", 1420 | "www.imdb.to", 1421 | "imdb-webservice.amazon.com", 1422 | "origin-www.imdb.com", 1423 | ], 1424 | ips={}, 1425 | ), 1426 | DomainGroup( 1427 | name="IMDB CDN", 1428 | group_type=GroupType.SHARED, 1429 | domains=[ 1430 | "m.media-amazon.com", 1431 | "Images-na.ssl-images-amazon.com", 1432 | "images-fe.ssl-images-amazon.com", 1433 | "images-eu.ssl-images-amazon.com", 1434 | "ia.media-imdb.com", 1435 | "f.media-amazon.com", 1436 | "imdb-video.media-imdb.com", 1437 | "dqpnq362acqdi.cloudfront.net", 1438 | ], 1439 | ips={}, 1440 | ), 1441 | DomainGroup( 1442 | name="Google 翻译", 1443 | group_type=GroupType.SHARED, 1444 | domains=[ 1445 | "translate.google.com", 1446 | "translate.googleapis.com", 1447 | "translate-pa.googleapis.com", 1448 | "jnn-pa.googleapis.com", 1449 | ], 1450 | ips={ 1451 | "108.177.127.214", 1452 | "108.177.97.141", 1453 | "142.250.101.157", 1454 | "142.250.110.102", 1455 | "142.250.141.100", 1456 | "142.250.145.113", 1457 | "142.250.145.139", 1458 | "142.250.157.133", 1459 | "142.250.157.149", 1460 | "142.250.176.6", 1461 | "142.250.181.232", 1462 | "142.250.183.106", 1463 | "142.250.187.139", 1464 | "142.250.189.6", 1465 | "142.250.196.174", 1466 | "142.250.199.161", 1467 | "142.250.199.75", 1468 | "142.250.204.37", 1469 | "142.250.204.38", 1470 | "142.250.204.49", 1471 | "142.250.27.113", 1472 | "142.250.4.136", 1473 | "142.250.66.10", 1474 | "142.250.76.35", 1475 | "142.251.1.102", 1476 | "142.251.1.136", 1477 | "142.251.163.91", 1478 | "142.251.165.101", 1479 | "142.251.165.104", 1480 | "142.251.165.106", 1481 | "142.251.165.107", 1482 | "142.251.165.110", 1483 | "142.251.165.112", 1484 | "142.251.165.122", 1485 | "142.251.165.133", 1486 | "142.251.165.139", 1487 | "142.251.165.146", 1488 | "142.251.165.152", 1489 | "142.251.165.155", 1490 | "142.251.165.164", 1491 | "142.251.165.165", 1492 | "142.251.165.193", 1493 | "142.251.165.195", 1494 | "142.251.165.197", 1495 | "142.251.165.201", 1496 | "142.251.165.82", 1497 | "142.251.165.94", 1498 | "142.251.178.105", 1499 | "142.251.178.110", 1500 | "142.251.178.114", 1501 | "142.251.178.117", 1502 | "142.251.178.122", 1503 | "142.251.178.137", 1504 | "142.251.178.146", 1505 | "142.251.178.164", 1506 | "142.251.178.166", 1507 | "142.251.178.181", 1508 | "142.251.178.190", 1509 | "142.251.178.195", 1510 | "142.251.178.197", 1511 | "142.251.178.199", 1512 | "142.251.178.200", 1513 | "142.251.178.214", 1514 | "142.251.178.83", 1515 | "142.251.178.84", 1516 | "142.251.178.88", 1517 | "142.251.178.92", 1518 | "142.251.178.99", 1519 | "142.251.2.139", 1520 | "142.251.221.121", 1521 | "142.251.221.129", 1522 | "142.251.221.138", 1523 | "142.251.221.98", 1524 | "142.251.40.104", 1525 | "142.251.41.14", 1526 | "142.251.41.36", 1527 | "142.251.42.197", 1528 | "142.251.8.155", 1529 | "142.251.8.189", 1530 | "172.217.16.210", 1531 | "172.217.164.103", 1532 | "172.217.168.203", 1533 | "172.217.168.215", 1534 | "172.217.168.227", 1535 | "172.217.169.138", 1536 | "172.217.17.104", 1537 | "172.217.171.228", 1538 | "172.217.175.23", 1539 | "172.217.19.72", 1540 | "172.217.192.149", 1541 | "172.217.192.92", 1542 | "172.217.197.156", 1543 | "172.217.197.91", 1544 | "172.217.204.104", 1545 | "172.217.204.156", 1546 | "172.217.214.112", 1547 | "172.217.218.133", 1548 | "172.217.222.92", 1549 | "172.217.31.136", 1550 | "172.217.31.142", 1551 | "172.217.31.163", 1552 | "172.217.31.168", 1553 | "172.217.31.174", 1554 | "172.253.117.118", 1555 | "172.253.122.154", 1556 | "172.253.62.88", 1557 | "173.194.199.94", 1558 | "173.194.216.102", 1559 | "173.194.220.101", 1560 | "173.194.220.138", 1561 | "173.194.221.101", 1562 | "173.194.222.106", 1563 | "173.194.222.138", 1564 | "173.194.66.137", 1565 | "173.194.67.101", 1566 | "173.194.68.97", 1567 | "173.194.73.106", 1568 | "173.194.73.189", 1569 | "173.194.76.107", 1570 | "173.194.77.81", 1571 | "173.194.79.200", 1572 | "209.85.201.155", 1573 | "209.85.201.198", 1574 | "209.85.201.201", 1575 | "209.85.203.198", 1576 | "209.85.232.101", 1577 | "209.85.232.110", 1578 | "209.85.232.133", 1579 | "209.85.232.195", 1580 | "209.85.233.100", 1581 | "209.85.233.102", 1582 | "209.85.233.105", 1583 | "209.85.233.136", 1584 | "209.85.233.191", 1585 | "209.85.233.93", 1586 | "216.239.32.40", 1587 | "216.58.200.10", 1588 | "216.58.213.8", 1589 | "34.105.140.105", 1590 | "34.128.8.104", 1591 | "34.128.8.40", 1592 | "34.128.8.55", 1593 | "34.128.8.64", 1594 | "34.128.8.70", 1595 | "34.128.8.71", 1596 | "34.128.8.85", 1597 | "34.128.8.97", 1598 | "35.196.72.166", 1599 | "35.228.152.85", 1600 | "35.228.168.221", 1601 | "35.228.195.190", 1602 | "35.228.40.236", 1603 | "64.233.162.102", 1604 | "64.233.163.97", 1605 | "64.233.165.132", 1606 | "64.233.165.97", 1607 | "64.233.169.100", 1608 | "64.233.188.155", 1609 | "64.233.189.133", 1610 | "64.233.189.148", 1611 | "66.102.1.167", 1612 | "66.102.1.88", 1613 | "74.125.133.155", 1614 | "74.125.135.17", 1615 | "74.125.139.97", 1616 | "74.125.142.116", 1617 | "74.125.193.152", 1618 | "74.125.196.195", 1619 | "74.125.201.91", 1620 | "74.125.204.101", 1621 | "74.125.204.113", 1622 | "74.125.204.114", 1623 | "74.125.204.132", 1624 | "74.125.204.141", 1625 | "74.125.204.147", 1626 | "74.125.206.117", 1627 | "74.125.206.137", 1628 | "74.125.206.144", 1629 | "74.125.206.146", 1630 | "74.125.206.154", 1631 | "74.125.21.191", 1632 | "74.125.71.145", 1633 | "74.125.71.152", 1634 | "74.125.71.199", 1635 | "2404:6800:4008:c13::5a", 1636 | "2404:6800:4008:c15::94", 1637 | "2607:f8b0:4004:c07::66", 1638 | "2607:f8b0:4004:c07::71", 1639 | "2607:f8b0:4004:c07::8a", 1640 | "2607:f8b0:4004:c07::8b", 1641 | "2a00:1450:4001:829::201a", 1642 | "2001:67c:2960:6464::d8ef:2076", 1643 | "2001:67c:2960:6464::d8ef:2039", 1644 | "2001:67c:2960:6464::d8ef:2038", 1645 | "2001:67c:2960:6464::d8ef:2028", 1646 | "2001:67c:2960:6464::d8ef:2006", 1647 | "185.199.109.133", 1648 | "185.199.110.133", 1649 | "185.199.111.133", 1650 | "172.217.70.133", 1651 | "142.251.42.55", 1652 | "142.251.43.87", 1653 | "142.251.43.75", 1654 | "142.251.43.139", 1655 | "142.251.223.231", 1656 | "142.251.223.183", 1657 | "172.217.24.75", 1658 | "172.217.24.77", 1659 | "142.250.117.107", 1660 | "142.250.64.181", 1661 | "142.250.117.95", 1662 | "142.251.43.210", 1663 | "142.251.43.185", 1664 | "142.251.43.88", 1665 | "142.251.43.56", 1666 | "142.251.43.119", 1667 | }, 1668 | ), 1669 | DomainGroup( 1670 | name="JetBrain 插件", 1671 | domains=[ 1672 | "plugins.jetbrains.com", 1673 | "download.jetbrains.com", 1674 | "cache-redirector.jetbrains.com", 1675 | ], 1676 | ips={}, 1677 | ), 1678 | ] 1679 | 1680 | # DNS 服务器 1681 | DNS_SERVERS = { 1682 | "international": [ 1683 | # 国际 DNS 服务器 1684 | # 第 1 梯队: 延迟较低 1685 | {"ip": "208.67.222.222", "provider": "OpenDNS", "type": "ipv4"}, # Open DNS 1686 | {"ip": "2620:0:ccc::2", "provider": "OpenDNS", "type": "ipv6"}, # Open DNS 1687 | { 1688 | "ip": "2001:4860:4860::8888", # Google Public DNS 1689 | "provider": "Google", 1690 | "type": "ipv6", 1691 | }, 1692 | { 1693 | "ip": "2001:4860:4860::8844", # Google Public DNS 1694 | "provider": "Google", 1695 | "type": "ipv6", 1696 | }, 1697 | {"ip": "210.184.24.65", "provider": "CPC HK", "type": "ipv4"}, # 香港 1698 | {"ip": "18.163.103.200", "provider": "Amazon HK", "type": "ipv4"}, # 香港 1699 | { 1700 | "ip": "43.251.159.130", 1701 | "provider": "IPTELECOM HK", # 香港 1702 | "type": "ipv4", 1703 | }, 1704 | { 1705 | "ip": "14.198.168.140", 1706 | "provider": "Broadband HK", # 香港 1707 | "type": "ipv4", 1708 | }, 1709 | { 1710 | "ip": "66.203.146.122", 1711 | "provider": "Dimension HK", # 香港 1712 | "type": "ipv4", 1713 | }, 1714 | {"ip": "118.201.189.90", "provider": "SingNet", "type": "ipv4"}, # 新加坡 1715 | {"ip": "1.228.180.5", "provider": "SK Broadband ", "type": "ipv4"}, # 韩国 1716 | {"ip": "183.99.33.6", "provider": "Korea Telecom ", "type": "ipv4"}, # 韩国 1717 | {"ip": "203.248.252.2", "provider": "LG DACOM ", "type": "ipv4"}, # 韩国 1718 | # 第 2 梯队:延迟适中 1719 | # { 1720 | # "ip": "129.250.35.250", 1721 | # "provider": "NTT Communications", # 日本 1722 | # "type": "ipv4" 1723 | # }, 1724 | # { 1725 | # "ip": "168.126.63.1", 1726 | # "provider": "KT DNS", # 韩国 1727 | # "type": "ipv4" 1728 | # }, 1729 | # { 1730 | # "ip": "101.110.50.106", 1731 | # "provider": "Soft Bank", 1732 | # "type": "ipv4" 1733 | # }, 1734 | # { 1735 | # "ip": "202.175.86.206", 1736 | # "provider": "Telecomunicacoes de Macau", #澳门 1737 | # "type": "ipv4" 1738 | # }, 1739 | # { 1740 | # "ip": "45.123.201.235", 1741 | # "provider": "Informacoes Tecnologia de Macau", #澳门 1742 | # "type": "ipv4" 1743 | # }, 1744 | # { 1745 | # "ip": "2400:6180:0:d0::5f6e:4001", 1746 | # "provider": "DigitalOcean", # 新加坡 1747 | # "type": "ipv6" 1748 | # }, 1749 | # { 1750 | # "ip": "2a09::", # DNS.SB 德国 2a11:: 1751 | # "provider": "DNS.SB", 1752 | # "type": "ipv6" 1753 | # }, 1754 | # { 1755 | # "ip": "185.222.222.222", # DNS.SB 德国45.11.45.11 1756 | # "provider": "DNS.SB", 1757 | # "type": "ipv4" 1758 | # }, 1759 | # { 1760 | # "ip": "9.9.9.9", # Quad9 DNS 1761 | # "provider": "Quad9", 1762 | # "type": "ipv4" 1763 | # }, 1764 | # { 1765 | # "ip": "149.112.112.112", # Quad9 DNS 1766 | # "provider": "Quad9", 1767 | # "type": "ipv4" 1768 | # }, 1769 | # { 1770 | # "ip": "208.67.222.222", # Open DNS 1771 | # "provider": "OpenDNS", 1772 | # "type": "ipv4" 1773 | # }, 1774 | # { 1775 | # "ip": "2620:0:ccc::2", # Open DNS 1776 | # "provider": "OpenDNS", 1777 | # "type": "ipv6" 1778 | # }, 1779 | # { 1780 | # "ip": "2620:fe::fe", # Quad9 1781 | # "provider": "Quad9", 1782 | # "type": "ipv6" 1783 | # }, 1784 | # { 1785 | # "ip": "2620:fe::9", # Quad9 1786 | # "provider": "Quad9", 1787 | # "type": "ipv6" 1788 | # }, 1789 | # { 1790 | # "ip": "77.88.8.1", 1791 | # "provider": "Yandex DNS",# 俄国 1792 | # "type": "ipv4" 1793 | # }, 1794 | # { 1795 | # "ip": "2a02:6b8::feed:0ff",# 俄国 1796 | # "provider": "Yandex DNS", 1797 | # "type": "ipv6" 1798 | # }, 1799 | ], 1800 | "china_mainland": [ 1801 | # 国内 DNS 服务器 1802 | # 第 1 梯队:正确解析Google翻译 1803 | # 首选:延迟较低,相对稳定: 1804 | {"ip": "114.114.114.114", "provider": "114DNS", "type": "ipv4"}, # 114 DNS 1805 | { 1806 | "ip": "1.1.8.8", # 上海牙木科技|联通机房 1807 | "provider": "上海牙木科技|联通机房", 1808 | "type": "ipv4", 1809 | }, 1810 | # 备选:延迟一般: 1811 | # { 1812 | # "ip": "180.76.76.76", # 百度 1813 | # "provider": "Baidu", 1814 | # "type": "ipv4" 1815 | # }, 1816 | # { 1817 | # "ip": "202.46.33.250", # 上海通讯 1818 | # "provider": "Shanghai Communications", 1819 | # "type": "ipv4" 1820 | # }, 1821 | # { 1822 | # "ip": "202.46.34.75", # 上海通讯 1823 | # "provider": "Shanghai Communications", 1824 | # "type": "ipv4" 1825 | # },240c::6644 1826 | # 第 2 梯队:无法正确解析Google翻译 1827 | # { 1828 | # "ip": "223.5.5.5", # 阿里云 DNS 1829 | # "provider": "Alibaba", 1830 | # "type": "ipv4" 1831 | # }, 1832 | # { 1833 | # "ip": "2400:3200::1", # 阿里云 DNS 1834 | # "provider": "Alibaba", 1835 | # "type": "ipv6" 1836 | # }, 1837 | # { 1838 | # "ip": "119.29.29.29", # DNSPod DNS 1839 | # "provider": "Tencent", 1840 | # "type": "ipv4" 1841 | # }, 1842 | # { 1843 | # "ip": "2402:4e00::", # DNSPod DNS 1844 | # "provider": "Tencent", 1845 | # "type": "ipv6" 1846 | # }, 1847 | {"ip": "114.114.114.114", "provider": "114DNS", "type": "ipv4"}, # 114 DNS 1848 | # { 1849 | # "ip": "101.226.4.6", # 未360dns 1850 | # "provider": "360dns", 1851 | # "type": "ipv4" 1852 | # } 1853 | ], 1854 | } 1855 | 1856 | @staticmethod 1857 | def get_dns_cache_file() -> Path: 1858 | """获取 DNS 缓存文件路径,并确保目录存在。""" 1859 | if getattr(sys, "frozen", False): 1860 | # 打包后的执行文件路径 1861 | # current_dir = Path(sys.executable).resolve().parent 1862 | # dns_cache_dir = current_dir / "dns_cache" 1863 | 1864 | # 获取用户目录下的 .setHosts,以防止没有写入权限 1865 | dns_cache_dir = ( 1866 | Path(os.getenv("USERPROFILE", os.getenv("HOME"))) 1867 | / ".setHosts" 1868 | / "dns_cache" 1869 | ) 1870 | else: 1871 | # 脚本运行时路径 1872 | current_dir = Path(__file__).resolve().parent 1873 | dns_cache_dir = current_dir / "dns_cache" 1874 | 1875 | dns_cache_dir.mkdir(parents=True, exist_ok=True) # 确保目录存在 1876 | 1877 | # (提示:dns_records.json 文件将存储 A、AAAA 等 DNS 资源记录缓存。) 1878 | return dns_cache_dir / "dns_records.json" # 返回缓存文件路径 1879 | 1880 | 1881 | # -------------------- 主函数入口 -------------------- # 1882 | async def main(): 1883 | rprint("[green]----------------------------------------------------------[/green]") 1884 | rprint( 1885 | "[blue on green]启动 setHosts 自动更新··· [/blue on green]" 1886 | ) 1887 | rprint( 1888 | "[green]----------------------------------------------------------[/green]\n" 1889 | ) 1890 | 1891 | start_time = datetime.now() # 记录程序开始运行时间 1892 | 1893 | # 从配置类中加载DOMAIN_GROUPS、DNS_SERVERS和dns_cache_dir 1894 | DOMAIN_GROUPS = Config.DOMAIN_GROUPS 1895 | dns_servers = Config.DNS_SERVERS 1896 | dns_cache_file = Config.get_dns_cache_file() 1897 | 1898 | # 1.域名解析 1899 | resolver = DomainResolver( 1900 | dns_servers=dns_servers, 1901 | max_latency=args.max_latency, 1902 | dns_cache_file=dns_cache_file, 1903 | ) 1904 | 1905 | # 2.延迟检测 1906 | tester = LatencyTester(hosts_num=args.hosts_num, max_workers=200) 1907 | 1908 | # 3.Hosts文件操作 1909 | hosts_manager = HostsManager() 1910 | 1911 | # 4.初始化 Hosts更新器 参数 1912 | updater = HostsUpdater( 1913 | domain_groups=DOMAIN_GROUPS, 1914 | resolver=resolver, 1915 | tester=tester, 1916 | hosts_manager=hosts_manager, 1917 | ) 1918 | 1919 | if not PrivilegeManager.is_admin() and not args.checkonly: 1920 | rprint( 1921 | "[bold red]需要管理员权限来修改hosts文件。正在尝试提升权限...[/bold red]" 1922 | ) 1923 | PrivilegeManager.run_as_admin() 1924 | 1925 | # 启动 Hosts更新器 1926 | await updater.update_hosts() 1927 | 1928 | # 计算程序运行时间 1929 | end_time = datetime.now() 1930 | total_time = end_time - start_time 1931 | rprint( 1932 | f"[bold]代码运行时间:[/bold] [cyan]{total_time.total_seconds():.2f} 秒[/cyan]" 1933 | ) 1934 | 1935 | if getattr(sys, "frozen", False): 1936 | # 如果打包为可执行程序时 1937 | input("\n任务执行完毕,按任意键退出!") 1938 | 1939 | 1940 | if __name__ == "__main__": 1941 | asyncio.run(main()) 1942 | --------------------------------------------------------------------------------