├── .vscode └── settings.json ├── funcs.go ├── .gitignore ├── .goreleaser.yaml ├── .github └── workflows │ ├── ci.yml │ └── build.yml ├── go.mod ├── LICENSE ├── README.md ├── models.go ├── cmd └── main.go ├── go.sum └── scanner.go /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.toolsEnvVars": { 3 | "GOPROXY": "https://proxy.golang.org" 4 | } 5 | } -------------------------------------------------------------------------------- /funcs.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | func shuffle(relays Relays) { 9 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 10 | r.Shuffle(len(relays), func(i, j int) { 11 | relays[i], relays[j] = relays[j], relays[i] 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | 23 | # tmp files 24 | out 25 | tor-relay-scanner-go -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod tidy 4 | builds: 5 | - env: 6 | - CGO_ENABLED=0 7 | main: ./cmd 8 | binary: tor-relay-scanner-go 9 | goos: 10 | - linux 11 | - windows 12 | - darwin 13 | archives: 14 | - 15 | format: binary 16 | name_template: >- 17 | {{ .ProjectName }}- 18 | {{- .Os }}- 19 | {{- if eq .Arch "amd64" }}amd64 20 | {{- else }}{{ .Arch }}{{ end }} 21 | checksum: 22 | name_template: 'checksums.txt' 23 | snapshot: 24 | name_template: "{{ incpatch .Version }}-next" 25 | changelog: 26 | sort: asc 27 | filters: 28 | exclude: 29 | - '^docs:' 30 | - '^test:' 31 | - '^bin' 32 | - '^Merge pull' -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | goreleaser: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - 13 | name: Checkout 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | - 18 | name: Set up Go 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: '>=1.20.0' 22 | check-latest: true 23 | - 24 | name: Run GoReleaser 25 | uses: goreleaser/goreleaser-action@v6 26 | with: 27 | distribution: goreleaser 28 | version: latest 29 | args: release --clean 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - 10 | name: Checkout 11 | uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | - 15 | name: Set up Go 16 | uses: actions/setup-go@v5 17 | with: 18 | go-version: '>=1.20.0' 19 | check-latest: true 20 | - 21 | name: Install dependencies 22 | run: | 23 | go version 24 | go install golang.org/x/lint/golint@latest 25 | - 26 | name: Run build 27 | run: go build . 28 | - 29 | name: Run vet & lint 30 | run: | 31 | go vet . 32 | golint . 33 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/juev/tor-relay-scanner-go 2 | 3 | go 1.23.0 4 | 5 | toolchain go1.24.0 6 | 7 | require ( 8 | github.com/carlmjohnson/requests v0.24.3 9 | github.com/gookit/color v1.5.4 10 | github.com/json-iterator/go v1.1.12 11 | github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 12 | github.com/schollz/progressbar/v3 v3.17.1 13 | github.com/sourcegraph/conc v0.3.0 14 | github.com/spf13/pflag v1.0.5 15 | ) 16 | 17 | require ( 18 | github.com/mattn/go-isatty v0.0.20 // indirect 19 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect 20 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 21 | github.com/modern-go/reflect2 v1.0.2 // indirect 22 | github.com/rivo/uniseg v0.4.7 // indirect 23 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 24 | go.uber.org/multierr v1.11.0 // indirect 25 | golang.org/x/net v0.38.0 // indirect 26 | golang.org/x/sys v0.31.0 // indirect 27 | golang.org/x/term v0.30.0 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Evsyukov Denis 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 | # Tor Relay Availability Checker 2 | 3 | This is golang fork of [ValdikSS/tor-relay-scanner](https://github.com/ValdikSS/tor-relay-scanner). 4 | 5 | --- 6 | 7 | This small program downloads all Tor Relay IP addresses from [onionoo.torproject.org](https://onionoo.torproject.org/) directly and via embedded proxies, and checks whether random Tor Relays are reachable from your Internet connection. 8 | 9 | It could be used to find working Relay in a countries with Internet censorship and blocked Tor, and use it as Bridge to connect to Tor network, bypassing standard well-known nodes embedded into Tor code. 10 | 11 | ## How to use with Tor (daemon) 12 | 13 | This utility is capable of generating `torrc` configuration file containing Bridge information. Launch it with the following arguments: 14 | 15 | `--torrc --outfile /etc/tor/bridges.conf` 16 | 17 | And append: 18 | 19 | `%include /etc/tor/bridges.conf` 20 | 21 | to the end of `/etc/tor/torrc` file to make Tor daemon load it. 22 | 23 | 24 | ## How to use as a standalone tool 25 | 26 | **Windows**: download ***.exe** file from [Releases](https://github.com/juev/tor-relay-scanner-go/releases) and run it in console (`start → cmd`) 27 | 28 | **Linux & macOS**: download binary file from [Releases](https://github.com/juev/tor-relay-scanner-go/releases) and run it: 29 | 30 | ```bash 31 | ./tor-relay-scanner-go 32 | ``` 33 | 34 | Tor-relay-scanner-go gets proxy information from environment. Or you can set it from variables: 35 | 36 | ```bash 37 | HTTP_PROXY=http://example.com:3128 HTTPS_PROXY=http://example.com:3128 ./tor-relay-scanner-go 38 | ``` 39 | -------------------------------------------------------------------------------- /models.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // TorRelayScanner ... 8 | type TorRelayScanner interface { 9 | Grab() (relays []ResultRelay) 10 | GetJSON() []byte 11 | } 12 | 13 | type torRelayScanner struct { 14 | relayInfo RelayInfo 15 | // The number of concurrent relays tested. default=30 16 | poolSize int 17 | // Test until at least this number of working relays are found. default=5 18 | goal int 19 | // Socket connection timeout. default=1s 20 | timeout time.Duration 21 | // Preferred alternative URL for onionoo relay list. Could be used multiple times 22 | urls []string 23 | // Scan for relays running on specified port number. Could be used multiple times 24 | ports []string 25 | // Exclude relays running on specified port number. Could be used multiple times 26 | excludePorts []string 27 | // Use ipv4 only nodes 28 | ipv4 bool 29 | // Use ipv6 only nodes 30 | ipv6 bool 31 | // Silent mode 32 | silent bool 33 | // Deadline time 34 | deadline time.Duration 35 | // Preferred country list, comma-separated. Example: se,gb,nl,det 36 | country string 37 | } 38 | 39 | type ( 40 | version string 41 | buildRevision string 42 | relaysPublished string 43 | bridgesPublished string 44 | bridges []any 45 | ) 46 | 47 | // RelayInfo struct with basics information relay lists 48 | type RelayInfo struct { 49 | Version version 50 | BuildRevision buildRevision `json:"build_revision"` 51 | RelaysPublished relaysPublished `json:"relays_published"` 52 | Relays Relays `json:"relays"` 53 | BridgesPublished bridgesPublished `json:"bridges_published"` 54 | Bridges bridges `json:"bridges"` 55 | } 56 | 57 | // Relays ... 58 | type Relays []Relay 59 | 60 | // Relay ... 61 | type Relay struct { 62 | Fingerprint string `json:"fingerprint"` 63 | OrAddresses []string `json:"or_addresses"` 64 | Country string `json:"country"` 65 | } 66 | 67 | // ResultRelay ... 68 | type ResultRelay struct { 69 | Fingerprint string `json:"fingerprint"` 70 | Address string `json:"or_addresses"` 71 | } 72 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "time" 7 | 8 | "github.com/gookit/color" 9 | flag "github.com/spf13/pflag" 10 | 11 | scanner "github.com/juev/tor-relay-scanner-go" 12 | ) 13 | 14 | var ( 15 | torRc, jsonRelays, silent bool 16 | outfile string 17 | ipv6 bool 18 | ) 19 | 20 | func main() { 21 | sc := createScanner() 22 | 23 | writer := setupOutputWriter() 24 | out := io.MultiWriter(writer) 25 | 26 | if jsonRelays { 27 | printJSONRelays(sc, out) 28 | return 29 | } 30 | 31 | printRelays(sc, out) 32 | } 33 | 34 | func setupOutputWriter() io.Writer { 35 | var writer io.Writer = os.Stdout 36 | if silent && outfile != "" { 37 | writer = io.Discard 38 | } 39 | if outfile != "" { 40 | logFile, err := os.OpenFile(outfile, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0666) 41 | if err != nil { 42 | color.Fprintf(os.Stderr, "cannot create file (%s): %s\n", outfile, err.Error()) 43 | os.Exit(2) 44 | } 45 | writer = io.MultiWriter(writer, logFile) 46 | } 47 | return writer 48 | } 49 | 50 | func printJSONRelays(sc scanner.TorRelayScanner, out io.Writer) { 51 | relays := sc.GetJSON() 52 | color.Fprintf(out, "%s\n", relays) 53 | } 54 | 55 | func printRelays(sc scanner.TorRelayScanner, out io.Writer) { 56 | relays := sc.Grab() 57 | if len(relays) == 0 { 58 | color.Fprintf(os.Stderr, "No relays are reachable this try.\n") 59 | os.Exit(3) 60 | } 61 | 62 | var prefix string 63 | if torRc { 64 | prefix = "Bridge " 65 | } 66 | 67 | for _, el := range relays { 68 | color.Fprintf(out, "%s%s %s\n", prefix, el.Address, el.Fingerprint) 69 | } 70 | if torRc { 71 | color.Fprintf(out, "UseBridges 1\n") 72 | if ipv6 { 73 | color.Fprintf(out, "ClientPreferIPv6ORPort 1\n") 74 | } 75 | } 76 | } 77 | 78 | func usage() { 79 | color.Fprintln(os.Stdout, "Usage of tor-relay-scanner-go:") 80 | flag.PrintDefaults() 81 | os.Exit(0) 82 | } 83 | 84 | func createScanner() scanner.TorRelayScanner { 85 | var poolSize, goal int 86 | var timeoutStr, deadlineStr, country string 87 | var urls, port, excludePort []string 88 | var ipv4 bool 89 | 90 | flag.IntVarP(&poolSize, "num_relays", "n", 100, `The number of concurrent relays tested.`) 91 | flag.IntVarP(&goal, "working_relay_num_goal", "g", 5, `Test until at least this number of working relays are found`) 92 | flag.StringVarP(&timeoutStr, "timeout", "t", "200ms", `Socket connection timeout`) 93 | flag.StringVarP(&outfile, "outfile", "o", "", `Output reachable relays to file`) 94 | flag.BoolVar(&torRc, "torrc", false, `Output reachable relays in torrc format (with "Bridge" prefix)`) 95 | flag.StringArrayVarP(&urls, "url", "u", []string{}, `Preferred alternative URL for onionoo relay list. Could be used multiple times.`) 96 | flag.StringArrayVarP(&port, "port", "p", []string{}, `Scan for relays running on specified port number. Could be used multiple times.`) 97 | flag.StringArrayVarP(&excludePort, "exclude_port", "x", []string{}, `Scan relays with exception of certain port number. Could be used multiple times.`) 98 | flag.BoolVarP(&ipv4, "ipv4", "4", false, `Use ipv4 only nodes`) 99 | flag.BoolVarP(&ipv6, "ipv6", "6", false, `Use ipv6 only nodes`) 100 | flag.BoolVarP(&jsonRelays, "json", "j", false, `Get available relays in json format`) 101 | flag.BoolVarP(&silent, "silent", "s", false, `Silent mode`) 102 | flag.StringVarP(&deadlineStr, "deadline", "d", "1m", `The deadline of program execution`) 103 | flag.StringVarP(&country, "preferred-country", "c", "", `Preferred country list, comma-separated. Example: se,gb,nl,det`) 104 | 105 | flag.Usage = usage 106 | flag.Parse() 107 | 108 | timeout := parseDuration(timeoutStr) 109 | deadline := parseDuration(deadlineStr) 110 | 111 | if timeout < 50*time.Millisecond { 112 | color.Println("It doesn't make sense to set a timeout of less than 50 ms") 113 | os.Exit(1) 114 | } 115 | 116 | if timeout > deadline { 117 | color.Println("The deadline must be greater than the timeout") 118 | os.Exit(1) 119 | } 120 | 121 | return scanner.New( 122 | poolSize, 123 | goal, 124 | timeout, 125 | urls, 126 | port, 127 | excludePort, 128 | ipv4, 129 | ipv6, 130 | silent, 131 | deadline, 132 | country, 133 | ) 134 | } 135 | 136 | func parseDuration(durationStr string) time.Duration { 137 | duration, err := time.ParseDuration(durationStr) 138 | if err != nil { 139 | color.Printf("cannot parse duration: %s\n", err) 140 | os.Exit(1) 141 | } 142 | return duration 143 | } 144 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/carlmjohnson/requests v0.24.3 h1:LYcM/jVIVPkioigMjEAnBACXl2vb42TVqiC8EYNoaXQ= 2 | github.com/carlmjohnson/requests v0.24.3/go.mod h1:duYA/jDnyZ6f3xbcF5PpZ9N8clgopubP2nK5i6MVMhU= 3 | github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM= 4 | github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 9 | github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= 10 | github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= 11 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 12 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 13 | github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213 h1:qGQQKEcAR99REcMpsXCp3lJ03zYT1PkRd3kQGPn9GVg= 14 | github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= 15 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 16 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 17 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 18 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 19 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= 20 | github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= 21 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 22 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 23 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 24 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 25 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 26 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 27 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 28 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 29 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 30 | github.com/schollz/progressbar/v3 v3.17.1 h1:bI1MTaoQO+v5kzklBjYNRQLoVpe0zbyRZNK6DFkVC5U= 31 | github.com/schollz/progressbar/v3 v3.17.1/go.mod h1:RzqpnsPQNjUyIgdglUjRLgD7sVnxN1wpmBMV+UiEbL4= 32 | github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= 33 | github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= 34 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 35 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 36 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 37 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 38 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 39 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 40 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= 41 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= 42 | go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= 43 | go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= 44 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= 45 | golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= 46 | golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= 47 | golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= 48 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 49 | golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= 50 | golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 51 | golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= 52 | golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= 53 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 54 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 55 | -------------------------------------------------------------------------------- /scanner.go: -------------------------------------------------------------------------------- 1 | package scanner 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "math/rand" 7 | "net" 8 | "net/url" 9 | "os" 10 | "slices" 11 | "strings" 12 | "time" 13 | 14 | "github.com/carlmjohnson/requests" 15 | "github.com/gookit/color" 16 | "github.com/k0kubun/go-ansi" 17 | "github.com/schollz/progressbar/v3" 18 | "github.com/sourcegraph/conc/pool" 19 | 20 | json "github.com/json-iterator/go" 21 | ) 22 | 23 | // New ... 24 | func New( 25 | poolSize int, 26 | goal int, 27 | timeout time.Duration, 28 | urlsList []string, 29 | port []string, 30 | excludePorts []string, 31 | ipv4 bool, 32 | ipv6 bool, 33 | silent bool, 34 | deadline time.Duration, 35 | country string, 36 | ) TorRelayScanner { 37 | baseURL := "https://onionoo.torproject.org/details?type=relay&running=true&fields=fingerprint,or_addresses,country" 38 | 39 | // Use public CORS proxy as a regular proxy in case if onionoo.torproject.org is unreachable 40 | urls := []string{ 41 | baseURL, 42 | "https://icors.vercel.app/?" + url.QueryEscape(baseURL), 43 | "https://github.com/ValdikSS/tor-onionoo-mirror/raw/master/details-running-relays-fingerprint-address-only.json", 44 | "https://bitbucket.org/ValdikSS/tor-onionoo-mirror/raw/master/details-running-relays-fingerprint-address-only.json", 45 | } 46 | 47 | // Additional urls should be first 48 | if len(urlsList) > 0 { 49 | urls = append(urlsList, urls...) 50 | } 51 | 52 | return &torRelayScanner{ 53 | poolSize: poolSize, 54 | goal: goal, 55 | timeout: timeout, 56 | urls: urls, 57 | ports: port, 58 | excludePorts: excludePorts, 59 | ipv4: ipv4, 60 | ipv6: ipv6, 61 | silent: silent, 62 | deadline: deadline, 63 | country: country, 64 | } 65 | } 66 | 67 | // Grab returns relay list from public addresses 68 | func (t *torRelayScanner) Grab() (relays []ResultRelay) { 69 | resultRelays := t.getRelays() 70 | if len(resultRelays) == 0 { 71 | return nil 72 | } 73 | 74 | r := rand.New(rand.NewSource(time.Now().UnixNano())) 75 | for _, el := range resultRelays { 76 | if len(el.OrAddresses) > 0 { 77 | relays = append(relays, ResultRelay{ 78 | Fingerprint: el.Fingerprint, 79 | Address: el.OrAddresses[r.Intn(len(el.OrAddresses))], 80 | }) 81 | } 82 | } 83 | 84 | return relays 85 | } 86 | 87 | // GetJSON returns available relays in json format 88 | func (t *torRelayScanner) GetJSON() []byte { 89 | resultRelays := t.getRelays() 90 | if len(resultRelays) == 0 { 91 | return nil 92 | } 93 | 94 | result, _ := json.MarshalIndent(RelayInfo{ 95 | Version: t.relayInfo.Version, 96 | BuildRevision: t.relayInfo.BuildRevision, 97 | RelaysPublished: t.relayInfo.RelaysPublished, 98 | Relays: resultRelays, 99 | BridgesPublished: t.relayInfo.BridgesPublished, 100 | Bridges: bridges{}, 101 | }, "", " ") 102 | 103 | return result 104 | } 105 | 106 | func (t *torRelayScanner) getRelays() Relays { 107 | if err := t.loadRelays(); err != nil { 108 | color.Fprintln(os.Stderr, "Tor Relay information can't be downloaded!") 109 | os.Exit(1) 110 | } 111 | 112 | chanRelays := make(chan Relay) 113 | go t.testRelays(chanRelays) 114 | 115 | bar := t.createProgressBar() 116 | 117 | var relays Relays 118 | for i := 1; i <= t.goal; i++ { 119 | select { 120 | case el, opened := <-chanRelays: 121 | if !opened { 122 | return relays 123 | } 124 | relays = append(relays, el) 125 | _ = bar.Add(1) 126 | case <-time.After(t.deadline): 127 | _ = bar.Add(t.goal) 128 | color.Fprintf(os.Stderr, "\nThe program was running for more than the specified time: %.2fs\n", t.deadline.Seconds()) 129 | return relays 130 | } 131 | } 132 | 133 | return relays 134 | } 135 | 136 | func (t *torRelayScanner) testRelays(chanRelays chan Relay) { 137 | p := pool.New().WithMaxGoroutines(t.poolSize) 138 | for _, el := range t.relayInfo.Relays { 139 | el := el 140 | p.Go(func() { 141 | if tcpSocketConnectChecker(el.OrAddresses[0], t.timeout) { 142 | chanRelays <- Relay{ 143 | Fingerprint: el.Fingerprint, 144 | OrAddresses: el.OrAddresses, 145 | Country: el.Country, 146 | } 147 | } 148 | }) 149 | } 150 | p.Wait() 151 | close(chanRelays) 152 | } 153 | 154 | func (t *torRelayScanner) createProgressBar() *progressbar.ProgressBar { 155 | return progressbar.NewOptions( 156 | t.goal, 157 | progressbar.OptionSetDescription("Testing"), 158 | progressbar.OptionSetWidth(15), 159 | progressbar.OptionSetWriter(ansi.NewAnsiStdout()), 160 | progressbar.OptionShowCount(), 161 | progressbar.OptionClearOnFinish(), 162 | progressbar.OptionEnableColorCodes(true), 163 | progressbar.OptionSetPredictTime(false), 164 | progressbar.OptionSetRenderBlankState(true), 165 | progressbar.OptionSetTheme(progressbar.Theme{ 166 | Saucer: "[green]=[reset]", 167 | SaucerHead: "[green]>[reset]", 168 | SaucerPadding: " ", 169 | BarStart: "[", 170 | BarEnd: "]", 171 | }), 172 | progressbar.OptionSetVisibility(!t.silent), 173 | ) 174 | } 175 | 176 | func (t *torRelayScanner) loadRelays() error { 177 | for _, addr := range t.urls { 178 | var err error 179 | t.relayInfo, err = t.grab(addr) 180 | if err == nil { 181 | break 182 | } 183 | } 184 | 185 | if len(t.relayInfo.Relays) == 0 { 186 | return errors.New("tor Relay information can't be downloaded") 187 | } 188 | 189 | t.relayInfo.Relays = t.filterRelays(t.relayInfo.Relays) 190 | 191 | if len(t.relayInfo.Relays) == 0 { 192 | return errors.New("there are no relays within specified port number constrains!\nTry changing port numbers") 193 | } 194 | 195 | shuffle(t.relayInfo.Relays) 196 | return nil 197 | } 198 | 199 | // filterRelays filters relays by country and addresses 200 | func (t *torRelayScanner) filterRelays(relays Relays) Relays { 201 | var filtered Relays 202 | for _, rel := range relays { 203 | if !t.filterCountry(rel) { 204 | continue 205 | } 206 | 207 | orAddresses := t.filterAddresses(rel.OrAddresses) 208 | if len(orAddresses) > 0 { 209 | rel.OrAddresses = orAddresses 210 | filtered = append(filtered, rel) 211 | } 212 | } 213 | return filtered 214 | } 215 | 216 | // filterCountry filters relays by country 217 | // if country is empty, it returns false 218 | // if relay's country is in the list of countries, it returns true 219 | func (t *torRelayScanner) filterCountry(relay Relay) bool { 220 | if t.country == "" { 221 | return true 222 | } 223 | 224 | return slices.Contains(strings.Split(t.country, ","), relay.Country) 225 | } 226 | 227 | func (t *torRelayScanner) filterAddresses(addresses []string) []string { 228 | var filtered []string 229 | for _, addr := range addresses { 230 | if t.skipAddrType(addr) || t.skipPorts(addr) { 231 | continue 232 | } 233 | filtered = append(filtered, addr) 234 | } 235 | return filtered 236 | } 237 | 238 | func (t *torRelayScanner) skipPorts(addr string) bool { 239 | u, _ := url.Parse("//" + addr) 240 | var skip bool 241 | if len(t.ports) > 0 && 242 | !slices.Contains(t.ports, u.Port()) { 243 | skip = true 244 | } 245 | if len(t.excludePorts) > 0 && 246 | slices.Contains(t.excludePorts, u.Port()) { 247 | skip = true 248 | } 249 | 250 | return skip 251 | } 252 | 253 | func (t *torRelayScanner) skipAddrType(addr string) bool { 254 | if t.ipv4 && !t.ipv6 { 255 | return addr[0] == '[' 256 | } 257 | if t.ipv6 && !t.ipv4 { 258 | return addr[0] != '[' 259 | } 260 | return false 261 | } 262 | 263 | func (t *torRelayScanner) grab(addr string) (RelayInfo, error) { 264 | var relayInfo RelayInfo 265 | err := requests. 266 | URL(addr). 267 | UserAgent("tor-relay-scanner"). 268 | ToJSON(&relayInfo). 269 | Fetch(context.Background()) 270 | if err != nil { 271 | return RelayInfo{}, err 272 | } 273 | 274 | return relayInfo, nil 275 | } 276 | 277 | // tcpSocketConnectChecker just checked network connection with specific host:port 278 | func tcpSocketConnectChecker(addr string, timeout time.Duration) bool { 279 | d := net.Dialer{Timeout: timeout} 280 | conn, err := d.Dial("tcp", addr) 281 | if err != nil { 282 | return false 283 | } 284 | _ = conn.Close() 285 | 286 | return true 287 | } 288 | --------------------------------------------------------------------------------