├── .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 | [](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 |
--------------------------------------------------------------------------------