├── .gitignore ├── Dockerfile ├── README.md ├── haproxy.cfg.erb ├── privoxy.cfg.erb └── start.rb /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:edge 2 | 3 | RUN apk add 'tor' --no-cache \ 4 | --repository http://dl-cdn.alpinelinux.org/alpine/edge/community \ 5 | --repository http://dl-cdn.alpinelinux.org/alpine/edge/main \ 6 | --allow-untrusted haproxy ruby privoxy 7 | 8 | RUN apk --update add --virtual build-dependencies ruby-bundler ruby-dev \ 9 | && apk add ruby-nokogiri --update-cache --repository http://dl-4.alpinelinux.org/alpine/v3.3/main/ \ 10 | && gem install --no-ri --no-rdoc socksify \ 11 | && apk del build-dependencies \ 12 | && rm -rf /var/cache/apk/* 13 | 14 | 15 | ADD haproxy.cfg.erb /usr/local/etc/haproxy.cfg.erb 16 | ADD privoxy.cfg.erb /usr/local/etc/privoxy.cfg.erb 17 | 18 | ADD start.rb /usr/local/bin/start.rb 19 | RUN chmod +x /usr/local/bin/start.rb 20 | 21 | EXPOSE 2090 8118 5566 22 | 23 | CMD ruby /usr/local/bin/start.rb 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | alpine-tor 2 | ================== 3 | 4 | ``` 5 | Docker Container 6 | ------------------------------------- 7 | (Optional) <-> Tor Proxy 1 8 | Client <----> Privoxy <-> HAproxy <-> Tor Proxy 2 9 | <-> Tor Proxy n 10 | ``` 11 | 12 | Parents 13 | ------- 14 | * [rdsubhas/docker-tor-privoxy-alpine](https://github.com/rdsubhas/docker-tor-privoxy-alpine) 15 | * [Negashev/docker-haproxy-tor](https://github.com/Negashev/docker-haproxy-tor) 16 | * [marcelmaatkamp/docker-alpine-tor](https://github.com/marcelmaatkamp/docker-alpine-tor) 17 | * [mattes/rotating-proxy](https://github.com/mattes/rotating-proxy) 18 | 19 | __Why:__ Lots of IP addresses. One single endpoint for your client. 20 | Load-balancing by HAproxy. 21 | 22 | Optionaly adds support for [Privoxy](https://www.privoxy.org/) using 23 | `-e privoxy=1`, useful for http (default `8118`, changable via 24 | `-e privoxy_port=`) proxy forward and ad removal. 25 | 26 | Environment Variables 27 | ----- 28 | * `tors` - Integer, number of tor instances to run. (Default: 20) 29 | * `new_circuit_period` - Integer, NewCircuitPeriod parameter value in seconds. 30 | (Default: 2 minutes) 31 | * `max_circuit_dirtiness` - Integer, MaxCircuitDirtiness parameter value in 32 | seconds. (Default: 10 minutes) 33 | * `circuit_build_timeout` - Integer, CircuitBuildTimeout parameter value in 34 | seconds. (Default: 60 seconds) 35 | * `privoxy` - Boolean, whatever to run insance of privoxy in front of haproxy. 36 | * `privoxy_port` - Integer, port for privoxy. (Default: 8118) 37 | * `privoxy_permit` - Space-separated list of source addresses for permit-access option. (Default: Unset) 38 | * `privoxy_deny` - Space-separated list of source addresses for deny-access option. (Default: Unset) 39 | * `haproxy_port` - Integer, port for haproxy. (Default: 5566) 40 | * `haproxy_stats` - Integer, port for haproxy monitor. (Default: 2090) 41 | * `haproxy_login` and `haproxy_pass` - BasicAuth config for haproxy monitor. 42 | (Default: `admin` in both variables) 43 | * `test_url` - URL for health check throught Tor proxy. 44 | (Default: http://google.com) 45 | * `test_status` - Integer, HTTP status code for `test_url` in working case. 46 | (Default: 302) 47 | 48 | Usage 49 | ----- 50 | 51 | ```bash 52 | # build docker container 53 | docker build -t zeta0/alpine-tor:latest . 54 | 55 | # ... or pull docker container 56 | docker pull zeta0/alpine-tor:latest 57 | 58 | # start docker container 59 | docker run -d -p 5566:5566 -p 2090:2090 -e tors=25 zeta0/alpine-tor 60 | 61 | # start docker with privoxy enabled and exposed 62 | docker run -d -p 8118:8118 -p 2090:2090 -e tors=25 -e privoxy=1 zeta0/alpine-tor 63 | 64 | # test with ... 65 | curl --socks5 localhost:5566 http://httpbin.org/ip 66 | 67 | # or if privoxy enabled ... 68 | curl --proxy localhost:8118 http://httpbin.org/ip 69 | 70 | # or to run chromium with your new found proxy 71 | chromium --proxy-server="http://localhost:8118" \ 72 | --host-resolver-rules="MAP * 0.0.0.0 , EXCLUDE localhost" 73 | 74 | # monitor 75 | # auth login:admin 76 | # auth pass:admin 77 | http://localhost:2090 or http://admin:admin@localhost:2090 78 | 79 | # start docket container with new auth 80 | docker run -d -p 5566:5566 -p 2090:2090 -e haproxy_login=MySecureLogin \ 81 | -e haproxy_pass=MySecurePassword zeta0/alpine-tor 82 | ``` 83 | 84 | Further Readings 85 | ---------------- 86 | 87 | * [Tor Manual](https://www.torproject.org/docs/tor-manual.html.en) 88 | * [Tor Control](https://www.thesprawl.org/research/tor-control-protocol/) 89 | * [HAProxy Manual](http://cbonte.github.io/haproxy-dconv/index.html) 90 | * [Privoxy Manual](https://www.privoxy.org/user-manual/) 91 | -------------------------------------------------------------------------------- /haproxy.cfg.erb: -------------------------------------------------------------------------------- 1 | global 2 | daemon 3 | user root 4 | group root 5 | pidfile <%= pid_file %> 6 | 7 | defaults 8 | mode http 9 | maxconn 50000 10 | timeout client 3600s 11 | timeout connect 1s 12 | timeout queue 5s 13 | timeout server 3600s 14 | 15 | 16 | listen stats 17 | bind *:<%= stats %> 18 | mode http 19 | stats enable 20 | stats uri / 21 | stats auth <%= login %>:<%= pass %> 22 | 23 | 24 | listen TOR-in 25 | bind *:<%= port %> 26 | mode tcp 27 | default_backend TOR 28 | balance roundrobin 29 | 30 | backend TOR 31 | mode tcp 32 | <% backends.each do |b| %> 33 | server <%= b[:addr] %>:<%= b[:port] %> <%= b[:addr] %>:<%= b[:port] %> check 34 | <% end %> 35 | -------------------------------------------------------------------------------- /privoxy.cfg.erb: -------------------------------------------------------------------------------- 1 | listen-address 0.0.0.0:<%= port %> 2 | forward-socks5 / localhost:<%= haproxy %> . 3 | <% permit.to_s.split(' ').each do |host| %> 4 | permit-access <%= host %><% end %> 5 | <% deny.to_s.split(' ').each do |host| %> 6 | deny-access <%= host %><% end %> 7 | -------------------------------------------------------------------------------- /start.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'erb' 3 | require 'socksify/http' 4 | require 'logger' 5 | 6 | $logger = Logger.new(STDOUT, ENV['DEBUG'] ? Logger::DEBUG : Logger::INFO) 7 | 8 | module Service 9 | class Base 10 | attr_reader :port 11 | 12 | def initialize(port) 13 | @port = port 14 | end 15 | 16 | def service_name 17 | self.class.name.downcase.split('::').last 18 | end 19 | 20 | def start 21 | ensure_directories 22 | $logger.info "starting #{service_name} on port #{port}" 23 | end 24 | 25 | def ensure_directories 26 | %w{lib run log}.each do |dir| 27 | path = "/var/#{dir}/#{service_name}" 28 | Dir.mkdir(path) unless Dir.exists?(path) 29 | end 30 | end 31 | 32 | def data_directory 33 | "/var/lib/#{service_name}" 34 | end 35 | 36 | def pid_file 37 | "/var/run/#{service_name}/#{port}.pid" 38 | end 39 | 40 | def executable 41 | self.class.which(service_name) 42 | end 43 | 44 | def stop 45 | $logger.info "stopping #{service_name} on port #{port}" 46 | if File.exists?(pid_file) 47 | pid = File.read(pid_file).strip 48 | begin 49 | self.class.kill(pid.to_i) 50 | rescue => e 51 | $logger.warn "couldn't kill #{service_name} on port #{port}: #{e.message}" 52 | end 53 | else 54 | $logger.info "#{service_name} on port #{port} was not running" 55 | end 56 | end 57 | 58 | def self.kill(pid, signal='SIGINT') 59 | Process.kill(signal, pid) 60 | end 61 | 62 | def self.fire_and_forget(*args) 63 | $logger.debug "running: #{args.join(' ')}" 64 | pid = Process.fork 65 | if pid.nil? then 66 | # In child 67 | exec args.join(" ") 68 | else 69 | # In parent 70 | Process.detach(pid) 71 | end 72 | end 73 | 74 | def self.which(executable) 75 | path = `which #{executable}`.strip 76 | if path == "" 77 | return nil 78 | else 79 | return path 80 | end 81 | end 82 | end 83 | 84 | 85 | class Tor < Base 86 | attr_reader :new_circuit_period 87 | attr_reader :max_circuit_dirtiness 88 | attr_reader :circuit_build_timeout 89 | 90 | def initialize(port) 91 | @port = port 92 | @new_circuit_period = ENV['new_circuit_period'] || 120 93 | @max_circuit_dirtiness = ENV['max_circuit_dirtiness'] || 600 94 | @circuit_build_timeout = ENV['circuit_build_timeout'] || 60 95 | end 96 | 97 | def data_directory 98 | "#{super}/#{port}" 99 | end 100 | 101 | def start 102 | super 103 | self.class.fire_and_forget(executable, 104 | "--SocksPort #{port}", 105 | "--NewCircuitPeriod #{new_circuit_period}", 106 | "--MaxCircuitDirtiness #{max_circuit_dirtiness}", 107 | "--CircuitBuildTimeout #{circuit_build_timeout}", 108 | "--DataDirectory #{data_directory}", 109 | "--PidFile #{pid_file}", 110 | "--Log \"warn syslog\"", 111 | '--RunAsDaemon 1', 112 | "| logger -t 'tor' 2>&1") 113 | end 114 | end 115 | 116 | class Proxy 117 | attr_reader :id 118 | attr_reader :tor 119 | 120 | def initialize(id) 121 | @id = id 122 | @tor = Tor.new(tor_port) 123 | end 124 | 125 | def start 126 | $logger.info "starting proxy id #{id}" 127 | @tor.start 128 | end 129 | 130 | def stop 131 | $logger.info "stopping proxy id #{id}" 132 | @tor.stop 133 | end 134 | 135 | def restart 136 | stop 137 | sleep 5 138 | start 139 | end 140 | 141 | def tor_port 142 | 10000 + id 143 | end 144 | 145 | alias_method :port, :tor_port 146 | 147 | def test_url 148 | ENV['test_url'] || 'http://google.com' 149 | end 150 | 151 | def test_status 152 | ENV['test_status'] || '302' 153 | end 154 | 155 | def working? 156 | uri = URI.parse(test_url) 157 | Net::HTTP.SOCKSProxy('127.0.0.1', port).start(uri.host, uri.port) do |http| 158 | http.get(uri.path).code==test_status 159 | end 160 | rescue 161 | false 162 | end 163 | end 164 | 165 | class Haproxy < Base 166 | attr_reader :backends 167 | attr_reader :stats 168 | attr_reader :login 169 | attr_reader :pass 170 | 171 | def initialize() 172 | @config_erb_path = "/usr/local/etc/haproxy.cfg.erb" 173 | @config_path = "/usr/local/etc/haproxy.cfg" 174 | @backends = [] 175 | @stats = ENV['haproxy_stats'] || 2090 176 | @login = ENV['haproxy_login'] || 'admin' 177 | @pass = ENV['haproxy_pass'] || 'admin' 178 | @port = ENV['haproxy_port'] || 5566 179 | end 180 | 181 | def start 182 | super 183 | compile_config 184 | self.class.fire_and_forget(executable, 185 | "-f #{@config_path}", 186 | "| logger 2>&1") 187 | end 188 | 189 | def soft_reload 190 | self.class.fire_and_forget(executable, 191 | "-f #{@config_path}", 192 | "-p #{pid_file}", 193 | "-sf #{File.read(pid_file)}", 194 | "| logger 2>&1") 195 | end 196 | 197 | def add_backend(backend) 198 | @backends << {:name => 'tor', :addr => '127.0.0.1', :port => backend.port} 199 | end 200 | 201 | private 202 | def compile_config 203 | File.write(@config_path, ERB.new(File.read(@config_erb_path)).result(binding)) 204 | end 205 | end 206 | 207 | class Privoxy < Base 208 | attr_reader :haproxy 209 | attr_reader :permit 210 | attr_reader :deny 211 | 212 | def initialize() 213 | @config_erb_path = "/usr/local/etc/privoxy.cfg.erb" 214 | @config_path = "/usr/local/etc/privoxy.cfg" 215 | @port = ENV['privoxy_port'] || 8118 216 | @haproxy = ENV['haproxy_port'] || 5566 217 | @permit = ENV['privoxy_permit'] || "" 218 | @pdeny = ENV['privoxy_deny'] || "" 219 | end 220 | 221 | def start 222 | super 223 | compile_config 224 | self.class.fire_and_forget(executable, "--no-daemon", "#{@config_path}", "| logger 2>&1") 225 | end 226 | 227 | private 228 | def compile_config 229 | File.write(@config_path, ERB.new(File.read(@config_erb_path)).result(binding)) 230 | end 231 | end 232 | end 233 | 234 | 235 | haproxy = Service::Haproxy.new 236 | proxies = [] 237 | 238 | tor_instances = ENV['tors'] || 20 239 | tor_instances.to_i.times.each do |id| 240 | proxy = Service::Proxy.new(id) 241 | haproxy.add_backend(proxy) 242 | proxy.start 243 | proxies << proxy 244 | end 245 | 246 | haproxy.start 247 | 248 | if ENV['privoxy'] 249 | privoxy = Service::Privoxy.new 250 | privoxy.start 251 | end 252 | 253 | sleep 60 254 | 255 | loop do 256 | $logger.info "testing proxies" 257 | proxies.each do |proxy| 258 | $logger.info "testing proxy #{proxy.id} (port #{proxy.port})" 259 | proxy.restart unless proxy.working? 260 | $logger.info "sleeping for #{tor_instances} seconds" 261 | sleep Integer(tor_instances) 262 | end 263 | 264 | $logger.info "sleeping for 60 seconds" 265 | sleep 60 266 | end 267 | --------------------------------------------------------------------------------