├── .gitattributes ├── .github ├── bom.current └── workflows │ └── auto-upgrade.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── bom.sh ├── clear-actions-run-history.sh ├── haproxy.cfg ├── images └── screenshot_1.gif ├── privoxy.cfg ├── start.sh └── tor.cfg /.gitattributes: -------------------------------------------------------------------------------- 1 | *.cfg linguist-detectable=false 2 | -------------------------------------------------------------------------------- /.github/bom.current: -------------------------------------------------------------------------------- 1 | alpine-3.22.0 bash-5.2.37 curl-8.14.1 haproxy-3.0.10 privoxy-3.0.34 sed-4.9 tor-0.4.8.16 2 | -------------------------------------------------------------------------------- /.github/workflows/auto-upgrade.yml: -------------------------------------------------------------------------------- 1 | name: auto-upgrade 2 | 3 | on: 4 | schedule: 5 | - cron: '30 7,19 * * *' 6 | workflow_dispatch: 7 | 8 | jobs: 9 | auto-upgrade: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v3 14 | with: 15 | ref: main 16 | - name: Build Docker image 17 | run: docker build --tag local-image . 18 | - name: Compare BoM 19 | id: compare-bom 20 | shell: bash 21 | run: | 22 | CURRENT_BOM="$(cat ${GITHUB_WORKSPACE}/.github/bom.current)" 23 | NEW_BOM="$(docker run --rm local-image /bom.sh)" 24 | echo "Current BOM: ${CURRENT_BOM}" 25 | echo "New BOM: ${NEW_BOM}" 26 | echo "compare-bom-result=$(if [ "${CURRENT_BOM}" = "${NEW_BOM}" ]; then echo NOCHANGE; else echo CHANGED; fi)" >> $GITHUB_OUTPUT 27 | echo "new-bom-data=${NEW_BOM}" >> $GITHUB_OUTPUT 28 | - name: Update BOM record 29 | if: ${{ success() && steps.compare-bom.outputs.compare-bom-result == 'CHANGED' }} 30 | shell: bash 31 | run: echo "${{ steps.compare-bom.outputs.new-bom-data }}" > ${GITHUB_WORKSPACE}/.github/bom.current 32 | - name: Update README 33 | if: ${{ success() && steps.compare-bom.outputs.compare-bom-result == 'CHANGED' }} 34 | shell: bash 35 | run: | 36 | cat ${GITHUB_WORKSPACE}/README.md |tr '\n' '\r' |sed -r "s/(.*\r).*/\1/" |tr '\r' '\n' > ${GITHUB_WORKSPACE}/~README.md 37 | for i in ${{ steps.compare-bom.outputs.new-bom-data }}; do 38 | echo "- ${i}" >> ${GITHUB_WORKSPACE}/~README.md 39 | done 40 | echo "" >> ${GITHUB_WORKSPACE}/~README.md 41 | mv -f ${GITHUB_WORKSPACE}/~README.md ${GITHUB_WORKSPACE}/README.md 42 | 43 | - name: Build multi-platform image (setup QEMU) 44 | if: ${{ success() && steps.compare-bom.outputs.compare-bom-result == 'CHANGED' }} 45 | uses: docker/setup-qemu-action@v2 46 | - name: Build multi-platform image (setup docker-buildx) 47 | if: ${{ success() && steps.compare-bom.outputs.compare-bom-result == 'CHANGED' }} 48 | uses: docker/setup-buildx-action@v2 49 | - name: Login to Docker Hub 50 | if: ${{ success() && steps.compare-bom.outputs.compare-bom-result == 'CHANGED' }} 51 | uses: docker/login-action@v2 52 | with: 53 | username: ${{ secrets.DOCKERHUB_USERNAME }} 54 | password: ${{ secrets.DOCKERHUB_TOKEN }} 55 | - name: Dry-run version bumping to get the new version number 56 | if: ${{ success() && steps.compare-bom.outputs.compare-bom-result == 'CHANGED' }} 57 | id: tag_version_dryrun 58 | uses: mathieudutour/github-tag-action@v6.1 59 | with: 60 | dry_run: true 61 | # align the parameters here with the real run below 62 | github_token: ${{ secrets.GITHUB_TOKEN }} 63 | default_bump: patch 64 | tag_prefix: '' 65 | - name: Build multi-platform image 66 | if: ${{ success() && steps.compare-bom.outputs.compare-bom-result == 'CHANGED' }} 67 | uses: docker/build-push-action@v4 68 | with: 69 | context: . 70 | platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 71 | push: true 72 | tags: zhaowde/rotating-tor-http-proxy:latest,zhaowde/rotating-tor-http-proxy:${{ steps.tag_version_dryrun.outputs.new_version }} 73 | 74 | - name: Commit changes 75 | if: ${{ success() && steps.compare-bom.outputs.compare-bom-result == 'CHANGED' }} 76 | uses: stefanzweifel/git-auto-commit-action@v4 77 | with: 78 | commit_message: auto upgrade triggered by BoM changes 79 | skip_fetch: true 80 | - name: Bump version and push tag 81 | if: ${{ success() && steps.compare-bom.outputs.compare-bom-result == 'CHANGED' }} 82 | id: tag_version 83 | uses: mathieudutour/github-tag-action@v6.1 84 | with: 85 | # align the parameters here with the dry-run above 86 | github_token: ${{ secrets.GITHUB_TOKEN }} 87 | default_bump: patch 88 | tag_prefix: '' 89 | - name: Create a release 90 | if: ${{ success() && steps.compare-bom.outputs.compare-bom-result == 'CHANGED' }} 91 | uses: actions/create-release@v1 92 | env: 93 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 94 | with: 95 | tag_name: ${{ steps.tag_version.outputs.new_tag }} 96 | release_name: Release ${{ steps.tag_version.outputs.new_tag }} 97 | body: ${{ steps.tag_version.outputs.changelog }} 98 | - name: Merge (back) to develop branch 99 | if: ${{ success() && steps.compare-bom.outputs.compare-bom-result == 'CHANGED' }} 100 | uses: everlytic/branch-merge@1.1.5 101 | with: 102 | github_token: ${{ secrets.GITHUB_TOKEN }} 103 | source_ref: main 104 | target_branch: develop 105 | - name: Update the Docker Hub description 106 | # For non-paid account, DockerHub has disabled the auto-build function, which is the prerequisite for 107 | # auto-description update. 108 | # We add this step here to update the description "manually." 109 | if: ${{ success() && steps.compare-bom.outputs.compare-bom-result == 'CHANGED' }} 110 | uses: peter-evans/dockerhub-description@v3 111 | with: 112 | username: ${{ secrets.DOCKERHUB_USERNAME }} 113 | password: ${{ secrets.DOCKERHUB_TOKEN }} 114 | repository: zhaowde/rotating-tor-http-proxy 115 | short-description: ${{ github.event.repository.description }} 116 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Windows template 3 | # Windows thumbnail cache files 4 | Thumbs.db 5 | Thumbs.db:encryptable 6 | ehthumbs.db 7 | ehthumbs_vista.db 8 | 9 | # Dump file 10 | *.stackdump 11 | 12 | # Folder config file 13 | [Dd]esktop.ini 14 | 15 | # Recycle Bin used on file shares 16 | $RECYCLE.BIN/ 17 | 18 | # Windows Installer files 19 | *.cab 20 | *.msi 21 | *.msix 22 | *.msm 23 | *.msp 24 | 25 | # Windows shortcuts 26 | *.lnk 27 | 28 | ### Linux template 29 | *~ 30 | 31 | # temporary files which can be created if a process still has a handle open of a deleted file 32 | .fuse_hidden* 33 | 34 | # KDE directory preferences 35 | .directory 36 | 37 | # Linux trash folder which might appear on any partition or disk 38 | .Trash-* 39 | 40 | # .nfs files are created when an open file is removed but is still being accessed 41 | .nfs* 42 | 43 | ### VisualStudioCode template 44 | .vscode/* 45 | !.vscode/settings.json 46 | !.vscode/tasks.json 47 | !.vscode/launch.json 48 | !.vscode/extensions.json 49 | *.code-workspace 50 | 51 | # Local History for Visual Studio Code 52 | .history/ 53 | 54 | ### JetBrains template 55 | .idea/ 56 | .idea_modules/ 57 | 58 | ### macOS template 59 | # General 60 | .DS_Store 61 | .AppleDouble 62 | .LSOverride 63 | 64 | # Icon must end with two \r 65 | Icon 66 | 67 | # Thumbnails 68 | ._* 69 | 70 | # Files that might appear in the root of a volume 71 | .DocumentRevisions-V100 72 | .fseventsd 73 | .Spotlight-V100 74 | .TemporaryItems 75 | .Trashes 76 | .VolumeIcon.icns 77 | .com.apple.timemachine.donotpresent 78 | 79 | # Directories potentially created on remote AFP share 80 | .AppleDB 81 | .AppleDesktop 82 | Network Trash Folder 83 | Temporary Items 84 | .apdisk 85 | 86 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | 3 | ENV \ 4 | # sets the number of tor instances 5 | TOR_INSTANCES=10 \ 6 | # sets the interval (in seconds) to rebuild tor circuits 7 | TOR_REBUILD_INTERVAL=1800 8 | 9 | EXPOSE 3128/tcp 4444/tcp 10 | 11 | COPY tor.cfg privoxy.cfg haproxy.cfg start.sh bom.sh / 12 | 13 | RUN apk --no-cache --no-progress --quiet upgrade && \ 14 | # alpine has a POSIX sed from busybox, for the log re-formatting, GNU sed is required to converting a capture group to lowercase 15 | apk --no-cache --no-progress --quiet add tor bash privoxy haproxy curl sed && \ 16 | # 17 | # directories and files 18 | mv /tor.cfg /etc/tor/torrc.default && \ 19 | mv /privoxy.cfg /etc/privoxy/config.templ && \ 20 | mv /haproxy.cfg /etc/haproxy/haproxy.cfg.default && \ 21 | chmod +x /start.sh && \ 22 | chmod +x /bom.sh && \ 23 | # 24 | # prepare for low-privilege execution \ 25 | addgroup proxy && \ 26 | adduser -S -D -u 1000 -G proxy proxy && \ 27 | touch /etc/haproxy/haproxy.cfg && \ 28 | chown -R proxy: /etc/haproxy/ && \ 29 | mkdir -p /var/lib/haproxy && \ 30 | chown -R proxy: /var/lib/haproxy && \ 31 | mkdir -p /var/local/haproxy && \ 32 | chown -R proxy: /var/local/haproxy && \ 33 | touch /etc/tor/torrc && \ 34 | chown -R proxy: /etc/tor/ && \ 35 | chown -R proxy: /etc/privoxy/ && \ 36 | mkdir -p /var/local/tor && \ 37 | chown -R proxy: /var/local/tor && \ 38 | mkdir -p /var/local/privoxy && \ 39 | chown -R proxy: /var/local/privoxy && \ 40 | chown -R proxy: /var/log/privoxy && \ 41 | # 42 | # cleanup 43 | # 44 | # tor 45 | rm -rf /etc/tor/torrc.sample && \ 46 | # privoxy 47 | rm -rf /etc/privoxy/*.new /etc/logrotate.d/privoxy && \ 48 | # files like /etc/shadow-, /etc/passwd- 49 | find / -xdev -type f -regex '.*-$' -exec rm -f {} \; && \ 50 | # temp and cache 51 | rm -rf /var/cache/apk/* /usr/share/doc /usr/share/man/ /usr/share/info/* /var/cache/man/* /tmp/* /etc/fstab && \ 52 | # init scripts 53 | rm -rf /etc/init.d /lib/rc /etc/conf.d /etc/inittab /etc/runlevels /etc/rc.conf && \ 54 | # kernel tunables 55 | rm -rf /etc/sysctl* /etc/modprobe.d /etc/modules /etc/mdev.conf /etc/acpi 56 | 57 | STOPSIGNAL SIGINT 58 | 59 | USER proxy 60 | 61 | CMD ["/start.sh"] 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Zhao Wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![GitHub](https://img.shields.io/github/license/zhaow-de/rotating-tor-http-proxy) 2 | ![Docker Image Version (latest semver)](https://img.shields.io/docker/v/zhaowde/rotating-tor-http-proxy?sort=semver) 3 | ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/zhaow-de/rotating-tor-http-proxy/auto-upgrade.yml) 4 | [![Docker Pulls](https://img.shields.io/docker/pulls/zhaowde/rotating-tor-http-proxy.svg)](https://hub.docker.com/r/zhaowde/rotating-tor-http-proxy/) 5 | ![Docker Image Size (latest semver)](https://img.shields.io/docker/image-size/zhaowde/rotating-tor-http-proxy?sort=semver) 6 | 7 | # rotating-tor-http-proxy 8 | 9 | This Docker image provides one HTTP proxy endpoint with many IP addresses for use scenarios like web crawling. 10 | 11 | ![Screenshot](https://raw.githubusercontent.com/zhaow-de/rotating-tor-http-proxy/main/images/screenshot_1.gif) 12 | 13 | Behind the scene, it has a HAProxy sitting in front of multiple pairs of Privoxy-Tor. The HAProxy dispatches the incoming 14 | requests to the Privoxy instances with a round-robin strategy. 15 | 16 | ## Usage 17 | 18 | This image is multi-platform enabled, currently supporting: 19 | - amd64 (x86_64) 20 | - arm64 (aarch64) 21 | - arm/v7 (armhf) 22 | - arm/v6 (armel) 23 | 24 | ### Simple case 25 | ```shell 26 | docker run --rm -it -p 3128:3128 zhaowde/rotating-tor-http-proxy 27 | ``` 28 | At the host, `127.0.0.1:3128` is the HTTP/HTTPS proxy address. 29 | 30 | ### Moreover 31 | 32 | ```shell 33 | docker run --rm -it -p 3128:3128 -p 4444:4444 -e "TOR_INSTANCES=5" -e "TOR_REBUILD_INTERVAL=3600" -e "TOR_EXIT_COUNTRY=de,ch,at" zhaowde/rotating-tor-http-proxy 34 | ``` 35 | 36 | #### Port `4444/TCP` 37 | 38 | Port `4444/TCP` can be mapped to the host if HAProxy stats information is needed. With `docker run -p 4444:4444`, the HAProxy statistics 39 | report is available at http://127.0.0.1:4444. An [article](https://www.haproxy.com/blog/exploring-the-haproxy-stats-page/) from the 40 | HAProxy official blog explains in detail how to understand this report. 41 | 42 | #### `TOR_INSTANCES` 43 | 44 | Environment variable `TOR_INSTANCES` can be used to config the number of concurrent Tor clients (as well as the associated Privoxy 45 | instances). The default is 10, and the valid value is purposely limited to the range between 1 and 40. 46 | 47 | #### `TOR_REBUILD_INTERVAL` 48 | Each Tor client attempts to build a new circuit (results in a new outbound IP address) every 30 seconds. Every 30 minutes, this image 49 | rebuilds all the circuits. This interval can be changed with environment variable `TOR_REBUILD_INTERVAL`, the default value is 1800 50 | seconds, while it can be set up any number greater than 600 seconds. 51 | 52 | #### `TOR_EXIT_COUNTRY` 53 | 54 | For some crawling tasks, it requires limiting the IP addresses to a certain country to avoid triggering the unnecessary recaptcha verification. 55 | Environment variable `TOR_EXIT_COUNTRY` can be used to specify a country (or a list of countries). 56 | Please note the following remarks: 57 | * **Note 1**: Modifying the way that Tor creates its circuits is strongly discouraged, overriding the entry/exit nodes can compromise the anonymity. 58 | * **Note 2**: The tor bridge is configured to strictly respect the exit countries if it is specified. 59 | For the countries with too fewer exit nodes (e.g., Switzerland), it would take significantly longer time to build up the circuit. 60 | * **Note 3**: The environment variable accepts a single country code (e.g., `TOR_EXIT_COUNTRY=de`) or a comma-separated list (e.g., `TOR_EXIT_COUNTRY=de,at,ch`) 61 | * **Note 4**: The acceptable country codes: 62 | 63 | | Country | Code | Country | Code | Country | Code | Country | Code | Country | Code | Country | Code | 64 | |---------------------------------------|------|---------------------------|------|---------------------------------------|------|-----------------------------|------|-------------------------------|-------------|------------------------------|------| 65 | | ASCENSION ISLAND | `ac` | AFGHANISTAN | `af` | ALAND | `ax` | ALBANIA | `al` | ALGERIA | `dz` | ANDORRA | `ad` | 66 | | ANGOLA | `ao` | ANGUILLA | `ai` | ANTARCTICA | `aq` | ANTIGUA AND BARBUDA | `ag` | ARGENTINA REPUBLIC | `ar` | ARMENIA | `am` | 67 | | ARUBA | `aw` | AUSTRALIA | `au` | AUSTRIA | `at` | AZERBAIJAN | `az` | BAHAMAS | `bs` | BAHRAIN | `bh` | 68 | | BANGLADESH | `bd` | BARBADOS | `bb` | BELARUS | `by` | BELGIUM | `be` | BELIZE | `bz` | BENIN | `bj` | 69 | | BERMUDA | `bm` | BHUTAN | `bt` | BOLIVIA | `bo` | BOSNIA AND HERZEGOVINA | `ba` | BOTSWANA | `bw` | BOUVET ISLAND | `bv` | 70 | | BRAZIL | `br` | BRITISH INDIAN OCEAN TERR | `io` | BRITISH VIRGIN ISLANDS | `vg` | BRUNEI DARUSSALAM | `bn` | BULGARIA | `bg` | BURKINA FASO | `bf` | 71 | | BURUNDI | `bi` | CAMBODIA | `kh` | CAMEROON | `cm` | CANADA | `ca` | CAPE VERDE | `cv` | CAYMAN ISLANDS | `ky` | 72 | | CENTRAL AFRICAN REPUBLIC | `cf` | CHAD | `td` | CHILE | `cl` | PEOPLE'S REPUBLIC OF CHINA | `cn` | CHRISTMAS ISLANDS | `cx` | COCOS ISLANDS | `cc` | 73 | | COLOMBIA | `co` | COMORAS | `km` | CONGO | `cg` | CONGO (DEMOCRATIC REPUBLIC) | `cd` | COOK ISLANDS | `ck` | COSTA RICA | `cr` | 74 | | COTE D IVOIRE | `ci` | CROATIA | `hr` | CUBA | `cu` | CYPRUS | `cy` | CZECH REPUBLIC | `cz` | DENMARK | `dk` | 75 | | DJIBOUTI | `dj` | DOMINICA | `dm` | DOMINICAN REPUBLIC | `do` | EAST TIMOR | `tp` | ECUADOR | `ec` EGYPT | `eg` | 76 | | EL SALVADOR | `sv` | EQUATORIAL GUINEA | `gq` | ESTONIA | `ee` | ETHIOPIA | `et` | FALKLAND ISLANDS | `fk` | FAROE ISLANDS | `fo` | 77 | | FIJI | `fj` | FINLAND | `fi` | FRANCE | `fr` | FRANCE METROPOLITAN | `fx` | FRENCH GUIANA | `gf` | FRENCH POLYNESIA | `pf` | 78 | | FRENCH SOUTHERN TERRITORIES | `tf` | GABON | `ga` | GAMBIA | `gm` | GEORGIA | `ge` | GERMANY | `de` | GHANA | `gh` | 79 | | GIBRALTER | `gi` | GREECE | `gr` | GREENLAND | `gl` | GRENADA | `gd` | GUADELOUPE | `gp` | GUAM | `gu` | 80 | | GUATEMALA | `gt` | GUINEA | `gn` | GUINEA-BISSAU | `gw` | GUYANA | `gy` | HAITI | `ht` | HEARD & MCDONALD ISLAND | `hm` | 81 | | HONDURAS | `hn` | HONG KONG | `hk` | HUNGARY | `hu` | ICELAND | `is` | INDIA | `in` | INDONESIA | `id` | 82 | | IRAN, ISLAMIC REPUBLIC OF | `ir` | IRAQ | `iq` | IRELAND | `ie` | ISLE OF MAN | `im` | ISRAEL | `il` | ITALY | `it` | 83 | | JAMAICA | `jm` | JAPAN | `jp` | JORDAN | `jo` | KAZAKHSTAN | `kz` | KENYA | `ke` | KIRIBATI | `ki` | 84 | | KOREA, DEM. PEOPLES REP OF | `kp` | KOREA, REPUBLIC OF | `kr` | KUWAIT | `kw` | KYRGYZSTAN | `kg` | LAO PEOPLE'S DEM. REPUBLIC | `la` | LATVIA | `lv` | 85 | | LEBANON | `lb` | LESOTHO | `ls` | LIBERIA | `lr` | LIBYAN ARAB JAMAHIRIYA | `ly` | LIECHTENSTEIN | `li` | LITHUANIA | `lt` | 86 | | LUXEMBOURG | `lu` | MACAO | `mo` | MACEDONIA | `mk` | MADAGASCAR | `mg` | MALAWI | `mw` | MALAYSIA | `my` | 87 | | MALDIVES | `mv` | MALI | `ml` | MALTA | `mt` | MARSHALL ISLANDS | `mh` | MARTINIQUE | `mq` | MAURITANIA | `mr` | 88 | | MAURITIUS | `mu` | MAYOTTE | `yt` | MEXICO | `mx` | MICRONESIA | `fm` | MOLDAVA REPUBLIC OF | `md` | MONACO | `mc` | 89 | | MONGOLIA | `mn` | MONTENEGRO | `me` | MONTSERRAT | `ms` | MOROCCO | `ma` | MOZAMBIQUE | `mz` | MYANMAR | `mm` | 90 | | NAMIBIA | `na` | NAURU | `nr` | NEPAL | `np` | NETHERLANDS ANTILLES | `an` | NETHERLANDS, THE | `nl` | NEW CALEDONIA | `nc` | 91 | | NEW ZEALAND | `nz` | NICARAGUA | `ni` | NIGER | `ne` | NIGERIA | `ng` | NIUE | `nu` | NORFOLK ISLAND | `nf` | 92 | | NORTHERN MARIANA ISLANDS | `mp` | NORWAY | `no` | OMAN | `om` | PAKISTAN | `pk` | PALAU | `pw` | PALESTINE | `ps` | 93 | | PANAMA | `pa` | PAPUA NEW GUINEA | `pg` | PARAGUAY | `py` | PERU | `pe` | PHILIPPINES (REPUBLIC OF THE) | `ph` | PITCAIRN | `pn` | 94 | | POLAND | `pl` | PORTUGAL | `pt` | PUERTO RICO | `pr` | QATAR | `qa` | REUNION | `re` | ROMANIA | `ro` | 95 | | RUSSIAN FEDERATION | `ru` | RWANDA | `rw` | SAMOA | `ws` | SAN MARINO | `sm` | SAO TOME/PRINCIPE | `st` | SAUDI ARABIA | `sa` | 96 | | SCOTLAND | `uk` | SENEGAL | `sn` | SERBIA | `rs` | SEYCHELLES | `sc` | SIERRA LEONE | `sl` | SINGAPORE | `sg` | 97 | | SLOVAKIA | `sk` | SLOVENIA | `si` | SOLOMON ISLANDS | `sb` | SOMALIA | `so` | SOMOA,GILBERT,ELLICE ISLANDS | `as` | SOUTH AFRICA | `za` | 98 | | SOUTH GEORGIA, SOUTH SANDWICH ISLANDS | `gs` | SOVIET UNION | `su` | SPAIN | `es` | SRI LANKA | `lk` | ST. HELENA | `sh` | ST. KITTS AND NEVIS | `kn` | 99 | | ST. LUCIA | `lc` | ST. PIERRE AND MIQUELON | `pm` | ST. VINCENT & THE GRENADINES | `vc` | SUDAN | `sd` | SURINAME | `sr` | SVALBARD AND JAN MAYEN | `sj` | 100 | | SWAZILAND | `sz` | SWEDEN | `se` | SWITZERLAND | `ch` | SYRIAN ARAB REPUBLIC | `sy` | TAIWAN | `tw` | TAJIKISTAN | `tj` | 101 | | TANZANIA, UNITED REPUBLIC OF | `tz` | THAILAND | `th` | TOGO | `tg` | TOKELAU | `tk` | TONGA | `to` | TRINIDAD AND TOBAGO | `tt` | 102 | | TUNISIA | `tn` | TURKEY | `tr` | TURKMENISTAN | `tm` | TURKS AND CALCOS ISLANDS | `tc` | TUVALU | `tv` | UGANDA | `ug` | 103 | | UKRAINE | `ua` | UNITED ARAB EMIRATES | `ae` | UNITED KINGDOM (no new registrations) | `gb` | UNITED KINGDOM | `uk` | UNITED STATES | `us` | UNITED STATES MINOR OUTL.IS. | `um` | 104 | | URUGUAY | `uy` | UZBEKISTAN | `uz` | VANUATU | `vu` | VATICAN CITY STATE | `va` | VENEZUELA | `ve` | VIET NAM | `vn` | 105 | | VIRGIN ISLANDS (USA) | `vi` | WALLIS AND FUTUNA ISLANDS | `wf` | WESTERN SAHARA | `eh` | YEMEN | `ye` | ZAMBIA | `zm` | ZIMBABWE | `zw` | 106 | 107 | ### Test the proxy 108 | 109 | ```shell 110 | while :; do curl -sx localhost:3128 ifconfig.io; echo ""; sleep 2; done 111 | ``` 112 | 113 | ## Credit 114 | 115 | At GitHub, there are many repos build Docker images to provide HTTP proxy connects to the Tor network. 116 | The project is reinventing the wheels based on many of them. 117 | Remarkably: 118 | - [y4ns0l0/docker-multi-tor](https://github.com/y4ns0l0/docker-multi-tor) creates a setup with multiple pairs of Privoxy-Tor. Having no 119 | HAProxy-like dispatcher, each Privoxy expose itself to the host as a different TCP port. 120 | - [mattes/rotating-proxy](https://github.com/mattes/rotating-proxy) does exactly the same job as this project. However, 121 | 1. it utilizes [Polipo](https://www.irif.fr/~jch/software/polipo/) as the HTTP-SOCKS proxy adapter. Polipo ceased to be maintained on 122 | 6 November 2016 123 | 2. the base image is Ubuntu 14.04, which it too heavy for this case, and out-of-maintenance as well 124 | 3. the main control logic is written in Ruby 125 | 126 | ## Bill-of-Material 127 | 128 | 129 | 130 | - alpine-3.22.0 131 | - bash-5.2.37 132 | - curl-8.14.1 133 | - haproxy-3.0.10 134 | - privoxy-3.0.34 135 | - sed-4.9 136 | - tor-0.4.8.16 137 | 138 | -------------------------------------------------------------------------------- /bom.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | BOM=$(echo "alpine-$(cat /etc/alpine-release)" && apk list --installed 2>&1 |grep -e '^bash\|^curl\|^haproxy\|^privoxy\|^sed\|^tor' |awk '{print $1}' |sed -r 's/\-r[0-9]+$//' |sort) 4 | 5 | # the last `xargs` step is to remove the trailing whitespace (see https://stackoverflow.com/a/12973694) 6 | echo "${BOM}" |tr '\n' ' ' |xargs 7 | -------------------------------------------------------------------------------- /clear-actions-run-history.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | WORKFLOW_ID=$(gh api repos/zhaow-de/rotating-tor-http-proxy/actions/workflows --paginate | jq '.workflows[] | select(.["name"] == "auto-upgrade") | .id') 4 | RUN_IDS=$(gh api repos/zhaow-de/rotating-tor-http-proxy/actions/workflows/$WORKFLOW_ID/runs --paginate | jq '.workflow_runs[].id') 5 | 6 | for run_id in ${RUN_IDS} 7 | do 8 | echo "Deleting Run ID $run_id" 9 | gh api repos/zhaow-de/rotating-tor-http-proxy/actions/runs/$run_id -X DELETE >/dev/null 10 | done 11 | -------------------------------------------------------------------------------- /haproxy.cfg: -------------------------------------------------------------------------------- 1 | global 2 | log stdout format raw local0 3 | pidfile /var/local/haproxy/haproxy.pid 4 | maxconn 1024 5 | user proxy 6 | 7 | defaults 8 | mode http 9 | log global 10 | log-format "%ST %B %{+Q}r" 11 | option dontlognull 12 | option http-server-close 13 | option forwardfor except 127.0.0.0/8 14 | option redispatch 15 | retries 3 16 | timeout http-request 10s 17 | timeout queue 1m 18 | timeout connect 10s 19 | timeout client 1m 20 | timeout server 1m 21 | timeout http-keep-alive 10s 22 | timeout check 10s 23 | maxconn 1024 24 | 25 | listen stats 26 | bind 0.0.0.0:4444 27 | mode http 28 | log global 29 | maxconn 30 30 | timeout client 100s 31 | timeout server 100s 32 | timeout connect 100s 33 | timeout queue 100s 34 | stats enable 35 | stats hide-version 36 | stats refresh 30s 37 | stats show-desc Rotating Tor HTTP proxy 38 | stats show-legends 39 | stats show-node 40 | stats uri / 41 | 42 | frontend main 43 | bind 0.0.0.0:3128 44 | default_backend privoxy 45 | mode http 46 | 47 | backend privoxy 48 | balance roundrobin 49 | -------------------------------------------------------------------------------- /images/screenshot_1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zhaow-de/rotating-tor-http-proxy/57a83d2e5d02c872df31eadbe4dbdbe687333f77/images/screenshot_1.gif -------------------------------------------------------------------------------- /privoxy.cfg: -------------------------------------------------------------------------------- 1 | confdir PLACEHOLDER_CONFDIR 2 | templdir /etc/privoxy/templates 3 | logdir /var/log/privoxy 4 | logfile /dev/null 5 | debug 1 6 | debug 1024 7 | debug 4096 8 | debug 8192 9 | listen-address 127.0.0.1:PLACEHOLDER_HTTP_PORT 10 | toggle 0 11 | enforce-blocks 0 12 | buffer-limit 4 13 | enable-proxy-authentication-forwarding 0 14 | forward-socks5 / 127.0.0.1:PLACEHOLDER_SOCKS_PORT . 15 | forward localhost/ . 16 | forwarded-connect-retries 3 17 | keep-alive-timeout 15 18 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function log() { 4 | if [[ $# == 1 ]]; then 5 | level="info" 6 | msg=$1 7 | elif [[ $# == 2 ]]; then 8 | level=$1 9 | msg=$2 10 | fi 11 | echo "$(date -u +"%Y-%m-%dT%H:%M:%SZ") [controller] [${level}] ${msg}" 12 | } 13 | 14 | if ((TOR_INSTANCES < 1 || TOR_INSTANCES > 40)); then 15 | log "fatal" "Environment variable TOR_INSTANCES has to be within the range of 1...40" 16 | exit 1 17 | fi 18 | 19 | if ((TOR_REBUILD_INTERVAL < 600)); then 20 | log "fatal" "Environment variable TOR_REBUILD_INTERVAL has to be bigger than 600 seconds" 21 | # otherwise AWS may complain about it, because http://checkip.amazonaws.com is asked too often 22 | exit 2 23 | fi 24 | 25 | base_tor_socks_port=10000 26 | base_tor_ctrl_port=20000 27 | base_http_port=30000 28 | 29 | log "Start creating a pool of ${TOR_INSTANCES} tor instances..." 30 | 31 | # "reset" the HAProxy config file because it may contain the previous Privoxy instances information from the previous docker run 32 | cp /etc/haproxy/haproxy.cfg.default /etc/haproxy/haproxy.cfg 33 | # same "reset" logic as above 34 | cp /etc/tor/torrc.default /etc/tor/torrc 35 | 36 | if [[ -n $TOR_EXIT_COUNTRY ]]; then 37 | IFS=', ' read -r -a countries <<< "$TOR_EXIT_COUNTRY" 38 | value="" 39 | is_first=1 40 | for country in "${countries[@]}" 41 | do 42 | country=$(xargs <<< "$country") 43 | length=${#country} 44 | if [[ $length -ne 2 ]]; then 45 | continue 46 | fi 47 | if [[ $is_first -ne 1 ]]; then 48 | value="$value," 49 | else 50 | is_first=0 51 | fi 52 | value="$value{$country}" 53 | done 54 | country_str=$(tr '[:upper:]' '[:lower:]' <<< "$value") 55 | if [[ -n $country_str ]]; then 56 | echo ExitNodes "$country_str" StrictNodes 1 >> /etc/tor/torrc 57 | log "Limited the exit nodes to countries: \"${TOR_EXIT_COUNTRY}\"" 58 | fi 59 | fi 60 | 61 | for ((i = 0; i < TOR_INSTANCES; i++)); do 62 | # 63 | # start one tor instance 64 | # 65 | socks_port=$((base_tor_socks_port + i)) 66 | ctrl_port=$((base_tor_ctrl_port + i)) 67 | tor_data_dir="/var/local/tor/${i}" 68 | mkdir -p "${tor_data_dir}" && chmod -R 700 "${tor_data_dir}" && chown -R proxy: "${tor_data_dir}" 69 | # spawn a child process to run the tor server at foreground so that logging to stdout is possible 70 | (tor --PidFile "${tor_data_dir}/tor.pid" \ 71 | --SocksPort 127.0.0.1:"${socks_port}" \ 72 | --ControlPort 127.0.0.1:"${ctrl_port}" \ 73 | --dataDirectory "${tor_data_dir}" 2>&1 | 74 | sed -r "s/^(\w+\ [0-9 :\.]+)(\[.*)[\r\n]?$/$(date -u +"%Y-%m-%dT%H:%M:%SZ") [tor#${i}] \2/") & 75 | # 76 | # start one privoxy instance connecting to the tor socks 77 | # 78 | http_port=$((base_http_port + i)) 79 | privoxy_data_dir="/var/local/privoxy/${i}" 80 | mkdir -p "${privoxy_data_dir}" && chown -R proxy: "${privoxy_data_dir}" 81 | cp /etc/privoxy/config.templ "${privoxy_data_dir}/config" 82 | sed -i \ 83 | -e 's@PLACEHOLDER_CONFDIR@'"${privoxy_data_dir}"'@g' \ 84 | -e 's@PLACEHOLDER_HTTP_PORT@'"${http_port}"'@g' \ 85 | -e 's@PLACEHOLDER_SOCKS_PORT@'"${socks_port}"'@g' \ 86 | "${privoxy_data_dir}/config" 87 | # spawn a child process 88 | (privoxy \ 89 | --no-daemon \ 90 | --pidfile "${privoxy_data_dir}/privoxy.pid" \ 91 | "${privoxy_data_dir}/config" 2>&1 | 92 | sed -r "s/^([0-9\-]+\ [0-9:\.]+\ [0-9a-f]+\ )([^:]+):\ (.*)[\r\n]?$/$(date -u +"%Y-%m-%dT%H:%M:%SZ") [privoxy#${i}] [\L\2] \E\3/") & 93 | # 94 | # "register" the privoxy instance to haproxy 95 | # 96 | echo " server privoxy${i} 127.0.0.1:${http_port} check" >>/etc/haproxy/haproxy.cfg 97 | done 98 | # 99 | # start an HAProxy instance 100 | # 101 | (haproxy -db -- /etc/haproxy/haproxy.cfg 2>&1 | 102 | sed -r "s/^(\[[^]]+]\ )?([\ 0-9\/\():]+)?(.*)[\r\n]?$/$(date -u +"%Y-%m-%dT%H:%M:%SZ") [haproxy] \L\1\E\3/") & 103 | # seems like haproxy starts logging only when the first request processed. We wait 15 seconds to build the first circuit then issue a 104 | # request to "activate" the HAProxy 105 | log "Wait 15 seconds to build the first Tor circuit" 106 | sleep 15 107 | curl -sx "http://127.0.0.1:3128" https://www.apple.com >/dev/null 108 | # 109 | # endless loop to reset circuits 110 | # 111 | while :; do 112 | log "Wait ${TOR_REBUILD_INTERVAL} seconds to rebuild all the tor circuits" 113 | sleep "$((TOR_REBUILD_INTERVAL))" 114 | log "Rebuilding all the tor circuits..." 115 | for ((i = 0; i < TOR_INSTANCES; i++)); do 116 | http_port=$((base_http_port + i)) 117 | IP=$(curl -sx "http://127.0.0.1:${http_port}" http://checkip.amazonaws.com) 118 | log "Current external IP address of proxy #${i}/${TOR_INSTANCES}: ${IP}" 119 | done 120 | done 121 | -------------------------------------------------------------------------------- /tor.cfg: -------------------------------------------------------------------------------- 1 | Log notice stdout 2 | HashedControlPassword 16:0E845EB82BCDB7BF604C82C0D8A5E4A4D44EDB7360098EBE6B099505D3 3 | RunAsDaemon 0 4 | NewCircuitPeriod 30 5 | MaxCircuitDirtiness 300 6 | UseEntryGuards 0 7 | LearnCircuitBuildTimeout 1 8 | ExitRelay 0 9 | RefuseUnknownExits 0 10 | ClientOnly 1 11 | --------------------------------------------------------------------------------