├── .gitignore ├── Test_test.go ├── LICENSE ├── README.md ├── IP.go └── Fetch.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.tmp 2 | *.test 3 | debug 4 | .vscode 5 | tor-ips.txt -------------------------------------------------------------------------------- /Test_test.go: -------------------------------------------------------------------------------- 1 | package ip2tor 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | // test code for manual debugging 9 | func Test1(t *testing.T) { 10 | Init(1, time.Hour*2, "tor-ips.txt") 11 | 12 | select {} 13 | } 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IP2Tor 2 | 3 | IP2Tor allows to check if an IP address is a Tor exit node. This can be used to identify and block traffic from the Tor network. IPv4 and IPv6 addresses are supported. 4 | 5 | It will run a daemon to download the Tor list from below sources and update it according to the time specified. It optionally uses a local file to cache the last results and allow continuous operation even after restart of your application. 6 | 7 | Tor node lists: 8 | * https://check.torproject.org/torbulkexitlist 9 | * https://www.dan.me.uk/tornodes 10 | 11 | It was observed that those lists only match about 72%, so they are both used as source by this package. 12 | 13 | ## Usage 14 | 15 | It is a Go package with no external dependencies. To download it: 16 | 17 | ```shell 18 | go get -u github.com/IntelligenceX/ip2tor 19 | ``` 20 | 21 | Then use it like this: 22 | 23 | ```go 24 | package main 25 | 26 | import ( 27 | "github.com/IntelligenceX/ip2tor" 28 | ) 29 | 30 | func init() { 31 | // Only download exit nodes, refetch the list every 2 hours. 32 | // Cache it to the file "tor-ips.txt". 33 | ip2tor.Init(1, time.Hour*2, "tor-ips.txt") 34 | } 35 | 36 | func main() { 37 | ip := net.ParseIP("1.2.3.4") 38 | 39 | ip2tor.IsTor(ip) // returns true or false 40 | } 41 | ``` -------------------------------------------------------------------------------- /IP.go: -------------------------------------------------------------------------------- 1 | /* 2 | File Name: IP.go 3 | Copyright: 2020 Kleissner Investments s.r.o. 4 | Author: Peter Kleissner 5 | */ 6 | 7 | package ip2tor 8 | 9 | import ( 10 | "net" 11 | "net/http" 12 | "time" 13 | ) 14 | 15 | var torIPs map[string]struct{} 16 | 17 | // Init starts the download daemon and optionally reads the cache file, if specified 18 | // Mode: 0 = disabled (no IP check), 1 = active (ban exit nodes only), 2 = active (ban all nodes), 3 = active, no fetching (only use file cache) 19 | func Init(mode int, waitTime time.Duration, filename string) { 20 | if mode == 0 { // disabled? 21 | torIPs = make(map[string]struct{}) 22 | return 23 | } 24 | 25 | var useCache bool 26 | torIPs, useCache = readCacheFile(filename) 27 | 28 | if mode == 3 { // only use file cache? 29 | startFileCacheFetcher(waitTime, filename) 30 | return 31 | } 32 | 33 | startDownloadDaemon(mode == 1, waitTime, filename, !useCache) 34 | } 35 | 36 | // IsTor checks if an IP address is listed as Tor IP 37 | func IsTor(IP net.IP) bool { 38 | if IP == nil { // invalid input? 39 | return false 40 | } 41 | 42 | _, ok := torIPs[IP.String()] 43 | 44 | return ok 45 | } 46 | 47 | // BlockTorMiddleware returns a middleware function to be used with mux.Router.Use(). Tor IPs will be denied access. 48 | func BlockTorMiddleware(BanStatusCode int, BanPayload []byte) func(http.Handler) http.Handler { 49 | return (func(next http.Handler) http.Handler { 50 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 51 | // parse IP:port 52 | host, _, _ := net.SplitHostPort(r.RemoteAddr) 53 | hostIP := net.ParseIP(host) 54 | 55 | // Is Tor? 56 | if IsTor(hostIP) { 57 | w.WriteHeader(BanStatusCode) 58 | w.Write(BanPayload) 59 | return 60 | } 61 | 62 | next.ServeHTTP(w, r) 63 | return 64 | }) 65 | }) 66 | } 67 | -------------------------------------------------------------------------------- /Fetch.go: -------------------------------------------------------------------------------- 1 | /* 2 | File Name: Fetch.go 3 | Copyright: 2020 Kleissner Investments s.r.o. 4 | Author: Peter Kleissner 5 | */ 6 | 7 | package ip2tor 8 | 9 | import ( 10 | "errors" 11 | "io/ioutil" 12 | "net" 13 | "net/http" 14 | "strings" 15 | "time" 16 | ) 17 | 18 | // URL for the official Tor exit node list. This list seems to match only about 72% IPs of the dan.me.uk one. 19 | const linkTorIPsOfficial = "https://check.torproject.org/torbulkexitlist" 20 | 21 | // URLs for downloading the Tor node list, full and exit-only. 22 | // These URLs shall not be downloaded more often than 30 minutes according to the website, otherwise it risks being blocked. 23 | const linkTorIPsFull = "https://www.dan.me.uk/torlist/" 24 | const linkTorIPsExit = "https://www.dan.me.uk/torlist/?exit" 25 | 26 | // startDownloadDaemon starts the download daemon according to the parameters 27 | // filename defines the file to use as cache. Empty to disable. 28 | func startDownloadDaemon(exitOnly bool, waitTime time.Duration, filename string, fetchImmediately bool) { 29 | if fetchImmediately { 30 | fetchTorLists(exitOnly, filename) 31 | } 32 | 33 | go func() { 34 | for { 35 | time.Sleep(waitTime) 36 | 37 | fetchTorLists(exitOnly, filename) 38 | } 39 | }() 40 | } 41 | 42 | func fetchTorLists(exitOnly bool, filename string) { 43 | ipMap := make(map[string]struct{}) 44 | 45 | // first download the official one 46 | err1 := downloadTorList(&ipMap, linkTorIPsOfficial) 47 | 48 | // second source 49 | dlLink := linkTorIPsFull 50 | if exitOnly { 51 | dlLink = linkTorIPsExit 52 | } 53 | err2 := downloadTorList(&ipMap, dlLink) 54 | 55 | // in case any of the sources fail, re-use the old list 56 | if err1 != nil || err2 != nil { 57 | for ipA := range torIPs { 58 | ipMap[ipA] = struct{}{} 59 | } 60 | } 61 | 62 | // update the live map 63 | torIPs = ipMap 64 | 65 | // write out cache file 66 | storeCacheFile(ipMap, filename) 67 | } 68 | 69 | // downloadTorList downloads the IP list and applies it to the map. 70 | func downloadTorList(ipMap *map[string]struct{}, link string) (err error) { 71 | data, err := downloadLink(link) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | records := strings.Fields(string(data)) 77 | 78 | for _, record := range records { 79 | ip := net.ParseIP(record) 80 | if ip == nil { 81 | continue 82 | } 83 | 84 | (*ipMap)[ip.String()] = struct{}{} 85 | } 86 | 87 | return nil 88 | } 89 | 90 | func downloadLink(link string) (buffer []byte, err error) { 91 | r, err := http.Get(link) 92 | if err != nil { 93 | return nil, err 94 | } 95 | defer r.Body.Close() 96 | 97 | if r.StatusCode != http.StatusOK { 98 | return nil, errors.New("Download failed") 99 | } 100 | 101 | return ioutil.ReadAll(r.Body) 102 | } 103 | 104 | // storeCacheFile stores the input map to the file specified 105 | func storeCacheFile(ipMap map[string]struct{}, filename string) { 106 | if filename == "" { 107 | return 108 | } 109 | 110 | var data string 111 | 112 | for ipA := range ipMap { 113 | data += ipA + "\n" 114 | } 115 | 116 | ioutil.WriteFile(filename, []byte(data), 0644) 117 | } 118 | 119 | // readCacheFile reads the cache file and processes it 120 | func readCacheFile(filename string) (ipMap map[string]struct{}, valid bool) { 121 | ipMap = make(map[string]struct{}) 122 | 123 | if filename == "" { 124 | return ipMap, false 125 | } 126 | 127 | data, err := ioutil.ReadFile(filename) 128 | if err != nil { 129 | return ipMap, false 130 | } 131 | 132 | records := strings.Fields(string(data)) 133 | 134 | for _, record := range records { 135 | ip := net.ParseIP(record) 136 | if ip == nil { 137 | continue 138 | } 139 | 140 | ipMap[ip.String()] = struct{}{} 141 | } 142 | 143 | return ipMap, true 144 | } 145 | 146 | // startFileCacheFetcher starts a Go routine to continuously reload the file cache 147 | func startFileCacheFetcher(waitTime time.Duration, filename string) { 148 | if filename == "" { 149 | return 150 | } 151 | 152 | go func() { 153 | for { 154 | time.Sleep(waitTime) 155 | 156 | if ipMap, valid := readCacheFile(filename); valid { 157 | torIPs = ipMap 158 | } 159 | } 160 | }() 161 | } 162 | --------------------------------------------------------------------------------