├── .gitignore ├── LICENSE ├── README.md ├── go.mod ├── go.sum ├── ips.go ├── main.go ├── pinger.go └── scanner.go /.gitignore: -------------------------------------------------------------------------------- 1 | openheimer 2 | openheimer.db 3 | openheimer.log 4 | ips 5 | old-db 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ryan 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 | # OpenHeimer 2 | [![Discord](https://img.shields.io/discord/1100991922018979850?color=%235865F2&label=discord&logo=discord&style=for-the-badge)](https://discord.com/invite/55FxeEd6dx) 3 | 4 | **:warning: openheimer is undergoing a major rewrite in Rust (`rust` branch)** 5 | 6 | OpenHeimer is the (unofficial) open source version of the Copenheimer project. It scans all the IPv4 addresses in order to find Minecraft servers exposed to the internet. The original version is currently closed-source and not available to anyone else, that's why this version exists. 7 | 8 | :rocket: Capable of scanning up to 16,000 IP addresses per second, finishing everything in just 3 days! :rocket: 9 | 10 | ## Installation 11 | 12 | In order to use the current version of openheimer, you must compile from source. 13 | 14 | ### Requirements 15 | - git (or download the tarball/zip) 16 | - [go](https://go.dev/dl/) 17 | 18 | In your terminal, 19 | 20 | ```shell 21 | $ # Not necessary with tarball/zip 22 | $ git clone https://github.com/ErrorNoInternet/openheimer.git 23 | 24 | $ cd openheimer 25 | $ go build 26 | ``` 27 | 28 | You should now have an executable in the same directory. 29 | 30 | ## Usage 31 | Run `openheimer -help` to see a list of commands 32 | ``` 33 | Usage of ./openheimer: 34 | -database string 35 | The database to store the results in (default "openheimer.db") 36 | -ipFile string 37 | The file to extract IP addresses from 38 | -logFile string 39 | The file to store the logs in (default "openheimer.log") 40 | -maxPingWorkers int 41 | The maximum amount of workers to ping IPs (default 4000) 42 | -maxScanWorkers int 43 | The maximum amount of workers to scan IPs (default 1000) 44 | -startingIP string 45 | The IP address to start scanning from (default "1.0.0.0") 46 | -timeout int 47 | The amount of seconds to wait before timing out (default 5) 48 | -verbose 49 | Display everything that's happening 50 | -version 51 | Display the current version of OpenHeimer 52 | ``` 53 | Note: `maxPingWorkers` are the amount of workers to use to check for open ports. `maxScanWorkers` are the amount of workers to use to check if an IP has a valid Minecraft server. Normally you would increase `maxPingWorkers` to ping IP addresses faster, but when you're scanning IP addresses from a file (produced by masscan for example), you should increase `maxScanWorkers`. 54 | 55 | If you would like to modify or use this repository (including its code) in your own project, please be sure to credit! 56 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ErrorNoInternet/openheimer 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/Raqbit/mc-pinger v0.2.3 7 | github.com/peterbourgon/diskv/v3 v3.0.1 8 | ) 9 | 10 | require ( 11 | github.com/google/btree v1.1.2 // indirect 12 | github.com/pires/go-proxyproto v0.6.2 // indirect 13 | ) 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Raqbit/mc-pinger v0.2.3 h1:CpXd6s3V3Nduy8AWngg18qKwsunLAilRDZqC9F9/KDg= 2 | github.com/Raqbit/mc-pinger v0.2.3/go.mod h1:3BtpUQAShwS0ClpL0Mfs8Lr4W42tOOCbtMtw9mw3Kng= 3 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 4 | github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= 5 | github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 6 | github.com/peterbourgon/diskv/v3 v3.0.1 h1:x06SQA46+PKIUftmEujdwSEpIx8kR+M9eLYsUxeYveU= 7 | github.com/peterbourgon/diskv/v3 v3.0.1/go.mod h1:kJ5Ny7vLdARGU3WUuy6uzO6T0nb/2gWcT1JiBvRmb5o= 8 | github.com/pires/go-proxyproto v0.6.0 h1:cLJUPnuQdiNf7P/wbeOKmM1khVdaMgTFDLj8h9ZrVYk= 9 | github.com/pires/go-proxyproto v0.6.0/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY= 10 | github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8= 11 | github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY= 12 | -------------------------------------------------------------------------------- /ips.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "strconv" 8 | "strings" 9 | "time" 10 | ) 11 | 12 | func readFromFile(filePath string, outputChannel chan string) int { 13 | log.Printf("Reading IPs from %v...\n", filePath) 14 | fileData, err := ioutil.ReadFile(filePath) 15 | if err != nil { 16 | log.Fatalf("Unable to read file: %v\n", err.Error()) 17 | return 1 18 | } 19 | ips := strings.Split(strings.TrimSpace(string(fileData)), "\n") 20 | ok, index := validateIps(ips) 21 | if !ok { 22 | log.Fatalf("The IP address on line %v is invalid!\n", index+1) 23 | return 1 24 | } 25 | log.Printf("Successfully read %v IP addresses from file\n", len(ips)) 26 | for _, ip := range ips { 27 | if strings.TrimSpace(ip) != "" { 28 | outputChannel <- ip 29 | } 30 | } 31 | 32 | time.Sleep(100 * time.Millisecond) 33 | outputChannel <- "end" 34 | return 0 35 | } 36 | 37 | func generateIps(startingIp string, outputChannel chan string) int { 38 | log.Printf("Generating IPs from %v...\n", startingIp) 39 | ok, _ := validateIps([]string{startingIp}) 40 | if !ok { 41 | log.Println("The starting IP address you specified is invalid!") 42 | return 1 43 | } 44 | 45 | segments := strings.Split(startingIp, ".") 46 | segmentA, _ := strconv.Atoi(segments[0]) 47 | segmentB, _ := strconv.Atoi(segments[1]) 48 | segmentC, _ := strconv.Atoi(segments[2]) 49 | segmentD, _ := strconv.Atoi(segments[3]) 50 | for { 51 | serverIp := fmt.Sprintf("%v.%v.%v.%v", segmentA, segmentB, segmentC, segmentD) 52 | outputChannel <- serverIp 53 | 54 | segmentD += 1 55 | if segmentD > 255 { 56 | segmentD = 0 57 | segmentC += 1 58 | if segmentC > 255 { 59 | segmentC = 0 60 | segmentB += 1 61 | log.Printf("Scanning IP range %v.%v.*.*...\n", segmentA, segmentB) 62 | if segmentB > 255 { 63 | segmentB = 0 64 | segmentA += 1 65 | if segmentA > 255 { 66 | break 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | time.Sleep(100 * time.Millisecond) 74 | outputChannel <- "end" 75 | return 0 76 | } 77 | 78 | func validateIps(ips []string) (bool, int) { 79 | for index, ip := range ips { 80 | if strings.TrimSpace(ip) == "" { 81 | continue 82 | } 83 | if strings.Contains(ip, ":") { 84 | addressSegments := strings.Split(ip, ":") 85 | ip = addressSegments[0] 86 | port := addressSegments[1] 87 | 88 | segments := strings.Split(ip, ".") 89 | if len(segments) != 4 { 90 | return false, index 91 | } 92 | for _, segment := range segments { 93 | number, err := strconv.Atoi(segment) 94 | if err != nil { 95 | return false, index 96 | } 97 | if number < 0 || number > 255 { 98 | return false, index 99 | } 100 | } 101 | 102 | number, err := strconv.ParseUint(port, 10, 16) 103 | if err != nil { 104 | return false, index 105 | } 106 | if number < 0 || number > 65535 { 107 | return false, index 108 | } 109 | } else { 110 | segments := strings.Split(ip, ".") 111 | if len(segments) != 4 { 112 | return false, index 113 | } 114 | for _, segment := range segments { 115 | number, err := strconv.Atoi(segment) 116 | if err != nil { 117 | return false, index 118 | } 119 | if number < 0 || number > 255 { 120 | return false, index 121 | } 122 | } 123 | } 124 | } 125 | return true, -1 126 | } 127 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | "time" 10 | 11 | "github.com/peterbourgon/diskv/v3" 12 | ) 13 | 14 | var ( 15 | version string = "0.1.1" 16 | database *diskv.Diskv 17 | addressChannel chan string = make(chan string) 18 | scanQueue chan string = make(chan string) 19 | pingWorkers int 20 | scanWorkers int 21 | pinging bool 22 | scanning bool 23 | pinged int64 24 | scanned int64 25 | valid int64 26 | 27 | databasePath string 28 | logFile string 29 | ipFile string 30 | startingIp string 31 | timeout int 32 | maxPingWorkers int 33 | maxScanWorkers int 34 | verbose *bool 35 | ) 36 | 37 | func main() { 38 | flag.StringVar(&databasePath, "database", "openheimer.db", "The database to store the results in") 39 | flag.StringVar(&logFile, "logFile", "openheimer.log", "The file to store the logs in") 40 | flag.StringVar(&ipFile, "ipFile", "", "The file to extract IP addresses from") 41 | flag.StringVar(&startingIp, "startingIp", "1.0.0.0", "The IP address to start scanning from") 42 | flag.IntVar(&timeout, "timeout", 5, "The amount of seconds to wait before timing out") 43 | flag.IntVar(&maxPingWorkers, "maxPingWorkers", 4000, "The maximum amount of workers to ping IPs") 44 | flag.IntVar(&maxScanWorkers, "maxScanWorkers", 1000, "The maximum amount of workers to scan IPs") 45 | verbose = flag.Bool("verbose", false, "Display everything that's happening") 46 | displayVersion := flag.Bool("version", false, "Display the current version of OpenHeimer") 47 | flag.Parse() 48 | 49 | if *displayVersion { 50 | fmt.Printf("OpenHeimer v%v\n", version) 51 | return 52 | } 53 | 54 | startTime := time.Now().Unix() 55 | file, err := os.Create(logFile) 56 | if err != nil { 57 | log.Fatalf("Unable to create %v: %v\n", logFile, err.Error()) 58 | return 59 | } 60 | log.SetOutput(io.MultiWriter(os.Stdout, file)) 61 | flatTransform := func(s string) []string { return []string{} } 62 | database = diskv.New(diskv.Options{ 63 | BasePath: databasePath, 64 | Transform: flatTransform, 65 | CacheSizeMax: 1024 * 1024, 66 | }) 67 | go displayStatus() 68 | go pingIps() 69 | go scanIps() 70 | if ipFile != "" { 71 | result := readFromFile(ipFile, addressChannel) 72 | if result == 1 { 73 | return 74 | } 75 | } else { 76 | result := generateIps(startingIp, addressChannel) 77 | if result == 1 { 78 | return 79 | } 80 | } 81 | 82 | for pinging || scanning { 83 | time.Sleep(1 * time.Second) 84 | } 85 | log.Printf("Done! Finished in %v seconds. Pinged: %v, Scanned: %v, Valid: %v.\n", time.Now().Unix()-startTime, pinged, scanned, valid) 86 | } 87 | 88 | func displayStatus() { 89 | for { 90 | time.Sleep(5 * time.Second) 91 | log.Printf("Pinged: %v, Scanned: %v, Valid: %v\n", pinged, scanned, valid) 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /pinger.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | "strings" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | func pingIps() { 13 | pinging = true 14 | var mutex sync.Mutex 15 | if *verbose { 16 | log.Println("Starting IP pinger...") 17 | } 18 | 19 | for { 20 | for pingWorkers >= maxPingWorkers { 21 | time.Sleep(100 * time.Millisecond) 22 | } 23 | 24 | address := <-addressChannel 25 | if address == "end" { 26 | break 27 | } 28 | ip, port := address, "25565" 29 | if strings.Contains(address, ":") { 30 | segments := strings.Split(address, ":") 31 | ip = segments[0] 32 | port = segments[1] 33 | } 34 | go pingIp(ip, port, &mutex) 35 | mutex.Lock() 36 | pingWorkers++ 37 | pinged++ 38 | mutex.Unlock() 39 | } 40 | 41 | for pingWorkers > 0 { 42 | time.Sleep(1 * time.Millisecond) 43 | } 44 | scanQueue <- "end" 45 | pinging = false 46 | } 47 | 48 | func pingIp(ip string, port string, mutex *sync.Mutex) { 49 | connection, err := net.DialTimeout("tcp", net.JoinHostPort(ip, port), time.Second*time.Duration(timeout)) 50 | if err != nil { 51 | if *verbose { 52 | log.Printf("Unable to ping %v:%v: %v\n", ip, port, err.Error()) 53 | } 54 | mutex.Lock() 55 | pingWorkers-- 56 | mutex.Unlock() 57 | return 58 | } 59 | if connection != nil { 60 | defer connection.Close() 61 | scanQueue <- fmt.Sprintf("%v:%v", ip, port) 62 | } 63 | 64 | mutex.Lock() 65 | pingWorkers-- 66 | mutex.Unlock() 67 | } 68 | -------------------------------------------------------------------------------- /scanner.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "strconv" 8 | "strings" 9 | "sync" 10 | "time" 11 | 12 | mcpinger "github.com/Raqbit/mc-pinger" 13 | ) 14 | 15 | func scanIps() { 16 | scanning = true 17 | var mutex sync.Mutex 18 | if *verbose { 19 | log.Println("Starting IP scanner...") 20 | } 21 | 22 | for { 23 | for scanWorkers >= maxScanWorkers { 24 | time.Sleep(100 * time.Millisecond) 25 | } 26 | 27 | address := <-scanQueue 28 | if address == "end" { 29 | break 30 | } 31 | ip := address 32 | var port uint16 = 25565 33 | if strings.Contains(address, ":") { 34 | segments := strings.Split(address, ":") 35 | ip = segments[0] 36 | parsedPort, _ := strconv.ParseUint(segments[1], 10, 16) 37 | port = uint16(parsedPort) 38 | } 39 | go scanIp(ip, port, &mutex) 40 | mutex.Lock() 41 | scanWorkers++ 42 | scanned++ 43 | mutex.Unlock() 44 | } 45 | 46 | for scanWorkers > 0 { 47 | time.Sleep(1 * time.Millisecond) 48 | } 49 | scanning = false 50 | } 51 | 52 | func scanIp(ip string, port uint16, mutex *sync.Mutex) { 53 | if *verbose { 54 | log.Printf("Scanning %v:%v...\n", ip, port) 55 | } 56 | response, err := mcpinger.New(ip, port, mcpinger.WithTimeout(time.Second*time.Duration(timeout))).Ping() 57 | if err != nil { 58 | if *verbose { 59 | log.Printf("Unable to scan %v:%v: %v\n", ip, port, err.Error()) 60 | } 61 | mutex.Lock() 62 | scanWorkers-- 63 | mutex.Unlock() 64 | return 65 | } 66 | jsonObject, err := json.Marshal(response) 67 | if err != nil { 68 | log.Printf("Unable to marshal server response: %v\n", err.Error()) 69 | mutex.Lock() 70 | scanWorkers-- 71 | mutex.Unlock() 72 | return 73 | } 74 | log.Printf("Found Minecraft server at %v:%v\n", ip, port) 75 | err = database.Write(fmt.Sprintf("%v:%v", ip, port), jsonObject) 76 | if err != nil { 77 | log.Printf("Unable to write to database: %v\n", err.Error()) 78 | } 79 | 80 | mutex.Lock() 81 | scanWorkers-- 82 | valid++ 83 | mutex.Unlock() 84 | } 85 | --------------------------------------------------------------------------------